diff --git a/app/comp/jury.py b/app/comp/jury.py
new file mode 100644
index 0000000000000000000000000000000000000000..bb7cd0d4e308d4112be92df6c38bd872a6e680f0
--- /dev/null
+++ b/app/comp/jury.py
@@ -0,0 +1,147 @@
+##############################################################################
+# 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' }}}
+ """
+
+ 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 f890b6a615f358790abcb6070cd30421e8c247da..f30f04912341f92cceb5d7596a79816b9bfeb1d0 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_cache.py b/app/comp/res_cache.py
new file mode 100644
index 0000000000000000000000000000000000000000..47c40b7e4281fde81ad4028e2a3277ddfc2f096d
--- /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_common.py b/app/comp/res_common.py
index 601f76a99bd51e11ed87c0b6baa0c6750a9622d8..3c408257da72d6873278c5551d1b4a6cc09c0603 100644
--- a/app/comp/res_common.py
+++ b/app/comp/res_common.py
@@ -9,10 +9,13 @@ from functools import cached_property
import numpy as np
import pandas as pd
from app.comp.aux_stats import StatsMoyenne
+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.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
@@ -25,7 +28,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 +39,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 +49,9 @@ 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)"""
def compute(self):
"Charge les notes et inscriptions et calcule toutes les moyennes"
@@ -101,7 +87,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 +110,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)
+ def get_etud_ue_validables(self, etudid: int) -> list[UniteEns]:
+ """Liste des UEs du semestre qui doivent être validées
+
+ 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".
"""
- return self.formsemestre.query_ues().filter(UniteEns.type != UE_SPORT).all()
+ 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):
- """Liste des modimpl de cet ue auxquels l'étudiant est inscrit"""
+ 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
@@ -180,6 +186,7 @@ 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.
@@ -243,6 +250,21 @@ class NotesTableCompat(ResultatsSemestre):
modimpls_dict.append(d)
return modimpls_dict
+ 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 +278,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,6 +311,31 @@ class NotesTableCompat(ResultatsSemestre):
"""
return self.etud_moy_gen[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 {
+ "ects_pot": ects_pot,
+ "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 {
@@ -333,8 +379,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 8e14d5afe2d990ee6256b3a00f800352232d8a87..5da2c7f061f0e99ecd056f972281d546e68909bd 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 f110849348d49f7a746271baacb3039d699155fd..d29b6bf3b12668747df443d869136081aa180128 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 2234fdfa11003bd1808226172efb4b25d8d8154c..e4b8fc8c8d81f8e46b3760500c55cc1140fcfdf3 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.
"""
diff --git a/app/models/notes.py b/app/models/notes.py
index fa8dc8d106b0bac8df18e9d87d3b84db50c688ff..7e5583579dd18428ac0978ecd5eaf433c0a8d273 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 0000000000000000000000000000000000000000..0bf487f3a85b1ac32fdefe243c3e800dc3227489
--- /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 07cbd13365d08b8256ec78a2e78947e394afba81..b9b4630ceb80830ba94191800c0853321f86cbb6 100644
--- a/app/scodoc/notes_table.py
+++ b/app/scodoc/notes_table.py
@@ -935,7 +935,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 +1011,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 +1140,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 +1317,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 23833f88caa9a496b4dac9adf9b04113d31c3a48..d1127110c4143b818aaa746197a41b0c0cb15e31 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 59ebab2d5b1de7a015d1cd9dfb74ae050de014b8..5fd0cec686d0d10d379d64ca16b08a9cbbee3008 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 823dd19fcdb4dd600c91f721568fed1042d058d0..59372b8d62f57290bd013c4f8cd22d595f4167f8 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 c1c75319bd81a91cb6fb7078a4adb860491635b1..c6b0151d6981bdd23746d67676c3b66b330c2aa9 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_evaluations.py b/app/scodoc/sco_evaluations.py
index 15b690a3ca5e343537a7c15445a966e809dce5b9..c54806e23efb512bacd6688b61383cf9dbe13572 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 1f440c8f21e52bae86f9afba4a2c06670d44761f..fcab0e7d9463962c2cdaa86741d5a28dbd93b347 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 ed5325ce8997d383cc6190b929090b5297c1b625..e835f1dd82c0c32bdeb13b13c3454f2a1887e74a 100644
--- a/app/scodoc/sco_formsemestre_validation.py
+++ b/app/scodoc/sco_formsemestre_validation.py
@@ -549,7 +549,7 @@ def formsemestre_recap_parcours_table(
ass = ""
formsemestre = FormSemestre.query.get(sem["formsemestre_id"])
- nt: NotesTableCompat = res_sem.load_formsemestre_result(formsemestre)
+ nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
# nt = sco_cache.NotesTableCache.get(sem["formsemestre_id"])
if is_cur:
type_sem = "*" # now unused
@@ -692,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(
'<tr class="%s rcp_l2 sem_%s">' % (class_sem, sem["formsemestre_id"])
)
@@ -703,7 +703,7 @@ def formsemestre_recap_parcours_table(
# total ECTS (affiché sous la moyenne générale)
H.append(
'<td class="sem_ects_tit"><a title="crédit potentiels (dont nb de fondamentaux)">ECTS:</a></td><td class="sem_ects">%g <span class="ects_fond">%g</span></td>'
- % (etud_moy_infos["ects_pot"], etud_moy_infos["ects_pot_fond"])
+ % (etud_ects_infos["ects_pot"], etud_ects_infos["ects_pot_fond"])
)
H.append('<td class="rcp_abs"></td>')
# ECTS validables dans chaque UE
@@ -1062,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 a342e3126ac43774a5203b42f290b24a41d05662..5d2871af69423e958f231a0ea0deb9532b4f8dcb 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 49c78263a691d570984c5b7963f1936308caad2e..0d682c0170a1cefb6067e3d5b23fe0e6d04966ab 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 ea43cb2d2a492ced862bc4142983f2848a226862..292c618a23a49c3fd2d66d247d7ee7b900d08824 100644
--- a/app/scodoc/sco_pvjury.py
+++ b/app/scodoc/sco_pvjury.py
@@ -140,23 +140,23 @@ 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 }
"""
+ raise NotImplementedError() # XXX #sco92
+ # ré-écrire en utilisant
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"]
- return ects_by_ue_code, ects_by_ue_type
+ return ects_by_ue_code
def _comp_ects_capitalises_by_ue_code(nt, etudid):
@@ -249,11 +249,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 997e835eac52078accb154b2cdc00adfa716255f..87ab8c26a01ad67a0de75dbba5f883d2b1528388 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 = [
@@ -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/views/notes.py b/app/views/notes.py
index 4b299ee522b8bcd751fe140a8468eca49f71841a..0762a5cee7e0bc51b98124b091f0cbc2e1ab8976 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