diff --git a/app/but/bulletin_but.py b/app/but/bulletin_but.py
index 9441c031..9a886eff 100644
--- a/app/but/bulletin_but.py
+++ b/app/but/bulletin_but.py
@@ -77,11 +77,20 @@ class BulletinBUT(ResultatsSemestreBUT):
"saes": self.etud_ue_mod_results(etud, ue, self.saes),
}
if ue.type != UE_SPORT:
+ if sco_preferences.get_preference(
+ "bul_show_ue_rangs", self.formsemestre.id
+ ):
+ rangs, effectif = self.ue_rangs[ue.id]
+ rang = rangs[etud.id]
+ else:
+ rang, effectif = "", 0
d["moyenne"] = {
"value": fmt_note(self.etud_moy_ue[ue.id][etud.id]),
"min": fmt_note(self.etud_moy_ue[ue.id].min()),
"max": fmt_note(self.etud_moy_ue[ue.id].max()),
"moy": fmt_note(self.etud_moy_ue[ue.id].mean()),
+ "rang": rang,
+ "total": effectif, # nb etud avec note dans cette UE
}
else:
# ceci suppose que l'on a une seule UE bonus,
@@ -227,8 +236,8 @@ class BulletinBUT(ResultatsSemestreBUT):
"date_debut": formsemestre.date_debut.isoformat(),
"date_fin": formsemestre.date_fin.isoformat(),
"annee_universitaire": self.formsemestre.annee_scolaire_str(),
- "inscription": "TODO-MM-JJ", # XXX TODO
"numero": formsemestre.semestre_id,
+ "inscription": "", # inutilisé mais nécessaire pour le js de Seb.
"groupes": [], # XXX TODO
"absences": {
"injustifie": nbabsjust,
diff --git a/app/comp/aux_stats.py b/app/comp/aux_stats.py
index 07517f36..3337c2b8 100644
--- a/app/comp/aux_stats.py
+++ b/app/comp/aux_stats.py
@@ -21,7 +21,7 @@ class StatsMoyenne:
Les valeurs NAN ou non numériques sont toujours enlevées.
Si vals is None, renvoie des zéros (utilisé pour UE bonus)
"""
- if vals is None:
+ if vals is None or len(vals) == 0:
self.moy = self.min = self.max = self.size = self.nb_vals = 0
else:
self.moy = np.nanmean(vals)
diff --git a/app/comp/jury.py b/app/comp/jury.py
new file mode 100644
index 00000000..6581a2fb
--- /dev/null
+++ b/app/comp/jury.py
@@ -0,0 +1,154 @@
+##############################################################################
+# ScoDoc
+# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
+# See LICENSE
+##############################################################################
+
+"""Stockage des décisions de jury
+"""
+import pandas as pd
+
+from app import db
+from app.models import FormSemestre, ScolarFormSemestreValidation
+from app.comp.res_cache import ResultatsCache
+from app.scodoc import sco_cache
+from app.scodoc import sco_codes_parcours
+
+
+class ValidationsSemestre(ResultatsCache):
+ """ """
+
+ _cached_attrs = (
+ "decisions_jury",
+ "decisions_jury_ues",
+ "ue_capitalisees",
+ )
+
+ def __init__(self, formsemestre: FormSemestre):
+ super().__init__(formsemestre, sco_cache.ValidationsSemestreCache)
+
+ self.decisions_jury = {}
+ """Décisions prises dans ce semestre:
+ { etudid : { 'code' : None|ATT|..., 'assidu' : 0|1 }}"""
+ self.decisions_jury_ues = {}
+ """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()
+ self.store()
+
+ def compute(self):
+ "Charge les résultats de jury et UEs capitalisées"
+ self.ue_capitalisees = formsemestre_get_ue_capitalisees(self.formsemestre)
+ self.comp_decisions_jury()
+
+ def comp_decisions_jury(self):
+ """Cherche les decisions du jury pour le semestre (pas les UE).
+ Calcule les attributs:
+ decisions_jury = { etudid : { 'code' : None|ATT|..., 'assidu' : 0|1 }}
+ decision_jury_ues={ etudid : { ue_id : { 'code' : Note|ADM|CMP, 'event_date' }}}
+ Si la décision n'a pas été prise, la clé etudid n'est pas présente.
+ Si l'étudiant est défaillant, pas de décisions d'UE.
+ """
+ # repris de NotesTable.comp_decisions_jury pour la compatibilité
+ decisions_jury_q = ScolarFormSemestreValidation.query.filter_by(
+ formsemestre_id=self.formsemestre.id
+ )
+ decisions_jury = {}
+ for decision in decisions_jury_q.filter(
+ ScolarFormSemestreValidation.ue_id == None # slt dec. sem.
+ ):
+ decisions_jury[decision.etudid] = {
+ "code": decision.code,
+ "assidu": decision.assidu,
+ "compense_formsemestre_id": decision.compense_formsemestre_id,
+ "event_date": decision.event_date.strftime("%d/%m/%Y"),
+ }
+ self.decisions_jury = decisions_jury
+
+ # UEs:
+ decisions_jury_ues = {}
+ for decision in decisions_jury_q.filter(
+ ScolarFormSemestreValidation.ue_id != None # slt dec. sem.
+ ):
+ if decision.etudid not in decisions_jury_ues:
+ decisions_jury_ues[decision.etudid] = {}
+ # Calcul des ECTS associés à cette UE:
+ if sco_codes_parcours.code_ue_validant(decision.code):
+ ects = decision.ue.ects or 0.0 # 0 if None
+ else:
+ ects = 0.0
+
+ decisions_jury_ues[decision.etudid][decision.ue.id] = {
+ "code": decision.code,
+ "ects": ects, # 0. si UE non validée
+ "event_date": decision.event_date.strftime("%d/%m/%Y"),
+ }
+
+ self.decisions_jury_ues = decisions_jury_ues
+
+
+def formsemestre_get_ue_capitalisees(formsemestre: FormSemestre) -> pd.DataFrame:
+ """Liste des UE capitalisées (ADM) utilisables dans ce formsemestre
+
+ Recherche dans les semestres des formations de même code, avec le même semestre_id
+ et une date de début antérieure que celle du formsemestre.
+ Prend aussi les UE externes validées.
+
+ Attention: fonction très coûteuse, cacher le résultat.
+
+ Résultat: DataFrame avec
+ etudid (index)
+ 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 :
+ event_date :
+ } ]
+ """
+ query = """
+ SELECT DISTINCT SFV.*, ue.ue_code
+ FROM
+ notes_ue ue,
+ notes_formations nf,
+ notes_formations nf2,
+ scolar_formsemestre_validation SFV,
+ notes_formsemestre sem,
+ notes_formsemestre_inscription ins
+
+ WHERE ue.formation_id = nf.id
+ and nf.formation_code = nf2.formation_code
+ and nf2.id=%(formation_id)s
+ and ins.etudid = SFV.etudid
+ and ins.formsemestre_id = %(formsemestre_id)s
+
+ and SFV.ue_id = ue.id
+ and SFV.code = 'ADM'
+
+ and ( (sem.id = SFV.formsemestre_id
+ and sem.date_debut < %(date_debut)s
+ and sem.semestre_id = %(semestre_id)s )
+ or (
+ ((SFV.formsemestre_id is NULL) OR (SFV.is_external)) -- les UE externes ou "anterieures"
+ AND (SFV.semestre_id is NULL OR SFV.semestre_id=%(semestre_id)s)
+ ) )
+ """
+ params = {
+ "formation_id": formsemestre.formation.id,
+ "formsemestre_id": formsemestre.id,
+ "semestre_id": formsemestre.semestre_id,
+ "date_debut": formsemestre.date_debut,
+ }
+
+ df = pd.read_sql_query(query, db.engine, params=params, index_col="etudid")
+ return df
diff --git a/app/comp/moy_mod.py b/app/comp/moy_mod.py
index f890b6a6..f30f0491 100644
--- a/app/comp/moy_mod.py
+++ b/app/comp/moy_mod.py
@@ -77,6 +77,8 @@ class ModuleImplResults:
"{ evaluation.id : bool } indique si à prendre en compte ou non."
self.evaluations_etat = {}
"{ evaluation_id: EvaluationEtat }"
+ self.en_attente = False
+ "Vrai si au moins une évaluation a une note en attente"
#
self.evals_notes = None
"""DataFrame, colonnes: EVALS, Lignes: etudid (inscrits au SEMESTRE)
@@ -133,7 +135,7 @@ class ModuleImplResults:
evals_notes = pd.DataFrame(index=self.etudids, dtype=float)
self.evaluations_completes = []
self.evaluations_completes_dict = {}
-
+ self.en_attente = False
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
@@ -160,6 +162,8 @@ class ModuleImplResults:
self.evaluations_etat[evaluation.id] = EvaluationEtat(
evaluation_id=evaluation.id, nb_attente=nb_att, is_complete=is_complete
)
+ if nb_att > 0:
+ self.en_attente = True
# Force columns names to integers (evaluation ids)
evals_notes.columns = pd.Int64Index(
@@ -209,6 +213,13 @@ class ModuleImplResults:
* self.evaluations_completes
).reshape(-1, 1)
+ # was _list_notes_evals_titles
+ def get_evaluations_completes(self, moduleimpl: ModuleImpl) -> list:
+ "Liste des évaluations complètes"
+ return [
+ e for e in moduleimpl.evaluations if self.evaluations_completes_dict[e.id]
+ ]
+
def get_eval_notes_sur_20(self, moduleimpl: ModuleImpl) -> np.array:
"""Les notes des évaluations,
remplace les ATT, EXC, ABS, NaN par zéro et mets les notes sur 20.
diff --git a/app/comp/res_but.py b/app/comp/res_but.py
index c4697238..c5267596 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,12 +94,16 @@ 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
)
- self.etud_moy_gen_ranks = moy_sem.comp_ranks_series(self.etud_moy_gen)
+ # --- UE capitalisées
+ self.apply_capitalisation()
+
+ # --- Classements:
+ self.compute_rangs()
def get_etud_mod_moy(self, moduleimpl_id: int, etudid: int) -> float:
"""La moyenne de l'étudiant dans le moduleimpl
@@ -109,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_cache.py b/app/comp/res_cache.py
new file mode 100644
index 00000000..47c40b7e
--- /dev/null
+++ b/app/comp/res_cache.py
@@ -0,0 +1,34 @@
+##############################################################################
+# ScoDoc
+# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
+# See LICENSE
+##############################################################################
+
+"""Cache pour résultats (super classe)
+"""
+
+from app.models import FormSemestre
+
+
+class ResultatsCache:
+ _cached_attrs = () # virtual
+
+ def __init__(self, formsemestre: FormSemestre, cache_class=None):
+ self.formsemestre: FormSemestre = formsemestre
+ self.cache_class = cache_class
+
+ def load_cached(self) -> bool:
+ "Load cached dataframes, returns False si pas en cache"
+ data = self.cache_class.get(self.formsemestre.id)
+ if not data:
+ return False
+ for attr in self._cached_attrs:
+ setattr(self, attr, data[attr])
+ return True
+
+ def store(self):
+ "Cache our data"
+ self.cache_class.set(
+ self.formsemestre.id,
+ {attr: getattr(self, attr) for attr in self._cached_attrs},
+ )
diff --git a/app/comp/res_classic.py b/app/comp/res_classic.py
index 58c505ee..0d8f3242 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 app.comp import moy_mod, moy_ue, moy_sem, inscr_mod
+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,8 +113,11 @@ class ResultatsSemestreClassic(NotesTableCompat):
self.bonus = (
bonus_mg # compat nt, utilisé pour l'afficher sur les bulletins
)
+ # --- UE capitalisées
+ self.apply_capitalisation()
+
# --- Classements:
- self.etud_moy_gen_ranks = moy_sem.comp_ranks_series(self.etud_moy_gen)
+ self.compute_rangs()
def get_etud_mod_moy(self, moduleimpl_id: int, etudid: int) -> float:
"""La moyenne de l'étudiant dans le moduleimpl
@@ -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 47caaa03..09d3ed75 100644
--- a/app/comp/res_common.py
+++ b/app/comp/res_common.py
@@ -8,13 +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):
@@ -25,7 +34,7 @@ from app.scodoc.sco_codes_parcours import UE_SPORT, ATT, DEF
# (durée de vie de l'instance de ResultatsSemestre)
# qui sont notamment les attributs décorés par `@cached_property``
#
-class ResultatsSemestre:
+class ResultatsSemestre(ResultatsCache):
_cached_attrs = (
"etud_moy_gen_ranks",
"etud_moy_gen",
@@ -36,7 +45,7 @@ class ResultatsSemestre:
)
def __init__(self, formsemestre: FormSemestre):
- self.formsemestre: FormSemestre = formsemestre
+ super().__init__(formsemestre, ResultatsSemestreCache)
# BUT ou standard ? (apc == "approche par compétences")
self.is_apc = formsemestre.formation.is_apc()
# Attributs "virtuels", définis dans les sous-classes
@@ -46,26 +55,10 @@ class ResultatsSemestre:
self.etud_moy_gen = {}
self.etud_moy_gen_ranks = {}
self.modimpls_results: ModuleImplResults = None
+ "Résultats de chaque modimpl: dict { modimpl.id : ModuleImplResults(Classique ou BUT) }"
self.etud_coef_ue_df = None
- """coefs d'UE effectifs pour chaque etudiant (pour form. classiques)"""
-
- # TODO ?
-
- def load_cached(self) -> bool:
- "Load cached dataframes, returns False si pas en cache"
- data = ResultatsSemestreCache.get(self.formsemestre.id)
- if not data:
- return False
- for attr in self._cached_attrs:
- setattr(self, attr, data[attr])
- return True
-
- def store(self):
- "Cache our data"
- ResultatsSemestreCache.set(
- self.formsemestre.id,
- {attr: getattr(self, attr) for attr in self._cached_attrs},
- )
+ """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"
@@ -101,7 +94,8 @@ class ResultatsSemestre:
@cached_property
def ues(self) -> list[UniteEns]:
"""Liste des UEs du semestre (avec les UE bonus sport)
- (indices des DataFrames)
+ (indices des DataFrames).
+ Note: un étudiant n'est pas nécessairement inscrit dans toutes ces UEs.
"""
return self.formsemestre.query_ues(with_sport=True).all()
@@ -123,15 +117,34 @@ class ResultatsSemestre:
if m.module.module_type == scu.ModuleType.SAE
]
- @cached_property
- def ue_validables(self) -> list:
- """Liste des UE du semestre qui doivent être validées
- (toutes sauf le sport)
- """
- return self.formsemestre.query_ues().filter(UniteEns.type != UE_SPORT).all()
+ def get_etud_ue_validables(self, etudid: int) -> list[UniteEns]:
+ """Liste des UEs du semestre qui doivent être validées
- def modimpls_in_ue(self, ue_id, etudid):
- """Liste des modimpl de cet ue auxquels l'étudiant est inscrit"""
+ Rappel: l'étudiant est inscrit à des modimpls et non à des UEs.
+
+ - En BUT: on considère que l'étudiant va (ou non) valider toutes les UEs des modules
+ du parcours. XXX notion à implémenter, pour l'instant toutes les UE du semestre.
+
+ - En classique: toutes les UEs des modimpls auxquels l'étufdiant est inscrit sont
+ susceptibles d'être validées.
+
+ Les UE "bonus" (sport) ne sont jamais "validables".
+ """
+ if self.is_apc:
+ # TODO: introduire la notion de parcours (#sco93)
+ return self.formsemestre.query_ues().filter(UniteEns.type != UE_SPORT).all()
+ else:
+ # restreint aux UE auxquelles l'étudiant est inscrit (dans l'un des modimpls)
+ ues = {
+ modimpl.module.ue
+ for modimpl in self.formsemestre.modimpls_sorted
+ if self.modimpl_inscr_df[modimpl.id][etudid]
+ }
+ ues = sorted(list(ues), key=lambda x: x.numero or 0)
+ return ues
+
+ def modimpls_in_ue(self, ue_id, etudid) -> list[ModuleImpl]:
+ """Liste des modimpl de cette UE auxquels l'étudiant est inscrit"""
# sert pour l'affichage ou non de l'UE sur le bulletin
return [
modimpl
@@ -148,6 +161,115 @@ class ResultatsSemestre:
"""
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):
@@ -163,6 +285,8 @@ class NotesTableCompat(ResultatsSemestre):
"bonus",
"bonus_ues",
"malus",
+ "etud_moy_gen_ranks",
+ "ue_rangs",
)
def __init__(self, formsemestre: FormSemestre):
@@ -243,6 +367,35 @@ class NotesTableCompat(ResultatsSemestre):
modimpls_dict.append(d)
return modimpls_dict
+ def compute_rangs(self):
+ """Calcule les classements
+ Moyenne générale: etud_moy_gen_ranks
+ Par UE:
+ """
+ self.etud_moy_gen_ranks = moy_sem.comp_ranks_series(self.etud_moy_gen)
+ for ue in self.formsemestre.query_ues():
+ moy_ue = self.etud_moy_ue[ue.id]
+ self.ue_rangs[ue.id] = (
+ moy_sem.comp_ranks_series(moy_ue),
+ int(moy_ue.count()),
+ )
+ # .count() -> nb of non NaN values
+
+ def get_etud_decision_ues(self, etudid: int) -> dict:
+ """Decisions du jury pour les UE de cet etudiant, ou None s'il n'y en pas eu.
+ Ne tient pas compte des UE capitalisées.
+ { ue_id : { 'code' : ADM|CMP|AJ, 'event_date' : }
+ Ne renvoie aucune decision d'UE pour les défaillants
+ """
+ if self.get_etud_etat(etudid) == DEF:
+ return {}
+ else:
+ if not self.validations:
+ self.validations = res_sem.load_formsemestre_validations(
+ self.formsemestre
+ )
+ return self.validations.decisions_jury_ues.get(etudid, None)
+
def get_etud_decision_sem(self, etudid: int) -> dict:
"""Decision du jury prise pour cet etudiant, ou None s'il n'y en pas eu.
{ 'code' : None|ATT|..., 'assidu' : 0|1, 'event_date' : , compense_formsemestre_id }
@@ -256,12 +409,11 @@ class NotesTableCompat(ResultatsSemestre):
"compense_formsemestre_id": None,
}
else:
- return {
- "code": ATT, # XXX TODO
- "assidu": True, # XXX TODO
- "event_date": "",
- "compense_formsemestre_id": None,
- }
+ if not self.validations:
+ self.validations = res_sem.load_formsemestre_validations(
+ self.formsemestre
+ )
+ return self.validations.decisions_jury.get(etudid, None)
def get_etud_etat(self, etudid: int) -> str:
"Etat de l'etudiant: 'I', 'D', DEF ou '' (si pas connu dans ce semestre)"
@@ -290,13 +442,29 @@ class NotesTableCompat(ResultatsSemestre):
"""
return self.etud_moy_gen[etudid]
- def get_etud_ue_status(self, etudid: int, ue_id: int):
- coef_ue = self.etud_coef_ue_df[ue_id][etudid]
+ def get_etud_ects_pot(self, etudid: int) -> dict:
+ """
+ Un dict avec les champs
+ ects_pot : (float) nb de crédits ECTS qui seraient validés (sous réserve de validation par le jury),
+ ects_pot_fond: (float) nb d'ECTS issus d'UE fondamentales (non électives)
+
+ Ce sont les ECTS des UE au dessus de la barre (10/20 en principe), avant le jury (donc non
+ encore enregistrées).
+ """
+ # was nt.get_etud_moy_infos
+ # XXX pour compat nt, à remplacer ultérieurement
+ ues = self.get_etud_ue_validables(etudid)
+ ects_pot = 0.0
+ for ue in ues:
+ if (
+ ue.id in self.etud_moy_ue
+ and ue.ects is not None
+ and self.etud_moy_ue[ue.id][etudid] > self.parcours.NOTES_BARRE_VALID_UE
+ ):
+ ects_pot += ue.ects
return {
- "cur_moy_ue": self.etud_moy_ue[ue_id][etudid],
- "moy": self.etud_moy_ue[ue_id][etudid],
- "is_capitalized": False, # XXX TODO
- "coef_ue": coef_ue, # XXX TODO
+ "ects_pot": ects_pot,
+ "ects_fond": 0.0, # not implemented (anciennemment pour école ingé)
}
def get_etud_rang(self, etudid: int):
@@ -332,8 +500,32 @@ class NotesTableCompat(ResultatsSemestre):
evals_results.append(d)
return evals_results
+ def get_evaluations_etats(self):
+ """[ {...evaluation et son etat...} ]"""
+ # TODO: à moderniser
+ if not hasattr(self, "_evaluations_etats"):
+ self._evaluations_etats = sco_evaluations.do_evaluation_list_in_sem(
+ self.formsemestre.id
+ )
+
+ return self._evaluations_etats
+
+ def get_mod_evaluation_etat_list(self, moduleimpl_id) -> list[dict]:
+ """Liste des états des évaluations de ce module"""
+ # XXX TODO à moderniser: lent, recharge des donénes que l'on a déjà...
+ return [
+ e
+ for e in self.get_evaluations_etats()
+ if e["moduleimpl_id"] == moduleimpl_id
+ ]
+
def get_moduleimpls_attente(self):
- return [] # XXX TODO
+ """Liste des modimpls du semestre ayant des notes en attente"""
+ return [
+ modimpl
+ for modimpl in self.formsemestre.modimpls_sorted
+ if self.modimpls_results[modimpl.id].en_attente
+ ]
def get_mod_stats(self, moduleimpl_id: int) -> dict:
"""Stats sur les notes obtenues dans un modimpl
diff --git a/app/comp/res_sem.py b/app/comp/res_sem.py
index 8e14d5af..5da2c7f0 100644
--- a/app/comp/res_sem.py
+++ b/app/comp/res_sem.py
@@ -8,31 +8,49 @@
"""
from flask import g
+from app.comp.jury import ValidationsSemestre
from app.comp.res_common import ResultatsSemestre
from app.comp.res_classic import ResultatsSemestreClassic
from app.comp.res_but import ResultatsSemestreBUT
from app.models.formsemestre import FormSemestre
-def load_formsemestre_result(formsemestre: FormSemestre) -> ResultatsSemestre:
+def load_formsemestre_results(formsemestre: FormSemestre) -> ResultatsSemestre:
"""Returns ResultatsSemestre for this formsemestre.
Suivant le type de formation, retour une instance de
ResultatsSemestreClassic ou de ResultatsSemestreBUT.
Search in local cache (g.formsemestre_result_cache)
- then global app cache (eg REDIS)
If not in cache, build it and cache it.
"""
# --- Try local cache (within the same request context)
- if not hasattr(g, "formsemestre_result_cache"):
- g.formsemestre_result_cache = {} # pylint: disable=C0237
+ if not hasattr(g, "formsemestre_results_cache"):
+ g.formsemestre_results_cache = {} # pylint: disable=C0237
else:
- if formsemestre.id in g.formsemestre_result_cache:
- return g.formsemestre_result_cache[formsemestre.id]
+ if formsemestre.id in g.formsemestre_results_cache:
+ return g.formsemestre_results_cache[formsemestre.id]
klass = (
ResultatsSemestreBUT
if formsemestre.formation.is_apc()
else ResultatsSemestreClassic
)
- return klass(formsemestre)
+ g.formsemestre_results_cache[formsemestre.id] = klass(formsemestre)
+ return g.formsemestre_results_cache[formsemestre.id]
+
+
+def load_formsemestre_validations(formsemestre: FormSemestre) -> ValidationsSemestre:
+ """Charge les résultats de jury de ce semestre.
+ Search in local cache (g.formsemestre_result_cache)
+ If not in cache, build it and cache it.
+ """
+ if not hasattr(g, "formsemestre_validation_cache"):
+ g.formsemestre_validations_cache = {} # pylint: disable=C0237
+ else:
+ if formsemestre.id in g.formsemestre_validations_cache:
+ return g.formsemestre_validations_cache[formsemestre.id]
+
+ g.formsemestre_validations_cache[formsemestre.id] = ValidationsSemestre(
+ formsemestre
+ )
+ return g.formsemestre_validations_cache[formsemestre.id]
diff --git a/app/models/__init__.py b/app/models/__init__.py
index f1108493..d29b6bf3 100644
--- a/app/models/__init__.py
+++ b/app/models/__init__.py
@@ -49,13 +49,15 @@ from app.models.evaluations import (
)
from app.models.groups import Partition, GroupDescr, group_membership
from app.models.notes import (
- ScolarEvent,
- ScolarFormSemestreValidation,
- ScolarAutorisationInscription,
BulAppreciations,
NotesNotes,
NotesNotesLog,
)
+from app.models.validations import (
+ ScolarEvent,
+ ScolarFormSemestreValidation,
+ ScolarAutorisationInscription,
+)
from app.models.preferences import ScoPreference
from app.models.but_refcomp import (
diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py
index 2234fdfa..932e3fec 100644
--- a/app/models/formsemestre.py
+++ b/app/models/formsemestre.py
@@ -158,7 +158,7 @@ class FormSemestre(db.Model):
@cached_property
def modimpls_sorted(self) -> list[ModuleImpl]:
- """Liste des modimpls du semestre
+ """Liste des modimpls du semestre (y compris bonus)
- triée par type/numéro/code en APC
- triée par numéros d'UE/matières/modules pour les formations standard.
"""
@@ -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/models/notes.py b/app/models/notes.py
index fa8dc8d1..7e558357 100644
--- a/app/models/notes.py
+++ b/app/models/notes.py
@@ -8,100 +8,6 @@ from app.models import SHORT_STR_LEN
from app.models import CODE_STR_LEN
-class ScolarEvent(db.Model):
- """Evenement dans le parcours scolaire d'un étudiant"""
-
- __tablename__ = "scolar_events"
- id = db.Column(db.Integer, primary_key=True)
- event_id = db.synonym("id")
- etudid = db.Column(
- db.Integer,
- db.ForeignKey("identite.id"),
- )
- event_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
- formsemestre_id = db.Column(
- db.Integer,
- db.ForeignKey("notes_formsemestre.id"),
- )
- ue_id = db.Column(
- db.Integer,
- db.ForeignKey("notes_ue.id"),
- )
- # 'CREATION', 'INSCRIPTION', 'DEMISSION',
- # 'AUT_RED', 'EXCLUS', 'VALID_UE', 'VALID_SEM'
- # 'ECHEC_SEM'
- # 'UTIL_COMPENSATION'
- event_type = db.Column(db.String(SHORT_STR_LEN))
- # Semestre compensé par formsemestre_id:
- comp_formsemestre_id = db.Column(
- db.Integer,
- db.ForeignKey("notes_formsemestre.id"),
- )
-
-
-class ScolarFormSemestreValidation(db.Model):
- """Décisions de jury"""
-
- __tablename__ = "scolar_formsemestre_validation"
- # Assure unicité de la décision:
- __table_args__ = (db.UniqueConstraint("etudid", "formsemestre_id", "ue_id"),)
-
- id = db.Column(db.Integer, primary_key=True)
- formsemestre_validation_id = db.synonym("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,
- )
- ue_id = db.Column(
- db.Integer,
- db.ForeignKey("notes_ue.id"),
- index=True,
- )
- code = db.Column(db.String(CODE_STR_LEN), nullable=False, index=True)
- # NULL pour les UE, True|False pour les semestres:
- assidu = db.Column(db.Boolean)
- event_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
- # NULL sauf si compense un semestre:
- compense_formsemestre_id = db.Column(
- db.Integer,
- db.ForeignKey("notes_formsemestre.id"),
- )
- moy_ue = db.Column(db.Float)
- # (normalement NULL) indice du semestre, utile seulement pour
- # UE "antérieures" et si la formation définit des UE utilisées
- # dans plusieurs semestres (cas R&T IUTV v2)
- semestre_id = db.Column(db.Integer)
- # Si UE validée dans le cursus d'un autre etablissement
- is_external = db.Column(db.Boolean, default=False, server_default="false")
-
-
-class ScolarAutorisationInscription(db.Model):
- """Autorisation d'inscription dans un semestre"""
-
- __tablename__ = "scolar_autorisation_inscription"
- id = db.Column(db.Integer, primary_key=True)
- autorisation_inscription_id = db.synonym("id")
-
- etudid = db.Column(
- db.Integer,
- db.ForeignKey("identite.id"),
- )
- formation_code = db.Column(db.String(SHORT_STR_LEN), nullable=False)
- # semestre ou on peut s'inscrire:
- semestre_id = db.Column(db.Integer)
- date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
- origin_formsemestre_id = db.Column(
- db.Integer,
- db.ForeignKey("notes_formsemestre.id"),
- )
-
-
class BulAppreciations(db.Model):
"""Appréciations sur bulletins"""
diff --git a/app/models/validations.py b/app/models/validations.py
new file mode 100644
index 00000000..0bf487f3
--- /dev/null
+++ b/app/models/validations.py
@@ -0,0 +1,109 @@
+# -*- coding: UTF-8 -*
+
+"""Notes, décisions de jury, évènements scolaires
+"""
+
+from app import db
+from app.models import SHORT_STR_LEN
+from app.models import CODE_STR_LEN
+
+
+class ScolarFormSemestreValidation(db.Model):
+ """Décisions de jury"""
+
+ __tablename__ = "scolar_formsemestre_validation"
+ # Assure unicité de la décision:
+ __table_args__ = (db.UniqueConstraint("etudid", "formsemestre_id", "ue_id"),)
+
+ id = db.Column(db.Integer, primary_key=True)
+ formsemestre_validation_id = db.synonym("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,
+ )
+ ue_id = db.Column(
+ db.Integer,
+ db.ForeignKey("notes_ue.id"),
+ index=True,
+ )
+ code = db.Column(db.String(CODE_STR_LEN), nullable=False, index=True)
+ # NULL pour les UE, True|False pour les semestres:
+ assidu = db.Column(db.Boolean)
+ event_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
+ # NULL sauf si compense un semestre:
+ compense_formsemestre_id = db.Column(
+ db.Integer,
+ db.ForeignKey("notes_formsemestre.id"),
+ )
+ moy_ue = db.Column(db.Float)
+ # (normalement NULL) indice du semestre, utile seulement pour
+ # UE "antérieures" et si la formation définit des UE utilisées
+ # dans plusieurs semestres (cas R&T IUTV v2)
+ semestre_id = db.Column(db.Integer)
+ # Si UE validée dans le cursus d'un autre etablissement
+ is_external = db.Column(
+ db.Boolean, default=False, server_default="false", index=True
+ )
+
+ ue = db.relationship("UniteEns", lazy="select", uselist=False)
+
+ def __repr__(self):
+ return f"{self.__class__.__name__}({self.formsemestre_id}, {self.etudid}, code={self.code}, ue={self.ue_id}, moy_ue={self.moy_ue})"
+
+
+class ScolarAutorisationInscription(db.Model):
+ """Autorisation d'inscription dans un semestre"""
+
+ __tablename__ = "scolar_autorisation_inscription"
+ id = db.Column(db.Integer, primary_key=True)
+ autorisation_inscription_id = db.synonym("id")
+
+ etudid = db.Column(
+ db.Integer,
+ db.ForeignKey("identite.id"),
+ )
+ formation_code = db.Column(db.String(SHORT_STR_LEN), nullable=False)
+ # semestre ou on peut s'inscrire:
+ semestre_id = db.Column(db.Integer)
+ date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
+ origin_formsemestre_id = db.Column(
+ db.Integer,
+ db.ForeignKey("notes_formsemestre.id"),
+ )
+
+
+class ScolarEvent(db.Model):
+ """Evenement dans le parcours scolaire d'un étudiant"""
+
+ __tablename__ = "scolar_events"
+ id = db.Column(db.Integer, primary_key=True)
+ event_id = db.synonym("id")
+ etudid = db.Column(
+ db.Integer,
+ db.ForeignKey("identite.id"),
+ )
+ event_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
+ formsemestre_id = db.Column(
+ db.Integer,
+ db.ForeignKey("notes_formsemestre.id"),
+ )
+ ue_id = db.Column(
+ db.Integer,
+ db.ForeignKey("notes_ue.id"),
+ )
+ # 'CREATION', 'INSCRIPTION', 'DEMISSION',
+ # 'AUT_RED', 'EXCLUS', 'VALID_UE', 'VALID_SEM'
+ # 'ECHEC_SEM'
+ # 'UTIL_COMPENSATION'
+ event_type = db.Column(db.String(SHORT_STR_LEN))
+ # Semestre compensé par formsemestre_id:
+ comp_formsemestre_id = db.Column(
+ db.Integer,
+ db.ForeignKey("notes_formsemestre.id"),
+ )
diff --git a/app/scodoc/notes_table.py b/app/scodoc/notes_table.py
index 07cbd133..e5077543 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,
@@ -935,7 +936,7 @@ class NotesTable:
"""
return self.moy_gen[etudid]
- def get_etud_moy_infos(self, etudid):
+ def get_etud_moy_infos(self, etudid): # XXX OBSOLETE
"""Infos sur moyennes"""
return self.etud_moy_infos[etudid]
@@ -1011,7 +1012,10 @@ class NotesTable:
cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
cursor.execute(
- "select etudid, code, assidu, compense_formsemestre_id, event_date from scolar_formsemestre_validation where formsemestre_id=%(formsemestre_id)s and ue_id is NULL;",
+ """SELECT etudid, code, assidu, compense_formsemestre_id, event_date
+ FROM scolar_formsemestre_validation
+ WHERE formsemestre_id=%(formsemestre_id)s AND ue_id is NULL;
+ """,
{"formsemestre_id": self.formsemestre_id},
)
decisions_jury = {}
@@ -1137,8 +1141,14 @@ class NotesTable:
"""
self.ue_capitalisees = scu.DictDefault(defaultvalue=[])
cnx = None
+ semestre_id = self.sem["semestre_id"]
for etudid in self.get_etudids():
- capital = formsemestre_get_etud_capitalisation(self.sem, etudid)
+ capital = formsemestre_get_etud_capitalisation(
+ self.formation["id"],
+ semestre_id,
+ ndb.DateDMYtoISO(self.sem["date_debut"]),
+ etudid,
+ )
for ue_cap in capital:
# Si la moyenne d'UE n'avait pas été stockée (anciennes versions de ScoDoc)
# il faut la calculer ici et l'enregistrer
@@ -1308,7 +1318,7 @@ class NotesTable:
"""Liste des evaluations de ce semestre, avec leur etat"""
return self.get_evaluations_etats()
- def get_mod_evaluation_etat_list(self, moduleimpl_id):
+ def get_mod_evaluation_etat_list(self, moduleimpl_id) -> list[dict]:
"""Liste des évaluations de ce module"""
return [
e
diff --git a/app/scodoc/sco_bulletins.py b/app/scodoc/sco_bulletins.py
index 23833f88..d1127110 100644
--- a/app/scodoc/sco_bulletins.py
+++ b/app/scodoc/sco_bulletins.py
@@ -142,7 +142,7 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
prefs = sco_preferences.SemPreferences(formsemestre_id)
# nt = sco_cache.NotesTableCache.get(formsemestre_id) # > toutes notes
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
- nt: NotesTableCompat = res_sem.load_formsemestre_result(formsemestre)
+ nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
if not nt.get_etud_etat(etudid):
raise ScoValueError("Etudiant non inscrit à ce semestre")
I = scu.DictDefault(defaultvalue="")
diff --git a/app/scodoc/sco_cache.py b/app/scodoc/sco_cache.py
index 59ebab2d..5fd0cec6 100644
--- a/app/scodoc/sco_cache.py
+++ b/app/scodoc/sco_cache.py
@@ -59,9 +59,9 @@ import traceback
from flask import g
+from app import log
from app.scodoc import notesdb as ndb
from app.scodoc import sco_utils as scu
-from app import log
CACHE = None # set in app.__init__.py
@@ -293,6 +293,7 @@ def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=Fa
SemBulletinsPDFCache.invalidate_sems(formsemestre_ids)
ResultatsSemestreCache.delete_many(formsemestre_ids)
+ ValidationsSemestreCache.delete_many(formsemestre_ids)
class DefferedSemCacheManager:
@@ -319,10 +320,20 @@ class DefferedSemCacheManager:
# ---- Nouvelles classes ScoDoc 9.2
class ResultatsSemestreCache(ScoDocCache):
- """Cache pour les résultats ResultatsSemestre.
+ """Cache pour les résultats ResultatsSemestre (notes et moyennes)
Clé: formsemestre_id
Valeur: { un paquet de dataframes }
"""
prefix = "RSEM"
timeout = 60 * 60 # ttl 1 heure (en phase de mise au point)
+
+
+class ValidationsSemestreCache(ScoDocCache):
+ """Cache pour les résultats de jury d'un semestre
+ Clé: formsemestre_id
+ Valeur: un paquet de DataFrames
+ """
+
+ prefix = "VSC"
+ timeout = 60 * 60 # ttl 1 heure (en phase de mise au point)
diff --git a/app/scodoc/sco_codes_parcours.py b/app/scodoc/sco_codes_parcours.py
index 823dd19f..59372b8d 100644
--- a/app/scodoc/sco_codes_parcours.py
+++ b/app/scodoc/sco_codes_parcours.py
@@ -170,18 +170,18 @@ CODES_SEM_REO = {NAR: 1} # reorientation
CODES_UE_VALIDES = {ADM: True, CMP: True} # UE validée
-def code_semestre_validant(code):
+def code_semestre_validant(code: str) -> bool:
"Vrai si ce CODE entraine la validation du semestre"
return CODES_SEM_VALIDES.get(code, False)
-def code_semestre_attente(code):
+def code_semestre_attente(code: str) -> bool:
"Vrai si ce CODE est un code d'attente (semestre validable plus tard par jury ou compensation)"
return CODES_SEM_ATTENTES.get(code, False)
-def code_ue_validant(code):
- "Vrai si ce code entraine la validation de l'UE"
+def code_ue_validant(code: str) -> bool:
+ "Vrai si ce code entraine la validation des UEs du semestre."
return CODES_UE_VALIDES.get(code, False)
@@ -259,6 +259,7 @@ class TypeParcours(object):
) # par defaut, autorise tous les types d'UE
APC_SAE = False # Approche par compétences avec ressources et SAÉs
USE_REFERENTIEL_COMPETENCES = False # Lien avec ref. comp.
+ ECTS_FONDAMENTAUX_PER_YEAR = 0.0 # pour ISCID, deprecated
def check(self, formation=None):
return True, "" # status, diagnostic_message
diff --git a/app/scodoc/sco_edit_apc.py b/app/scodoc/sco_edit_apc.py
index c1c75319..c6b0151d 100644
--- a/app/scodoc/sco_edit_apc.py
+++ b/app/scodoc/sco_edit_apc.py
@@ -32,7 +32,7 @@ from flask_login import current_user
from app import db
from app.models import Formation, UniteEns, Matiere, Module, FormSemestre, ModuleImpl
-from app.models.notes import ScolarFormSemestreValidation
+from app.models.validations import ScolarFormSemestreValidation
from app.scodoc.sco_codes_parcours import UE_SPORT
import app.scodoc.sco_utils as scu
from app.scodoc import sco_groups
diff --git a/app/scodoc/sco_edit_module.py b/app/scodoc/sco_edit_module.py
index ab5292a7..a0c7b81f 100644
--- a/app/scodoc/sco_edit_module.py
+++ b/app/scodoc/sco_edit_module.py
@@ -123,9 +123,6 @@ def module_create(
Sinon, donne le choix de l'UE de rattachement et utilise la première
matière de cette UE (si elle n'existe pas, la crée).
"""
- from app.scodoc import sco_formations
- from app.scodoc import sco_edit_ue
-
if matiere_id:
matiere = Matiere.query.get_or_404(matiere_id)
ue = matiere.ue
@@ -160,7 +157,7 @@ def module_create(
else:
H += [
f"""Création {object_name} dans la matière {matiere.titre},
- (UE {ue.acronyme})
+ (UE {ue.acronyme}), semestre {ue.semestre_idx}
"""
]
@@ -534,19 +531,15 @@ def module_edit(module_id=None):
formsemestres=FormSemestre.query.filter(
ModuleImpl.formsemestre_id == FormSemestre.id,
ModuleImpl.module_id == module_id,
- ).all(),
+ )
+ .order_by(FormSemestre.date_debut)
+ .all(),
),
]
if not unlocked:
H.append(
"""Formation verrouillée, seuls certains éléments peuvent être modifiés
"""
)
- if in_use:
- H.append(
- """Module déjà utilisé dans des semestres,
- soyez prudents !
-
"""
- )
if is_apc:
module_types = scu.ModuleType # tous les types
else:
@@ -728,30 +721,9 @@ def module_edit(module_id=None):
initvalues=module,
submitlabel="Modifier ce module",
)
- # Affiche liste des formseemstre utilisant ce module
- if in_use:
- formsemestre_ids = {modimpl.formsemestre_id for modimpl in a_module.modimpls}
- formsemestres = [FormSemestre.query.get(fid) for fid in formsemestre_ids]
- formsemestres.sort(key=lambda f: f.date_debut)
- items = [
- f"""{f.titre}"""
- for f in formsemestres
- ]
- sem_descr = f"""
-
-
Ce module est utilisé dans les formsemestres suivants:
-
-
- """
- else:
- sem_descr = ""
#
if tf[0] == 0:
- return "\n".join(H) + tf[1] + sem_descr + html_sco_header.sco_footer()
+ return "\n".join(H) + tf[1] + html_sco_header.sco_footer()
elif tf[0] == -1:
return flask.redirect(
url_for(
diff --git a/app/scodoc/sco_edit_ue.py b/app/scodoc/sco_edit_ue.py
index 0615db1e..4fb2eb58 100644
--- a/app/scodoc/sco_edit_ue.py
+++ b/app/scodoc/sco_edit_ue.py
@@ -244,18 +244,22 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
"""Formulaire modification ou création d'une UE"""
create = int(create)
if not create:
- U = ue_list(args={"ue_id": ue_id})
- if not U:
- raise ScoValueError("UE inexistante !")
- U = U[0]
- formation_id = U["formation_id"]
- title = "Modification de l'UE %(titre)s" % U
- initvalues = U
+ ue: UniteEns = UniteEns.query.get_or_404(ue_id)
+ ue_dict = ue.to_dict()
+ formation_id = ue.formation_id
+ title = f"Modification de l'UE {ue.acronyme} {ue.titre}"
+ initvalues = ue_dict
submitlabel = "Modifier les valeurs"
+ can_change_semestre_id = ue.modules.count() == 0
else:
+ ue = None
title = "Création d'une UE"
- initvalues = {"semestre_idx": default_semestre_idx}
+ initvalues = {
+ "semestre_idx": default_semestre_idx,
+ "color": ue_guess_color_default(formation_id, default_semestre_idx),
+ }
submitlabel = "Créer cette UE"
+ can_change_semestre_id = True
formation = Formation.query.get(formation_id)
if not formation:
raise ScoValueError(f"Formation inexistante ! (id={formation_id})")
@@ -282,7 +286,7 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
ue_types_names = [sco_codes_parcours.UE_TYPE_NAME[k] for k in ue_types]
ue_types = [str(x) for x in ue_types]
- fw = [
+ form_descr = [
("ue_id", {"input_type": "hidden"}),
("create", {"input_type": "hidden", "default": create}),
("formation_id", {"input_type": "hidden", "default": formation_id}),
@@ -296,18 +300,28 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
"type": "int",
},
),
- (
- "semestre_idx",
- {
- "input_type": "menu",
- "type": "int",
- "allow_null": False,
- "title": parcours.SESSION_NAME.capitalize(),
- "explanation": "%s de l'UE dans la formation" % parcours.SESSION_NAME,
- "labels": ["non spécifié"] + [str(x) for x in semestres_indices],
- "allowed_values": [""] + semestres_indices,
- },
- ),
+ ]
+ if can_change_semestre_id:
+ form_descr += [
+ (
+ "semestre_idx",
+ {
+ "input_type": "menu",
+ "type": "int",
+ "allow_null": False,
+ "title": parcours.SESSION_NAME.capitalize(),
+ "explanation": "%s de l'UE dans la formation"
+ % parcours.SESSION_NAME,
+ "labels": ["non spécifié"] + [str(x) for x in semestres_indices],
+ "allowed_values": [""] + semestres_indices,
+ },
+ ),
+ ]
+ else:
+ form_descr += [
+ ("semestre_idx", {"default": ue.semestre_idx, "input_type": "hidden"}),
+ ]
+ form_descr += [
(
"type",
{
@@ -377,7 +391,7 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
),
]
if create and not parcours.UE_IS_MODULE and not is_apc:
- fw.append(
+ form_descr.append(
(
"create_matiere",
{
@@ -391,14 +405,33 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
tf = TrivialFormulator(
request.base_url,
scu.get_request_args(),
- fw,
+ form_descr,
initvalues=initvalues,
submitlabel=submitlabel,
)
if tf[0] == 0:
- ue_div = """"""
+ if ue and ue.modules.count():
+ modules_div = f"""
+
{ue.modules.count()} modules sont rattachés
+ à cette UE du semestre S{ue.semestre_idx},
+ elle ne peut donc pas être changée de semestre.
+
"""
+ for m in ue.modules:
+ modules_div += f"""- {m.code} {m.titre}
"""
+ modules_div += """
"""
+ else:
+ modules_div = ""
bonus_div = """"""
- return "\n".join(H) + tf[1] + bonus_div + ue_div + html_sco_header.sco_footer()
+ ue_div = """"""
+ return (
+ "\n".join(H)
+ + tf[1]
+ + modules_div
+ + bonus_div
+ + ue_div
+ + html_sco_header.sco_footer()
+ )
else:
if create:
if not tf[2]["ue_code"]:
@@ -547,14 +580,15 @@ def ue_table(formation_id=None, semestre_idx=1, msg=""): # was ue_list
formation_id=formation_id, is_external=True
)
if is_apc:
- # pour faciliter la transition des anciens programmes non APC
+ # Pour faciliter la transition des anciens programmes non APC
for ue in ues_obj:
ue.guess_semestre_idx()
- # vérifie qu'on a bien au moins une matière dans chaque UE
- for ue in ues_obj:
+ # vérifie qu'on a bien au moins une matière dans chaque UE
if ue.matieres.count() < 1:
mat = Matiere(ue_id=ue.id)
db.session.add(mat)
+ # donne des couleurs aux UEs crées avant
+ colorie_anciennes_ues(ues_obj)
db.session.commit()
ues = [ue.to_dict() for ue in ues_obj]
ues_externes = [ue.to_dict() for ue in ues_externes_obj]
@@ -1370,7 +1404,7 @@ def formation_table_recap(formation_id, format="html"):
return tab.make_page(format=format)
-def ue_list_semestre_ids(ue):
+def ue_list_semestre_ids(ue: dict):
"""Liste triée des numeros de semestres des modules dans cette UE
Il est recommandable que tous les modules d'une UE aient le même indice de semestre.
Mais cela n'a pas toujours été le cas dans les programmes pédagogiques officiels,
@@ -1378,3 +1412,45 @@ def ue_list_semestre_ids(ue):
"""
modules = sco_edit_module.module_list(args={"ue_id": ue["ue_id"]})
return sorted(list(set([mod["semestre_id"] for mod in modules])))
+
+
+UE_PALETTE = [
+ "#EFA00B",
+ "#99C24D",
+ "#EC9192",
+ "#0075C4",
+ "#D65108",
+ "#DEC0F1",
+ "#B02E0C",
+ "#151E3F",
+ "#FB3640",
+]
+
+
+def colorie_anciennes_ues(ues: list[UniteEns]) -> None:
+ """Avant ScoDoc 9.2, les ue n'avaient pas de couleurs
+ Met des défauts raisonnables
+ """
+ nb_colors = len(UE_PALETTE)
+ index = 0
+ last_sem_idx = 0
+ for ue in ues:
+ if ue.semestre_idx != last_sem_idx:
+ last_sem_idx = ue.semestre_idx
+ index = 0
+ if ue.color is None:
+ ue.color = UE_PALETTE[index % nb_colors]
+ index += 1
+ db.session.add(ue)
+
+
+def ue_guess_color_default(formation_id: int, default_semestre_idx: int) -> str:
+ """Un code couleur pour une nouvelle UE dans ce semestre"""
+ nb_colors = len(UE_PALETTE)
+ # UE existantes dans ce semestre:
+ nb_ues = UniteEns.query.filter(
+ UniteEns.formation_id == formation_id,
+ UniteEns.semestre_idx == default_semestre_idx,
+ ).count()
+ index = nb_ues
+ return UE_PALETTE[index % nb_colors]
diff --git a/app/scodoc/sco_evaluations.py b/app/scodoc/sco_evaluations.py
index 15b690a3..c54806e2 100644
--- a/app/scodoc/sco_evaluations.py
+++ b/app/scodoc/sco_evaluations.py
@@ -393,9 +393,8 @@ def do_evaluation_etat_in_mod(nt, moduleimpl_id):
""""""
evals = nt.get_mod_evaluation_etat_list(moduleimpl_id)
etat = _eval_etat(evals)
- etat["attente"] = moduleimpl_id in [
- m["moduleimpl_id"] for m in nt.get_moduleimpls_attente()
- ] # > liste moduleimpl en attente
+ # Il y a-t-il des notes en attente dans ce module ?
+ etat["attente"] = nt.modimpls_results[moduleimpl_id].en_attente
return etat
diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py
index 1f440c8f..fcab0e7d 100644
--- a/app/scodoc/sco_formsemestre_status.py
+++ b/app/scodoc/sco_formsemestre_status.py
@@ -991,9 +991,9 @@ def formsemestre_status(formsemestre_id=None):
modimpls = sco_moduleimpl.moduleimpl_withmodule_list(
formsemestre_id=formsemestre_id
)
- nt = sco_cache.NotesTableCache.get(formsemestre_id)
- # WIP formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
- # WIP nt = res_sem.load_formsemestre_result(formsemestre)
+ # nt = sco_cache.NotesTableCache.get(formsemestre_id)
+ formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
+ nt = res_sem.load_formsemestre_results(formsemestre)
# Construit la liste de tous les enseignants de ce semestre:
mails_enseignants = set(
diff --git a/app/scodoc/sco_formsemestre_validation.py b/app/scodoc/sco_formsemestre_validation.py
index baee42f3..e835f1dd 100644
--- a/app/scodoc/sco_formsemestre_validation.py
+++ b/app/scodoc/sco_formsemestre_validation.py
@@ -31,15 +31,21 @@ import time
import flask
from flask import url_for, g, request
+from app.api.sco_api import formsemestre
import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu
from app import log
-from app.scodoc.scolog import logdb
-from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message
-from app.scodoc.sco_exceptions import ScoValueError
+from app.comp import res_sem
+from app.comp.res_common import NotesTableCompat
+from app.models import FormSemestre
+
+from app.scodoc.sco_exceptions import ScoValueError
+from app.scodoc.scolog import logdb
from app.scodoc.sco_codes_parcours import *
+from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message
+
from app.scodoc import html_sco_header
from app.scodoc import sco_abs
from app.scodoc import sco_codes_parcours
@@ -542,9 +548,9 @@ def formsemestre_recap_parcours_table(
else:
ass = ""
- nt = sco_cache.NotesTableCache.get(
- sem["formsemestre_id"]
- ) # > get_ues_stat_dict, get_etud_moy_gen, get_etud_ue_status
+ formsemestre = FormSemestre.query.get(sem["formsemestre_id"])
+ nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
+ # nt = sco_cache.NotesTableCache.get(sem["formsemestre_id"])
if is_cur:
type_sem = "*" # now unused
class_sem = "sem_courant"
@@ -686,7 +692,7 @@ def formsemestre_recap_parcours_table(
sco_preferences.get_preference("bul_show_ects", sem["formsemestre_id"])
or nt.parcours.ECTS_ONLY
):
- etud_moy_infos = nt.get_etud_moy_infos(etudid)
+ etud_ects_infos = nt.get_etud_ects_pot(etudid)
H.append(
'' % (class_sem, sem["formsemestre_id"])
)
@@ -697,7 +703,7 @@ def formsemestre_recap_parcours_table(
# total ECTS (affiché sous la moyenne générale)
H.append(
'ECTS: | %g %g | '
- % (etud_moy_infos["ects_pot"], etud_moy_infos["ects_pot_fond"])
+ % (etud_ects_infos["ects_pot"], etud_ects_infos["ects_pot_fond"])
)
H.append(' | ')
# ECTS validables dans chaque UE
@@ -1056,7 +1062,7 @@ def formsemestre_validate_previous_ue(formsemestre_id, etudid):
"title": "Indice du semestre",
"explanation": "Facultatif: indice du semestre dans la formation",
"allow_null": True,
- "allowed_values": [""] + [str(x) for x in range(11)],
+ "allowed_values": [""] + [x for x in range(11)],
"labels": ["-"] + list(range(11)),
},
),
diff --git a/app/scodoc/sco_liste_notes.py b/app/scodoc/sco_liste_notes.py
index a342e312..5d2871af 100644
--- a/app/scodoc/sco_liste_notes.py
+++ b/app/scodoc/sco_liste_notes.py
@@ -837,7 +837,7 @@ def _add_apc_columns(
# => On recharge tout dans les nouveaux modèles
# rows est une liste de dict avec une clé "etudid"
# on va y ajouter une clé par UE du semestre
- nt: NotesTableCompat = res_sem.load_formsemestre_result(modimpl.formsemestre)
+ nt: NotesTableCompat = res_sem.load_formsemestre_results(modimpl.formsemestre)
modimpl_results: ModuleImplResults = nt.modimpls_results[modimpl.id]
# XXX A ENLEVER TODO
diff --git a/app/scodoc/sco_parcours_dut.py b/app/scodoc/sco_parcours_dut.py
index 49c78263..0d682c01 100644
--- a/app/scodoc/sco_parcours_dut.py
+++ b/app/scodoc/sco_parcours_dut.py
@@ -28,6 +28,7 @@
"""Semestres: gestion parcours DUT (Arreté du 13 août 2005)
"""
+from app.models.ues import UniteEns
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
from app import log
@@ -678,10 +679,10 @@ class SituationEtudParcoursECTS(SituationEtudParcoursGeneric):
Dans ce type de parcours, on n'utilise que ADM, AJ, et ADJ (?).
"""
- etud_moy_infos = self.nt.get_etud_moy_infos(self.etudid)
+ etud_ects_infos = self.nt.get_etud_ects_pot(self.etudid)
if (
- etud_moy_infos["ects_pot"] >= self.parcours.ECTS_BARRE_VALID_YEAR
- and etud_moy_infos["ects_pot"] >= self.parcours.ECTS_FONDAMENTAUX_PER_YEAR
+ etud_ects_infos["ects_pot"] >= self.parcours.ECTS_BARRE_VALID_YEAR
+ and etud_ects_infos["ects_pot"] >= self.parcours.ECTS_FONDAMENTAUX_PER_YEAR
):
choices = [
DecisionSem(
@@ -954,6 +955,9 @@ def do_formsemestre_validate_ue(
is_external=False,
):
"""Ajoute ou change validation UE"""
+ if semestre_id is None:
+ ue = UniteEns.query.get_or_404(ue_id)
+ semestre_id = ue.semestre_idx
args = {
"formsemestre_id": formsemestre_id,
"etudid": etudid,
@@ -971,7 +975,8 @@ def do_formsemestre_validate_ue(
if formsemestre_id:
cond += " and formsemestre_id=%(formsemestre_id)s"
if semestre_id:
- cond += " and semestre_id=%(semestre_id)s"
+ cond += " and (semestre_id=%(semestre_id)s or semestre_id is NULL)"
+ log(f"formsemestre_validate_ue: deleting where {cond}, args={args})")
cursor.execute("delete from scolar_formsemestre_validation where " + cond, args)
# insert
args["code"] = code
@@ -980,7 +985,7 @@ def do_formsemestre_validate_ue(
# stocke la moyenne d'UE capitalisée:
moy_ue = nt.get_etud_ue_status(etudid, ue_id)["moy"]
args["moy_ue"] = moy_ue
- log("formsemestre_validate_ue: %s" % args)
+ log("formsemestre_validate_ue: create %s" % args)
if code != None:
scolar_formsemestre_validation_create(cnx, args)
else:
@@ -1039,7 +1044,9 @@ def formsemestre_get_autorisation_inscription(etudid, origin_formsemestre_id):
)
-def formsemestre_get_etud_capitalisation(sem, etudid):
+def formsemestre_get_etud_capitalisation(
+ formation_id: int, semestre_idx: int, date_debut, etudid: int
+) -> list[dict]:
"""Liste des UE capitalisées (ADM) correspondant au semestre sem et à l'étudiant.
Recherche dans les semestres de la même formation (code) avec le même
@@ -1057,30 +1064,32 @@ def formsemestre_get_etud_capitalisation(sem, etudid):
cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
cursor.execute(
- """select distinct SFV.*, ue.ue_code from notes_ue ue, notes_formations nf,
- notes_formations nf2, scolar_formsemestre_validation SFV, notes_formsemestre sem
+ """
+ SELECT DISTINCT SFV.*, ue.ue_code
+ FROM notes_ue ue, notes_formations nf,
+ notes_formations nf2, scolar_formsemestre_validation SFV, notes_formsemestre sem
- WHERE ue.formation_id = nf.id
+ WHERE ue.formation_id = nf.id
and nf.formation_code = nf2.formation_code
and nf2.id=%(formation_id)s
and SFV.ue_id = ue.id
and SFV.code = 'ADM'
and SFV.etudid = %(etudid)s
-
- and ( (sem.id = SFV.formsemestre_id
- and sem.date_debut < %(date_debut)s
- and sem.semestre_id = %(semestre_id)s )
- or (
- ((SFV.formsemestre_id is NULL) OR (SFV.is_external)) -- les UE externes ou "anterieures"
- AND (SFV.semestre_id is NULL OR SFV.semestre_id=%(semestre_id)s)
+
+ and ( (sem.id = SFV.formsemestre_id
+ and sem.date_debut < %(date_debut)s
+ and sem.semestre_id = %(semestre_id)s )
+ or (
+ ((SFV.formsemestre_id is NULL) OR (SFV.is_external)) -- les UE externes ou "anterieures"
+ AND (SFV.semestre_id is NULL OR SFV.semestre_id=%(semestre_id)s)
) )
""",
{
"etudid": etudid,
- "formation_id": sem["formation_id"],
- "semestre_id": sem["semestre_id"],
- "date_debut": ndb.DateDMYtoISO(sem["date_debut"]),
+ "formation_id": formation_id,
+ "semestre_id": semestre_idx,
+ "date_debut": date_debut,
},
)
diff --git a/app/scodoc/sco_pvjury.py b/app/scodoc/sco_pvjury.py
index ea43cb2d..4479409d 100644
--- a/app/scodoc/sco_pvjury.py
+++ b/app/scodoc/sco_pvjury.py
@@ -53,6 +53,7 @@ from reportlab.lib import styles
import flask
from flask import url_for, g, request
+from app.models.ues import UniteEns
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
@@ -140,23 +141,21 @@ def descr_autorisations(autorisations):
return ", ".join(alist)
-def _comp_ects_by_ue_code_and_type(nt, decision_ues):
+def _comp_ects_by_ue_code(nt, decision_ues):
"""Calcul somme des ECTS validés dans ce semestre (sans les UE capitalisées)
decision_ues est le resultat de nt.get_etud_decision_ues
Chaque resultat est un dict: { ue_code : ects }
"""
if not decision_ues:
- return {}, {}
+ return {}
ects_by_ue_code = {}
- ects_by_ue_type = scu.DictDefault(defaultvalue=0) # { ue_type : ects validés }
for ue_id in decision_ues:
d = decision_ues[ue_id]
- ue = nt.uedict[ue_id]
- ects_by_ue_code[ue["ue_code"]] = d["ects"]
- ects_by_ue_type[ue["type"]] += d["ects"]
+ ue = UniteEns.query.get(ue_id)
+ ects_by_ue_code[ue.ue_code] = d["ects"]
- return ects_by_ue_code, ects_by_ue_type
+ return ects_by_ue_code
def _comp_ects_capitalises_by_ue_code(nt, etudid):
@@ -166,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
@@ -249,11 +245,8 @@ def dict_pvjury(
ects_capitalises_by_ue_code = _comp_ects_capitalises_by_ue_code(nt, etudid)
d["sum_ects_capitalises"] = sum(ects_capitalises_by_ue_code.values())
- ects_by_ue_code, ects_by_ue_type = _comp_ects_by_ue_code_and_type(
- nt, d["decisions_ue"]
- )
+ ects_by_ue_code = _comp_ects_by_ue_code(nt, d["decisions_ue"])
d["sum_ects"] = _sum_ects_dicts(ects_capitalises_by_ue_code, ects_by_ue_code)
- d["sum_ects_by_type"] = ects_by_ue_type
if d["decision_sem"] and sco_codes_parcours.code_semestre_validant(
d["decision_sem"]["code"]
diff --git a/app/scodoc/sco_recapcomplet.py b/app/scodoc/sco_recapcomplet.py
index 997e835e..16830b08 100644
--- a/app/scodoc/sco_recapcomplet.py
+++ b/app/scodoc/sco_recapcomplet.py
@@ -25,7 +25,7 @@
#
##############################################################################
-"""Tableau recapitulatif des notes d'un semestre
+"""Tableau récapitulatif des notes d'un semestre
"""
import datetime
import json
@@ -41,6 +41,7 @@ from app.comp import res_sem
from app.comp.res_common import NotesTableCompat
from app.models import FormSemestre
from app.models.etudiants import Identite
+from app.models.evaluations import Evaluation
import app.scodoc.sco_utils as scu
from app.scodoc import html_sco_header
@@ -308,8 +309,8 @@ def make_formsemestre_recapcomplet(
# nt = sco_cache.NotesTableCache.get(formsemestre_id) # sco91
# sco92 :
- nt: NotesTableCompat = res_sem.load_formsemestre_result(formsemestre)
- modimpls = nt.get_modimpls_dict()
+ nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
+ modimpls = formsemestre.modimpls_sorted
ues = nt.get_ues_stat_dict() # incluant le(s) UE de sport
#
# if formsemestre.formation.is_apc():
@@ -367,15 +368,16 @@ def make_formsemestre_recapcomplet(
pass
if not hidemodules and not ue["is_external"]:
for modimpl in modimpls:
- if modimpl["module"]["ue_id"] == ue["ue_id"]:
- code = modimpl["module"]["code"]
+ if modimpl.module.ue_id == ue["ue_id"]:
+ code = modimpl.module.code
h.append(code)
cod2mod[code] = modimpl # pour fabriquer le lien
if format == "xlsall":
- evals = nt.get_mod_evaluation_etat_list(
- modimpl["moduleimpl_id"]
- )
- mod_evals[modimpl["moduleimpl_id"]] = evals
+ evals = nt.modimpls_results[
+ modimpl.id
+ ].get_evaluations_completes(modimpl)
+ # evals = nt.get_mod_evaluation_etat_list(...
+ mod_evals[modimpl.id] = evals
h += _list_notes_evals_titles(code, evals)
h += admission_extra_cols
@@ -483,7 +485,7 @@ def make_formsemestre_recapcomplet(
if not hidemodules and not ue["is_external"]:
j = 0
for modimpl in modimpls:
- if modimpl["module"]["ue_id"] == ue["ue_id"]:
+ if modimpl.module.ue_id == ue["ue_id"]:
l.append(
fmtnum(
scu.fmt_note(
@@ -492,9 +494,7 @@ def make_formsemestre_recapcomplet(
)
) # moyenne etud dans module
if format == "xlsall":
- l += _list_notes_evals(
- mod_evals[modimpl["moduleimpl_id"]], etudid
- )
+ l += _list_notes_evals(mod_evals[modimpl.id], etudid)
j += 1
if not hidebac:
for k in admission_extra_cols:
@@ -509,9 +509,7 @@ def make_formsemestre_recapcomplet(
if not hidemodules: # moy/min/max dans chaque module
mods_stats = {} # moduleimpl_id : stats
for modimpl in modimpls:
- mods_stats[modimpl["moduleimpl_id"]] = nt.get_mod_stats(
- modimpl["moduleimpl_id"]
- )
+ mods_stats[modimpl.id] = nt.get_mod_stats(modimpl.id)
def add_bottom_stat(key, title, corner_value=""):
l = ["", title]
@@ -551,16 +549,16 @@ def make_formsemestre_recapcomplet(
# ue_index.append(len(l) - 1)
if not hidemodules and not ue["is_external"]:
for modimpl in modimpls:
- if modimpl["module"]["ue_id"] == ue["ue_id"]:
+ if modimpl.module.ue_id == ue["ue_id"]:
if key == "coef":
- coef = modimpl["module"]["coefficient"]
+ coef = modimpl.module.coefficient
if format[:3] != "xls":
coef = str(coef)
l.append(coef)
elif key == "ects":
l.append("") # ECTS module ?
else:
- val = mods_stats[modimpl["moduleimpl_id"]][key]
+ val = mods_stats[modimpl.id][key]
if key == "nb_valid_evals":
if (
format[:3] != "xls"
@@ -571,9 +569,7 @@ def make_formsemestre_recapcomplet(
l.append(val)
if format == "xlsall":
- l += _list_notes_evals_stats(
- mod_evals[modimpl["moduleimpl_id"]], key
- )
+ l += _list_notes_evals_stats(mod_evals[modimpl.id], key)
if modejury:
l.append("") # case vide sur ligne "Moyennes"
@@ -595,7 +591,7 @@ def make_formsemestre_recapcomplet(
add_bottom_stat("nb_valid_evals", "Nb évals")
add_bottom_stat("ects", "ECTS")
- # Generation table au format demandé
+ # Génération de la table au format demandé
if format == "html":
# Table format HTML
H = [
@@ -649,12 +645,12 @@ def make_formsemestre_recapcomplet(
): # Rang: force tri numerique pour sortable
cls = cls + " sortnumeric"
if F[0][i] in cod2mod: # lien vers etat module
- mod = cod2mod[F[0][i]]
+ modimpl = cod2mod[F[0][i]]
cells += '%s | ' % (
cls,
- mod["moduleimpl_id"],
- mod["module"]["titre"],
- sco_users.user_info(mod["responsable_id"])["nomcomplet"],
+ modimpl.id,
+ modimpl.module.titre,
+ sco_users.user_info(modimpl.responsable_id)["nomcomplet"],
F[0][i],
)
else:
@@ -838,63 +834,50 @@ def make_formsemestre_recapcomplet(
raise ValueError("unknown format %s" % format)
-def _list_notes_evals(evals, etudid):
+def _list_notes_evals(evals: list[Evaluation], etudid: int) -> list[str]:
"""Liste des notes des evaluations completes de ce module
(pour table xls avec evals)
"""
L = []
for e in evals:
- if (
- e["etat"]["evalcomplete"]
- or e["etat"]["evalattente"]
- or e["publish_incomplete"]
- ):
- notes_db = sco_evaluation_db.do_evaluation_get_all_notes(e["evaluation_id"])
- if etudid in notes_db:
- val = notes_db[etudid]["value"]
- else:
- # Note manquante mais prise en compte immédiate: affiche ATT
- val = scu.NOTES_ATTENTE
- val_fmt = scu.fmt_note(val, keep_numeric=True)
- L.append(val_fmt)
+ notes_db = sco_evaluation_db.do_evaluation_get_all_notes(e.evaluation_id)
+ if etudid in notes_db:
+ val = notes_db[etudid]["value"]
+ else:
+ # Note manquante mais prise en compte immédiate: affiche ATT
+ val = scu.NOTES_ATTENTE
+ val_fmt = scu.fmt_note(val, keep_numeric=True)
+ L.append(val_fmt)
return L
-def _list_notes_evals_titles(codemodule, evals):
+def _list_notes_evals_titles(codemodule: str, evals: list[Evaluation]) -> list[str]:
"""Liste des titres des evals completes"""
L = []
eval_index = len(evals) - 1
for e in evals:
- if (
- e["etat"]["evalcomplete"]
- or e["etat"]["evalattente"]
- or e["publish_incomplete"]
- ):
- L.append(codemodule + "-" + str(eval_index) + "-" + e["jour"].isoformat())
+ L.append(codemodule + "-" + str(eval_index) + "-" + e.jour.isoformat())
eval_index -= 1
return L
-def _list_notes_evals_stats(evals, key):
+def _list_notes_evals_stats(evals: list[Evaluation], key: str) -> list[str]:
"""Liste des stats (moy, ou rien!) des evals completes"""
L = []
for e in evals:
- if (
- e["etat"]["evalcomplete"]
- or e["etat"]["evalattente"]
- or e["publish_incomplete"]
- ):
- if key == "moy":
- val = e["etat"]["moy_num"]
- L.append(scu.fmt_note(val, keep_numeric=True))
- elif key == "max":
- L.append(e["note_max"])
- elif key == "min":
- L.append(0.0)
- elif key == "coef":
- L.append(e["coefficient"])
- else:
- L.append("") # on n'a pas sous la main min/max
+ if key == "moy":
+ # TODO #sco92
+ # val = e["etat"]["moy_num"]
+ # L.append(scu.fmt_note(val, keep_numeric=True))
+ L.append("")
+ elif key == "max":
+ L.append(e.note_max)
+ elif key == "min":
+ L.append(0.0)
+ elif key == "coef":
+ L.append(e.coefficient)
+ else:
+ L.append("") # on n'a pas sous la main min/max
return L
diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css
index c5758cb5..8ca51f7b 100644
--- a/app/static/css/scodoc.css
+++ b/app/static/css/scodoc.css
@@ -1771,40 +1771,54 @@ ul.notes_module_list {
}
div#ue_list_code {
- background-color: rgb(220,220,220);
- font-size: small;
- padding-left: 4px;
- padding-bottom: 1px;
+ background-color: rgb(155, 218, 155);
+ padding: 10px;
+ border: 1px solid blue;
+ border-radius: 10px;
+ padding: 10px;
margin-top: 10px;
- margin: 3ex;
+ margin-right: 15px;
}
ul.notes_module_list {
list-style-type: none;
}
+div#ue_list_modules {
+ background-color: rgb(191, 242, 255);
+ border: 1px solid blue;
+ border-radius: 10px;
+ padding: 10px;
+ margin-top: 10px;
+ margin-right: 15px;
+}
+
div#ue_list_etud_validations {
- background-color: rgb(220,250,220);
- padding-left: 4px;
- padding-bottom: 1px;
- margin: 3ex;
+ background-color: rgb(220,250,220);
+ padding-left: 4px;
+ padding-bottom: 1px;
+ margin: 3ex;
}
div#ue_list_etud_validations span {
font-weight: bold;
}
span.ue_share {
- font-weight: bold;
+ font-weight: bold;
}
div.ue_warning {
- border: 1px solid red;
- background-color: rgb(250,220,220);
- margin: 3ex;
- padding-left: 1ex;
- padding-right: 1ex;
+ border: 1px solid red;
+ border-radius: 10px;
+ background-color: rgb(250,220,220);
+ margin-top: 10px;
+ margin-right: 15px;
+ margin-bottom: 10px;
+ padding: 10px;
+}
+div.ue_warning:first-child {
+ font-weight: bold;
}
-
div.ue_warning span:before {
content: url(/ScoDoc/static/icons/warning_img.png);
vertical-align: -80%;
diff --git a/app/templates/scodoc/help/modules.html b/app/templates/scodoc/help/modules.html
index cd6e0767..924e128f 100644
--- a/app/templates/scodoc/help/modules.html
+++ b/app/templates/scodoc/help/modules.html
@@ -1,3 +1,4 @@
+{# -*- mode: jinja-html -*- #}
Les modules sont décrits dans le programme pédagogique. Un module est pour ce
@@ -24,24 +25,26 @@
la documentation.
{%endif%}
+
- {% if formsemestres %}
-
- Ce module est utilisé dans des semestres déjà mis en place, il faut prêter attention
- aux conséquences des changements effectués ici: par exemple les coefficients vont modifier
- les notes moyennes calculées. Les modules déjà utilisés ne peuvent pas être changés de semestre, ni détruits.
- Si vous souhaitez faire cela, allez d'abord modifier les semestres concernés pour déselectionner le module.
-
- Semestres utilisant ce module:
-
+{% if formsemestres %}
+ Module déjà utilisé dans des semestres, soyez prudents !
+
+ Ce module est utilisé dans des semestres déjà mis en place, il faut prêter attention
+ aux conséquences des changements effectués ici: par exemple les coefficients vont modifier
+ les notes moyennes calculées. Les modules déjà utilisés ne peuvent pas être changés de semestre, ni détruits.
+ Si vous souhaitez faire cela, allez d'abord modifier les semestres concernés pour déselectionner le module.
+
+
Semestres utilisant ce module:
+
- {%endif%}
+
+
+{%endif%}
-
\ No newline at end of file
diff --git a/app/views/absences.py b/app/views/absences.py
index a9ec8570..07f4bfdb 100644
--- a/app/views/absences.py
+++ b/app/views/absences.py
@@ -496,6 +496,7 @@ def SignaleAbsenceGrSemestre(
require_module = sco_preferences.get_preference(
"abs_require_module", formsemestre_id
)
+ sem = sco_formsemestre.do_formsemestre_list({"formsemestre_id": formsemestre_id})[0]
etuds = [
sco_etud.get_etud_info(etudid=m["etudid"], filled=True)[0]
for m in groups_infos.members
@@ -526,9 +527,7 @@ def SignaleAbsenceGrSemestre(
if etuds:
nt = sco_cache.NotesTableCache.get(formsemestre_id)
- sem = sco_formsemestre.do_formsemestre_list(
- {"formsemestre_id": formsemestre_id}
- )[0]
+
work_saturday = sco_abs.is_work_saturday()
jourdebut = sco_abs.ddmmyyyy(datedebut, work_saturday=work_saturday)
jourfin = sco_abs.ddmmyyyy(datefin, work_saturday=work_saturday)
diff --git a/app/views/notes.py b/app/views/notes.py
index 4b299ee5..0762a5ce 100644
--- a/app/views/notes.py
+++ b/app/views/notes.py
@@ -2140,7 +2140,7 @@ def formsemestre_validation_etud_manu(
)
-@bp.route("/formsemestre_validate_previous_ue")
+@bp.route("/formsemestre_validate_previous_ue", methods=["GET", "POST"])
@scodoc
@permission_required(Permission.ScoView)
@scodoc7func