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."
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.

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 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)
"""
return self.formsemestre.query_ues().filter(UniteEns.type != UE_SPORT).all()
def get_etud_ue_validables(self, etudid: int) -> list[UniteEns]:
"""Liste des UEs du semestre qui doivent être validées
def modimpls_in_ue(self, ue_id, etudid):
"""Liste des modimpl de cet ue auxquels l'étudiant est inscrit"""
Rappel: l'étudiant est inscrit à des modimpls et non à des UEs.
- En BUT: on considère que l'étudiant va (ou non) valider toutes les UEs des modules
du parcours. XXX notion à implémenter, pour l'instant toutes les UE du semestre.
- En classique: toutes les UEs des modimpls auxquels l'étufdiant est inscrit sont
susceptibles d'être validées.
Les UE "bonus" (sport) ne sont jamais "validables".
"""
if self.is_apc:
# TODO: introduire la notion de parcours (#sco93)
return self.formsemestre.query_ues().filter(UniteEns.type != UE_SPORT).all()
else:
# restreint aux UE auxquelles l'étudiant est inscrit (dans l'un des modimpls)
ues = {
modimpl.module.ue
for modimpl in self.formsemestre.modimpls_sorted
if self.modimpl_inscr_df[modimpl.id][etudid]
}
ues = sorted(list(ues), key=lambda x: x.numero or 0)
return ues
def modimpls_in_ue(self, ue_id, etudid) -> list[ModuleImpl]:
"""Liste des modimpl de cette UE auxquels l'étudiant est inscrit"""
# sert pour l'affichage ou non de l'UE sur le bulletin
return [
modimpl
@ -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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,7 +1064,9 @@ 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,
"""
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
@ -1078,9 +1087,9 @@ def formsemestre_get_etud_capitalisation(sem, etudid):
""",
{
"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,
},
)

View File

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

View File

@ -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,18 +834,13 @@ 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"])
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(e.evaluation_id)
if etudid in notes_db:
val = notes_db[etudid]["value"]
else:
@ -860,39 +851,31 @@ def _list_notes_evals(evals, etudid):
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))
# 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"])
L.append(e.note_max)
elif key == "min":
L.append(0.0)
elif key == "coef":
L.append(e["coefficient"])
L.append(e.coefficient)
else:
L.append("") # on n'a pas sous la main min/max
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
@permission_required(Permission.ScoView)
@scodoc7func