WIP: validations d'UE et de semestres

This commit is contained in:
Emmanuel Viennet 2022-02-06 16:09:17 +01:00
parent 5467ad2437
commit e6bd6cf28a
22 changed files with 562 additions and 256 deletions

147
app/comp/jury.py Normal file
View File

@ -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

View File

@ -77,6 +77,8 @@ class ModuleImplResults:
"{ evaluation.id : bool } indique si à prendre en compte ou non." "{ evaluation.id : bool } indique si à prendre en compte ou non."
self.evaluations_etat = {} self.evaluations_etat = {}
"{ evaluation_id: EvaluationEtat }" "{ evaluation_id: EvaluationEtat }"
self.en_attente = False
"Vrai si au moins une évaluation a une note en attente"
# #
self.evals_notes = None self.evals_notes = None
"""DataFrame, colonnes: EVALS, Lignes: etudid (inscrits au SEMESTRE) """DataFrame, colonnes: EVALS, Lignes: etudid (inscrits au SEMESTRE)
@ -133,7 +135,7 @@ class ModuleImplResults:
evals_notes = pd.DataFrame(index=self.etudids, dtype=float) evals_notes = pd.DataFrame(index=self.etudids, dtype=float)
self.evaluations_completes = [] self.evaluations_completes = []
self.evaluations_completes_dict = {} self.evaluations_completes_dict = {}
self.en_attente = False
for evaluation in moduleimpl.evaluations: for evaluation in moduleimpl.evaluations:
eval_df = self._load_evaluation_notes(evaluation) eval_df = self._load_evaluation_notes(evaluation)
# is_complete ssi tous les inscrits (non dem) au semestre ont une note # 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( self.evaluations_etat[evaluation.id] = EvaluationEtat(
evaluation_id=evaluation.id, nb_attente=nb_att, is_complete=is_complete 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) # Force columns names to integers (evaluation ids)
evals_notes.columns = pd.Int64Index( evals_notes.columns = pd.Int64Index(
@ -209,6 +213,13 @@ class ModuleImplResults:
* self.evaluations_completes * self.evaluations_completes
).reshape(-1, 1) ).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: def get_eval_notes_sur_20(self, moduleimpl: ModuleImpl) -> np.array:
"""Les notes des évaluations, """Les notes des évaluations,
remplace les ATT, EXC, ABS, NaN par zéro et mets les notes sur 20. remplace les ATT, EXC, ABS, NaN par zéro et mets les notes sur 20.

34
app/comp/res_cache.py Normal file
View File

@ -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},
)

View File

@ -9,10 +9,13 @@ from functools import cached_property
import numpy as np import numpy as np
import pandas as pd import pandas as pd
from app.comp.aux_stats import StatsMoyenne 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.comp.moy_mod import ModuleImplResults
from app.models import FormSemestre, Identite, ModuleImpl from app.models import FormSemestre, Identite, ModuleImpl
from app.models.ues import UniteEns from app.models.ues import UniteEns
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
from app.scodoc import sco_evaluations
from app.scodoc.sco_cache import ResultatsSemestreCache from app.scodoc.sco_cache import ResultatsSemestreCache
from app.scodoc.sco_codes_parcours import UE_SPORT, ATT, DEF from app.scodoc.sco_codes_parcours import UE_SPORT, ATT, DEF
@ -25,7 +28,7 @@ from app.scodoc.sco_codes_parcours import UE_SPORT, ATT, DEF
# (durée de vie de l'instance de ResultatsSemestre) # (durée de vie de l'instance de ResultatsSemestre)
# qui sont notamment les attributs décorés par `@cached_property`` # qui sont notamment les attributs décorés par `@cached_property``
# #
class ResultatsSemestre: class ResultatsSemestre(ResultatsCache):
_cached_attrs = ( _cached_attrs = (
"etud_moy_gen_ranks", "etud_moy_gen_ranks",
"etud_moy_gen", "etud_moy_gen",
@ -36,7 +39,7 @@ class ResultatsSemestre:
) )
def __init__(self, formsemestre: FormSemestre): def __init__(self, formsemestre: FormSemestre):
self.formsemestre: FormSemestre = formsemestre super().__init__(formsemestre, ResultatsSemestreCache)
# BUT ou standard ? (apc == "approche par compétences") # BUT ou standard ? (apc == "approche par compétences")
self.is_apc = formsemestre.formation.is_apc() self.is_apc = formsemestre.formation.is_apc()
# Attributs "virtuels", définis dans les sous-classes # Attributs "virtuels", définis dans les sous-classes
@ -46,26 +49,9 @@ class ResultatsSemestre:
self.etud_moy_gen = {} self.etud_moy_gen = {}
self.etud_moy_gen_ranks = {} self.etud_moy_gen_ranks = {}
self.modimpls_results: ModuleImplResults = None self.modimpls_results: ModuleImplResults = None
"Résultats de chaque modimpl: dict { modimpl.id : ModuleImplResults(Classique ou BUT) }"
self.etud_coef_ue_df = None self.etud_coef_ue_df = None
"""coefs d'UE effectifs pour chaque etudiant (pour form. classiques)""" """coefs d'UE effectifs pour chaque étudiant (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},
)
def compute(self): def compute(self):
"Charge les notes et inscriptions et calcule toutes les moyennes" "Charge les notes et inscriptions et calcule toutes les moyennes"
@ -101,7 +87,8 @@ class ResultatsSemestre:
@cached_property @cached_property
def ues(self) -> list[UniteEns]: def ues(self) -> list[UniteEns]:
"""Liste des UEs du semestre (avec les UE bonus sport) """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() return self.formsemestre.query_ues(with_sport=True).all()
@ -123,15 +110,34 @@ class ResultatsSemestre:
if m.module.module_type == scu.ModuleType.SAE if m.module.module_type == scu.ModuleType.SAE
] ]
@cached_property def get_etud_ue_validables(self, etudid: int) -> list[UniteEns]:
def ue_validables(self) -> list: """Liste des UEs du semestre qui doivent être validées
"""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 modimpls_in_ue(self, ue_id, etudid): Rappel: l'étudiant est inscrit à des modimpls et non à des UEs.
"""Liste des modimpl de cet ue auxquels l'étudiant est inscrit"""
- 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 # sert pour l'affichage ou non de l'UE sur le bulletin
return [ return [
modimpl modimpl
@ -180,6 +186,7 @@ class NotesTableCompat(ResultatsSemestre):
self.moy_moy = "NA" self.moy_moy = "NA"
self.expr_diagnostics = "" self.expr_diagnostics = ""
self.parcours = self.formsemestre.formation.get_parcours() self.parcours = self.formsemestre.formation.get_parcours()
self.validations = None
def get_etudids(self, sorted=False) -> list[int]: def get_etudids(self, sorted=False) -> list[int]:
"""Liste des etudids inscrits, incluant les démissionnaires. """Liste des etudids inscrits, incluant les démissionnaires.
@ -243,6 +250,21 @@ class NotesTableCompat(ResultatsSemestre):
modimpls_dict.append(d) modimpls_dict.append(d)
return modimpls_dict 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: 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. """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 } { 'code' : None|ATT|..., 'assidu' : 0|1, 'event_date' : , compense_formsemestre_id }
@ -256,12 +278,11 @@ class NotesTableCompat(ResultatsSemestre):
"compense_formsemestre_id": None, "compense_formsemestre_id": None,
} }
else: else:
return { if not self.validations:
"code": ATT, # XXX TODO self.validations = res_sem.load_formsemestre_validations(
"assidu": True, # XXX TODO self.formsemestre
"event_date": "", )
"compense_formsemestre_id": None, return self.validations.decisions_jury.get(etudid, None)
}
def get_etud_etat(self, etudid: int) -> str: def get_etud_etat(self, etudid: int) -> str:
"Etat de l'etudiant: 'I', 'D', DEF ou '' (si pas connu dans ce semestre)" "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] 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): def get_etud_ue_status(self, etudid: int, ue_id: int):
coef_ue = self.etud_coef_ue_df[ue_id][etudid] coef_ue = self.etud_coef_ue_df[ue_id][etudid]
return { return {
@ -333,8 +379,32 @@ class NotesTableCompat(ResultatsSemestre):
evals_results.append(d) evals_results.append(d)
return evals_results 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): 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: def get_mod_stats(self, moduleimpl_id: int) -> dict:
"""Stats sur les notes obtenues dans un modimpl """Stats sur les notes obtenues dans un modimpl

View File

@ -8,31 +8,49 @@
""" """
from flask import g from flask import g
from app.comp.jury import ValidationsSemestre
from app.comp.res_common import ResultatsSemestre from app.comp.res_common import ResultatsSemestre
from app.comp.res_classic import ResultatsSemestreClassic from app.comp.res_classic import ResultatsSemestreClassic
from app.comp.res_but import ResultatsSemestreBUT from app.comp.res_but import ResultatsSemestreBUT
from app.models.formsemestre import FormSemestre 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. """Returns ResultatsSemestre for this formsemestre.
Suivant le type de formation, retour une instance de Suivant le type de formation, retour une instance de
ResultatsSemestreClassic ou de ResultatsSemestreBUT. ResultatsSemestreClassic ou de ResultatsSemestreBUT.
Search in local cache (g.formsemestre_result_cache) Search in local cache (g.formsemestre_result_cache)
then global app cache (eg REDIS)
If not in cache, build it and cache it. If not in cache, build it and cache it.
""" """
# --- Try local cache (within the same request context) # --- Try local cache (within the same request context)
if not hasattr(g, "formsemestre_result_cache"): if not hasattr(g, "formsemestre_results_cache"):
g.formsemestre_result_cache = {} # pylint: disable=C0237 g.formsemestre_results_cache = {} # pylint: disable=C0237
else: else:
if formsemestre.id in g.formsemestre_result_cache: if formsemestre.id in g.formsemestre_results_cache:
return g.formsemestre_result_cache[formsemestre.id] return g.formsemestre_results_cache[formsemestre.id]
klass = ( klass = (
ResultatsSemestreBUT ResultatsSemestreBUT
if formsemestre.formation.is_apc() if formsemestre.formation.is_apc()
else ResultatsSemestreClassic 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]

View File

@ -49,13 +49,15 @@ from app.models.evaluations import (
) )
from app.models.groups import Partition, GroupDescr, group_membership from app.models.groups import Partition, GroupDescr, group_membership
from app.models.notes import ( from app.models.notes import (
ScolarEvent,
ScolarFormSemestreValidation,
ScolarAutorisationInscription,
BulAppreciations, BulAppreciations,
NotesNotes, NotesNotes,
NotesNotesLog, NotesNotesLog,
) )
from app.models.validations import (
ScolarEvent,
ScolarFormSemestreValidation,
ScolarAutorisationInscription,
)
from app.models.preferences import ScoPreference from app.models.preferences import ScoPreference
from app.models.but_refcomp import ( from app.models.but_refcomp import (

View File

@ -158,7 +158,7 @@ class FormSemestre(db.Model):
@cached_property @cached_property
def modimpls_sorted(self) -> list[ModuleImpl]: 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 type/numéro/code en APC
- triée par numéros d'UE/matières/modules pour les formations standard. - triée par numéros d'UE/matières/modules pour les formations standard.
""" """

View File

@ -8,100 +8,6 @@ from app.models import SHORT_STR_LEN
from app.models import CODE_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): class BulAppreciations(db.Model):
"""Appréciations sur bulletins""" """Appréciations sur bulletins"""

109
app/models/validations.py Normal file
View File

@ -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"),
)

View File

@ -935,7 +935,7 @@ class NotesTable:
""" """
return self.moy_gen[etudid] return self.moy_gen[etudid]
def get_etud_moy_infos(self, etudid): def get_etud_moy_infos(self, etudid): # XXX OBSOLETE
"""Infos sur moyennes""" """Infos sur moyennes"""
return self.etud_moy_infos[etudid] return self.etud_moy_infos[etudid]
@ -1011,7 +1011,10 @@ class NotesTable:
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
cursor.execute( 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}, {"formsemestre_id": self.formsemestre_id},
) )
decisions_jury = {} decisions_jury = {}
@ -1137,8 +1140,14 @@ class NotesTable:
""" """
self.ue_capitalisees = scu.DictDefault(defaultvalue=[]) self.ue_capitalisees = scu.DictDefault(defaultvalue=[])
cnx = None cnx = None
semestre_id = self.sem["semestre_id"]
for etudid in self.get_etudids(): 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: for ue_cap in capital:
# Si la moyenne d'UE n'avait pas été stockée (anciennes versions de ScoDoc) # Si la moyenne d'UE n'avait pas été stockée (anciennes versions de ScoDoc)
# il faut la calculer ici et l'enregistrer # il faut la calculer ici et l'enregistrer
@ -1308,7 +1317,7 @@ class NotesTable:
"""Liste des evaluations de ce semestre, avec leur etat""" """Liste des evaluations de ce semestre, avec leur etat"""
return self.get_evaluations_etats() 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""" """Liste des évaluations de ce module"""
return [ return [
e e

View File

@ -142,7 +142,7 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
prefs = sco_preferences.SemPreferences(formsemestre_id) prefs = sco_preferences.SemPreferences(formsemestre_id)
# nt = sco_cache.NotesTableCache.get(formsemestre_id) # > toutes notes # nt = sco_cache.NotesTableCache.get(formsemestre_id) # > toutes notes
formsemestre = FormSemestre.query.get_or_404(formsemestre_id) 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): if not nt.get_etud_etat(etudid):
raise ScoValueError("Etudiant non inscrit à ce semestre") raise ScoValueError("Etudiant non inscrit à ce semestre")
I = scu.DictDefault(defaultvalue="") I = scu.DictDefault(defaultvalue="")

View File

@ -59,9 +59,9 @@ import traceback
from flask import g from flask import g
from app import log
from app.scodoc import notesdb as ndb from app.scodoc import notesdb as ndb
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
from app import log
CACHE = None # set in app.__init__.py 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) SemBulletinsPDFCache.invalidate_sems(formsemestre_ids)
ResultatsSemestreCache.delete_many(formsemestre_ids) ResultatsSemestreCache.delete_many(formsemestre_ids)
ValidationsSemestreCache.delete_many(formsemestre_ids)
class DefferedSemCacheManager: class DefferedSemCacheManager:
@ -319,10 +320,20 @@ class DefferedSemCacheManager:
# ---- Nouvelles classes ScoDoc 9.2 # ---- Nouvelles classes ScoDoc 9.2
class ResultatsSemestreCache(ScoDocCache): class ResultatsSemestreCache(ScoDocCache):
"""Cache pour les résultats ResultatsSemestre. """Cache pour les résultats ResultatsSemestre (notes et moyennes)
Clé: formsemestre_id Clé: formsemestre_id
Valeur: { un paquet de dataframes } Valeur: { un paquet de dataframes }
""" """
prefix = "RSEM" prefix = "RSEM"
timeout = 60 * 60 # ttl 1 heure (en phase de mise au point) 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)

View File

@ -170,18 +170,18 @@ CODES_SEM_REO = {NAR: 1} # reorientation
CODES_UE_VALIDES = {ADM: True, CMP: True} # UE validée 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" "Vrai si ce CODE entraine la validation du semestre"
return CODES_SEM_VALIDES.get(code, False) 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)" "Vrai si ce CODE est un code d'attente (semestre validable plus tard par jury ou compensation)"
return CODES_SEM_ATTENTES.get(code, False) return CODES_SEM_ATTENTES.get(code, False)
def code_ue_validant(code): def code_ue_validant(code: str) -> bool:
"Vrai si ce code entraine la validation de l'UE" "Vrai si ce code entraine la validation des UEs du semestre."
return CODES_UE_VALIDES.get(code, False) return CODES_UE_VALIDES.get(code, False)
@ -259,6 +259,7 @@ class TypeParcours(object):
) # par defaut, autorise tous les types d'UE ) # par defaut, autorise tous les types d'UE
APC_SAE = False # Approche par compétences avec ressources et SAÉs APC_SAE = False # Approche par compétences avec ressources et SAÉs
USE_REFERENTIEL_COMPETENCES = False # Lien avec ref. comp. USE_REFERENTIEL_COMPETENCES = False # Lien avec ref. comp.
ECTS_FONDAMENTAUX_PER_YEAR = 0.0 # pour ISCID, deprecated
def check(self, formation=None): def check(self, formation=None):
return True, "" # status, diagnostic_message return True, "" # status, diagnostic_message

View File

@ -32,7 +32,7 @@ from flask_login import current_user
from app import db from app import db
from app.models import Formation, UniteEns, Matiere, Module, FormSemestre, ModuleImpl 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 from app.scodoc.sco_codes_parcours import UE_SPORT
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.scodoc import sco_groups from app.scodoc import sco_groups

View File

@ -393,9 +393,8 @@ def do_evaluation_etat_in_mod(nt, moduleimpl_id):
"""""" """"""
evals = nt.get_mod_evaluation_etat_list(moduleimpl_id) evals = nt.get_mod_evaluation_etat_list(moduleimpl_id)
etat = _eval_etat(evals) etat = _eval_etat(evals)
etat["attente"] = moduleimpl_id in [ # Il y a-t-il des notes en attente dans ce module ?
m["moduleimpl_id"] for m in nt.get_moduleimpls_attente() etat["attente"] = nt.modimpls_results[moduleimpl_id].en_attente
] # > liste moduleimpl en attente
return etat return etat

View File

@ -991,9 +991,9 @@ def formsemestre_status(formsemestre_id=None):
modimpls = sco_moduleimpl.moduleimpl_withmodule_list( modimpls = sco_moduleimpl.moduleimpl_withmodule_list(
formsemestre_id=formsemestre_id formsemestre_id=formsemestre_id
) )
nt = sco_cache.NotesTableCache.get(formsemestre_id) # nt = sco_cache.NotesTableCache.get(formsemestre_id)
# WIP formsemestre = FormSemestre.query.get_or_404(formsemestre_id) formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
# WIP nt = res_sem.load_formsemestre_result(formsemestre) nt = res_sem.load_formsemestre_results(formsemestre)
# Construit la liste de tous les enseignants de ce semestre: # Construit la liste de tous les enseignants de ce semestre:
mails_enseignants = set( mails_enseignants = set(

View File

@ -549,7 +549,7 @@ def formsemestre_recap_parcours_table(
ass = "" ass = ""
formsemestre = FormSemestre.query.get(sem["formsemestre_id"]) 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"]) # nt = sco_cache.NotesTableCache.get(sem["formsemestre_id"])
if is_cur: if is_cur:
type_sem = "*" # now unused type_sem = "*" # now unused
@ -692,7 +692,7 @@ def formsemestre_recap_parcours_table(
sco_preferences.get_preference("bul_show_ects", sem["formsemestre_id"]) sco_preferences.get_preference("bul_show_ects", sem["formsemestre_id"])
or nt.parcours.ECTS_ONLY 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( H.append(
'<tr class="%s rcp_l2 sem_%s">' % (class_sem, sem["formsemestre_id"]) '<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) # total ECTS (affiché sous la moyenne générale)
H.append( 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>' '<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>') H.append('<td class="rcp_abs"></td>')
# ECTS validables dans chaque UE # ECTS validables dans chaque UE
@ -1062,7 +1062,7 @@ def formsemestre_validate_previous_ue(formsemestre_id, etudid):
"title": "Indice du semestre", "title": "Indice du semestre",
"explanation": "Facultatif: indice du semestre dans la formation", "explanation": "Facultatif: indice du semestre dans la formation",
"allow_null": True, "allow_null": True,
"allowed_values": [""] + [str(x) for x in range(11)], "allowed_values": [""] + [x for x in range(11)],
"labels": ["-"] + list(range(11)), "labels": ["-"] + list(range(11)),
}, },
), ),

View File

@ -837,7 +837,7 @@ def _add_apc_columns(
# => On recharge tout dans les nouveaux modèles # => On recharge tout dans les nouveaux modèles
# rows est une liste de dict avec une clé "etudid" # rows est une liste de dict avec une clé "etudid"
# on va y ajouter une clé par UE du semestre # 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] modimpl_results: ModuleImplResults = nt.modimpls_results[modimpl.id]
# XXX A ENLEVER TODO # XXX A ENLEVER TODO

View File

@ -28,6 +28,7 @@
"""Semestres: gestion parcours DUT (Arreté du 13 août 2005) """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.sco_utils as scu
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
from app import log from app import log
@ -678,10 +679,10 @@ class SituationEtudParcoursECTS(SituationEtudParcoursGeneric):
Dans ce type de parcours, on n'utilise que ADM, AJ, et ADJ (?). 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 ( if (
etud_moy_infos["ects_pot"] >= self.parcours.ECTS_BARRE_VALID_YEAR etud_ects_infos["ects_pot"] >= self.parcours.ECTS_BARRE_VALID_YEAR
and etud_moy_infos["ects_pot"] >= self.parcours.ECTS_FONDAMENTAUX_PER_YEAR and etud_ects_infos["ects_pot"] >= self.parcours.ECTS_FONDAMENTAUX_PER_YEAR
): ):
choices = [ choices = [
DecisionSem( DecisionSem(
@ -954,6 +955,9 @@ def do_formsemestre_validate_ue(
is_external=False, is_external=False,
): ):
"""Ajoute ou change validation UE""" """Ajoute ou change validation UE"""
if semestre_id is None:
ue = UniteEns.query.get_or_404(ue_id)
semestre_id = ue.semestre_idx
args = { args = {
"formsemestre_id": formsemestre_id, "formsemestre_id": formsemestre_id,
"etudid": etudid, "etudid": etudid,
@ -971,7 +975,8 @@ def do_formsemestre_validate_ue(
if formsemestre_id: if formsemestre_id:
cond += " and formsemestre_id=%(formsemestre_id)s" cond += " and formsemestre_id=%(formsemestre_id)s"
if semestre_id: 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) cursor.execute("delete from scolar_formsemestre_validation where " + cond, args)
# insert # insert
args["code"] = code args["code"] = code
@ -980,7 +985,7 @@ def do_formsemestre_validate_ue(
# stocke la moyenne d'UE capitalisée: # stocke la moyenne d'UE capitalisée:
moy_ue = nt.get_etud_ue_status(etudid, ue_id)["moy"] moy_ue = nt.get_etud_ue_status(etudid, ue_id)["moy"]
args["moy_ue"] = moy_ue args["moy_ue"] = moy_ue
log("formsemestre_validate_ue: %s" % args) log("formsemestre_validate_ue: create %s" % args)
if code != None: if code != None:
scolar_formsemestre_validation_create(cnx, args) scolar_formsemestre_validation_create(cnx, args)
else: 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. """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 Recherche dans les semestres de la même formation (code) avec le même
@ -1057,8 +1064,10 @@ def formsemestre_get_etud_capitalisation(sem, etudid):
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
cursor.execute( 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 nf.formation_code = nf2.formation_code
@ -1068,19 +1077,19 @@ def formsemestre_get_etud_capitalisation(sem, etudid):
and SFV.code = 'ADM' and SFV.code = 'ADM'
and SFV.etudid = %(etudid)s and SFV.etudid = %(etudid)s
and ( (sem.id = SFV.formsemestre_id and ( (sem.id = SFV.formsemestre_id
and sem.date_debut < %(date_debut)s and sem.date_debut < %(date_debut)s
and sem.semestre_id = %(semestre_id)s ) and sem.semestre_id = %(semestre_id)s )
or ( or (
((SFV.formsemestre_id is NULL) OR (SFV.is_external)) -- les UE externes ou "anterieures" ((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 (SFV.semestre_id is NULL OR SFV.semestre_id=%(semestre_id)s)
) ) ) )
""", """,
{ {
"etudid": etudid, "etudid": etudid,
"formation_id": sem["formation_id"], "formation_id": formation_id,
"semestre_id": sem["semestre_id"], "semestre_id": semestre_idx,
"date_debut": ndb.DateDMYtoISO(sem["date_debut"]), "date_debut": date_debut,
}, },
) )

View File

@ -140,23 +140,23 @@ def descr_autorisations(autorisations):
return ", ".join(alist) 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) """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 decision_ues est le resultat de nt.get_etud_decision_ues
Chaque resultat est un dict: { ue_code : ects } Chaque resultat est un dict: { ue_code : ects }
""" """
raise NotImplementedError() # XXX #sco92
# ré-écrire en utilisant
if not decision_ues: if not decision_ues:
return {}, {} return {}
ects_by_ue_code = {} ects_by_ue_code = {}
ects_by_ue_type = scu.DictDefault(defaultvalue=0) # { ue_type : ects validés }
for ue_id in decision_ues: for ue_id in decision_ues:
d = decision_ues[ue_id] d = decision_ues[ue_id]
ue = nt.uedict[ue_id] ue = nt.uedict[ue_id]
ects_by_ue_code[ue["ue_code"]] = d["ects"] 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): 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) ects_capitalises_by_ue_code = _comp_ects_capitalises_by_ue_code(nt, etudid)
d["sum_ects_capitalises"] = sum(ects_capitalises_by_ue_code.values()) 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( ects_by_ue_code = _comp_ects_by_ue_code(nt, d["decisions_ue"])
nt, d["decisions_ue"]
)
d["sum_ects"] = _sum_ects_dicts(ects_capitalises_by_ue_code, ects_by_ue_code) 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( if d["decision_sem"] and sco_codes_parcours.code_semestre_validant(
d["decision_sem"]["code"] d["decision_sem"]["code"]

View File

@ -25,7 +25,7 @@
# #
############################################################################## ##############################################################################
"""Tableau recapitulatif des notes d'un semestre """Tableau récapitulatif des notes d'un semestre
""" """
import datetime import datetime
import json import json
@ -41,6 +41,7 @@ from app.comp import res_sem
from app.comp.res_common import NotesTableCompat from app.comp.res_common import NotesTableCompat
from app.models import FormSemestre from app.models import FormSemestre
from app.models.etudiants import Identite from app.models.etudiants import Identite
from app.models.evaluations import Evaluation
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
@ -308,8 +309,8 @@ def make_formsemestre_recapcomplet(
# nt = sco_cache.NotesTableCache.get(formsemestre_id) # sco91 # nt = sco_cache.NotesTableCache.get(formsemestre_id) # sco91
# sco92 : # sco92 :
nt: NotesTableCompat = res_sem.load_formsemestre_result(formsemestre) nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
modimpls = nt.get_modimpls_dict() modimpls = formsemestre.modimpls_sorted
ues = nt.get_ues_stat_dict() # incluant le(s) UE de sport ues = nt.get_ues_stat_dict() # incluant le(s) UE de sport
# #
# if formsemestre.formation.is_apc(): # if formsemestre.formation.is_apc():
@ -367,15 +368,16 @@ def make_formsemestre_recapcomplet(
pass pass
if not hidemodules and not ue["is_external"]: if not hidemodules and not ue["is_external"]:
for modimpl in modimpls: for modimpl in modimpls:
if modimpl["module"]["ue_id"] == ue["ue_id"]: if modimpl.module.ue_id == ue["ue_id"]:
code = modimpl["module"]["code"] code = modimpl.module.code
h.append(code) h.append(code)
cod2mod[code] = modimpl # pour fabriquer le lien cod2mod[code] = modimpl # pour fabriquer le lien
if format == "xlsall": if format == "xlsall":
evals = nt.get_mod_evaluation_etat_list( evals = nt.modimpls_results[
modimpl["moduleimpl_id"] modimpl.id
) ].get_evaluations_completes(modimpl)
mod_evals[modimpl["moduleimpl_id"]] = evals # evals = nt.get_mod_evaluation_etat_list(...
mod_evals[modimpl.id] = evals
h += _list_notes_evals_titles(code, evals) h += _list_notes_evals_titles(code, evals)
h += admission_extra_cols h += admission_extra_cols
@ -483,7 +485,7 @@ def make_formsemestre_recapcomplet(
if not hidemodules and not ue["is_external"]: if not hidemodules and not ue["is_external"]:
j = 0 j = 0
for modimpl in modimpls: for modimpl in modimpls:
if modimpl["module"]["ue_id"] == ue["ue_id"]: if modimpl.module.ue_id == ue["ue_id"]:
l.append( l.append(
fmtnum( fmtnum(
scu.fmt_note( scu.fmt_note(
@ -492,9 +494,7 @@ def make_formsemestre_recapcomplet(
) )
) # moyenne etud dans module ) # moyenne etud dans module
if format == "xlsall": if format == "xlsall":
l += _list_notes_evals( l += _list_notes_evals(mod_evals[modimpl.id], etudid)
mod_evals[modimpl["moduleimpl_id"]], etudid
)
j += 1 j += 1
if not hidebac: if not hidebac:
for k in admission_extra_cols: for k in admission_extra_cols:
@ -509,9 +509,7 @@ def make_formsemestre_recapcomplet(
if not hidemodules: # moy/min/max dans chaque module if not hidemodules: # moy/min/max dans chaque module
mods_stats = {} # moduleimpl_id : stats mods_stats = {} # moduleimpl_id : stats
for modimpl in modimpls: for modimpl in modimpls:
mods_stats[modimpl["moduleimpl_id"]] = nt.get_mod_stats( mods_stats[modimpl.id] = nt.get_mod_stats(modimpl.id)
modimpl["moduleimpl_id"]
)
def add_bottom_stat(key, title, corner_value=""): def add_bottom_stat(key, title, corner_value=""):
l = ["", title] l = ["", title]
@ -551,16 +549,16 @@ def make_formsemestre_recapcomplet(
# ue_index.append(len(l) - 1) # ue_index.append(len(l) - 1)
if not hidemodules and not ue["is_external"]: if not hidemodules and not ue["is_external"]:
for modimpl in modimpls: for modimpl in modimpls:
if modimpl["module"]["ue_id"] == ue["ue_id"]: if modimpl.module.ue_id == ue["ue_id"]:
if key == "coef": if key == "coef":
coef = modimpl["module"]["coefficient"] coef = modimpl.module.coefficient
if format[:3] != "xls": if format[:3] != "xls":
coef = str(coef) coef = str(coef)
l.append(coef) l.append(coef)
elif key == "ects": elif key == "ects":
l.append("") # ECTS module ? l.append("") # ECTS module ?
else: else:
val = mods_stats[modimpl["moduleimpl_id"]][key] val = mods_stats[modimpl.id][key]
if key == "nb_valid_evals": if key == "nb_valid_evals":
if ( if (
format[:3] != "xls" format[:3] != "xls"
@ -571,9 +569,7 @@ def make_formsemestre_recapcomplet(
l.append(val) l.append(val)
if format == "xlsall": if format == "xlsall":
l += _list_notes_evals_stats( l += _list_notes_evals_stats(mod_evals[modimpl.id], key)
mod_evals[modimpl["moduleimpl_id"]], key
)
if modejury: if modejury:
l.append("") # case vide sur ligne "Moyennes" 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("nb_valid_evals", "Nb évals")
add_bottom_stat("ects", "ECTS") add_bottom_stat("ects", "ECTS")
# Generation table au format demandé # Génération de la table au format demandé
if format == "html": if format == "html":
# Table format HTML # Table format HTML
H = [ H = [
@ -838,63 +834,50 @@ def make_formsemestre_recapcomplet(
raise ValueError("unknown format %s" % format) 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 """Liste des notes des evaluations completes de ce module
(pour table xls avec evals) (pour table xls avec evals)
""" """
L = [] L = []
for e in evals: for e in evals:
if ( notes_db = sco_evaluation_db.do_evaluation_get_all_notes(e.evaluation_id)
e["etat"]["evalcomplete"] if etudid in notes_db:
or e["etat"]["evalattente"] val = notes_db[etudid]["value"]
or e["publish_incomplete"] else:
): # Note manquante mais prise en compte immédiate: affiche ATT
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(e["evaluation_id"]) val = scu.NOTES_ATTENTE
if etudid in notes_db: val_fmt = scu.fmt_note(val, keep_numeric=True)
val = notes_db[etudid]["value"] L.append(val_fmt)
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 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""" """Liste des titres des evals completes"""
L = [] L = []
eval_index = len(evals) - 1 eval_index = len(evals) - 1
for e in evals: for e in evals:
if ( L.append(codemodule + "-" + str(eval_index) + "-" + e.jour.isoformat())
e["etat"]["evalcomplete"]
or e["etat"]["evalattente"]
or e["publish_incomplete"]
):
L.append(codemodule + "-" + str(eval_index) + "-" + e["jour"].isoformat())
eval_index -= 1 eval_index -= 1
return L 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""" """Liste des stats (moy, ou rien!) des evals completes"""
L = [] L = []
for e in evals: for e in evals:
if ( if key == "moy":
e["etat"]["evalcomplete"] # TODO #sco92
or e["etat"]["evalattente"] # val = e["etat"]["moy_num"]
or e["publish_incomplete"] # L.append(scu.fmt_note(val, keep_numeric=True))
): L.append("")
if key == "moy": elif key == "max":
val = e["etat"]["moy_num"] L.append(e.note_max)
L.append(scu.fmt_note(val, keep_numeric=True)) elif key == "min":
elif key == "max": L.append(0.0)
L.append(e["note_max"]) elif key == "coef":
elif key == "min": L.append(e.coefficient)
L.append(0.0) else:
elif key == "coef": L.append("") # on n'a pas sous la main min/max
L.append(e["coefficient"])
else:
L.append("") # on n'a pas sous la main min/max
return L return L

View File

@ -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 @scodoc
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@scodoc7func @scodoc7func