Merge branch 'refactor_nt' of https://scodoc.org/git/ScoDoc/ScoDoc into entreprises

This commit is contained in:
Arthur ZHU 2022-02-07 20:39:13 +01:00
commit e67cbf87eb
31 changed files with 983 additions and 382 deletions

View File

@ -77,11 +77,20 @@ class BulletinBUT(ResultatsSemestreBUT):
"saes": self.etud_ue_mod_results(etud, ue, self.saes),
}
if ue.type != UE_SPORT:
if sco_preferences.get_preference(
"bul_show_ue_rangs", self.formsemestre.id
):
rangs, effectif = self.ue_rangs[ue.id]
rang = rangs[etud.id]
else:
rang, effectif = "", 0
d["moyenne"] = {
"value": fmt_note(self.etud_moy_ue[ue.id][etud.id]),
"min": fmt_note(self.etud_moy_ue[ue.id].min()),
"max": fmt_note(self.etud_moy_ue[ue.id].max()),
"moy": fmt_note(self.etud_moy_ue[ue.id].mean()),
"rang": rang,
"total": effectif, # nb etud avec note dans cette UE
}
else:
# ceci suppose que l'on a une seule UE bonus,
@ -227,8 +236,8 @@ class BulletinBUT(ResultatsSemestreBUT):
"date_debut": formsemestre.date_debut.isoformat(),
"date_fin": formsemestre.date_fin.isoformat(),
"annee_universitaire": self.formsemestre.annee_scolaire_str(),
"inscription": "TODO-MM-JJ", # XXX TODO
"numero": formsemestre.semestre_id,
"inscription": "", # inutilisé mais nécessaire pour le js de Seb.
"groupes": [], # XXX TODO
"absences": {
"injustifie": nbabsjust,

View File

@ -21,7 +21,7 @@ class StatsMoyenne:
Les valeurs NAN ou non numériques sont toujours enlevées.
Si vals is None, renvoie des zéros (utilisé pour UE bonus)
"""
if vals is None:
if vals is None or len(vals) == 0:
self.moy = self.min = self.max = self.size = self.nb_vals = 0
else:
self.moy = np.nanmean(vals)

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

@ -0,0 +1,154 @@
##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
"""Stockage des décisions de jury
"""
import pandas as pd
from app import db
from app.models import FormSemestre, ScolarFormSemestreValidation
from app.comp.res_cache import ResultatsCache
from app.scodoc import sco_cache
from app.scodoc import sco_codes_parcours
class ValidationsSemestre(ResultatsCache):
""" """
_cached_attrs = (
"decisions_jury",
"decisions_jury_ues",
"ue_capitalisees",
)
def __init__(self, formsemestre: FormSemestre):
super().__init__(formsemestre, sco_cache.ValidationsSemestreCache)
self.decisions_jury = {}
"""Décisions prises dans ce semestre:
{ etudid : { 'code' : None|ATT|..., 'assidu' : 0|1 }}"""
self.decisions_jury_ues = {}
"""Décisions sur des UEs dans ce semestre:
{ etudid : { ue_id : { 'code' : Note|ADM|CMP, 'event_date' }}}
"""
self.ue_capitalisees: pd.DataFrame = None
"""DataFrame, index etudid
formsemestre_id : origine de l'UE capitalisée
is_external : vrai si validation effectuée dans un semestre extérieur
ue_id : dans le semestre origine (pas toujours de la même formation)
ue_code : code de l'UE, moy_ue : note enregistrée,
event_date : date de la validation (jury)."""
if not self.load_cached():
self.compute()
self.store()
def compute(self):
"Charge les résultats de jury et UEs capitalisées"
self.ue_capitalisees = formsemestre_get_ue_capitalisees(self.formsemestre)
self.comp_decisions_jury()
def comp_decisions_jury(self):
"""Cherche les decisions du jury pour le semestre (pas les UE).
Calcule les attributs:
decisions_jury = { etudid : { 'code' : None|ATT|..., 'assidu' : 0|1 }}
decision_jury_ues={ etudid : { ue_id : { 'code' : Note|ADM|CMP, 'event_date' }}}
Si la décision n'a pas été prise, la clé etudid n'est pas présente.
Si l'étudiant est défaillant, pas de décisions d'UE.
"""
# repris de NotesTable.comp_decisions_jury pour la compatibilité
decisions_jury_q = ScolarFormSemestreValidation.query.filter_by(
formsemestre_id=self.formsemestre.id
)
decisions_jury = {}
for decision in decisions_jury_q.filter(
ScolarFormSemestreValidation.ue_id == None # slt dec. sem.
):
decisions_jury[decision.etudid] = {
"code": decision.code,
"assidu": decision.assidu,
"compense_formsemestre_id": decision.compense_formsemestre_id,
"event_date": decision.event_date.strftime("%d/%m/%Y"),
}
self.decisions_jury = decisions_jury
# UEs:
decisions_jury_ues = {}
for decision in decisions_jury_q.filter(
ScolarFormSemestreValidation.ue_id != None # slt dec. sem.
):
if decision.etudid not in decisions_jury_ues:
decisions_jury_ues[decision.etudid] = {}
# Calcul des ECTS associés à cette UE:
if sco_codes_parcours.code_ue_validant(decision.code):
ects = decision.ue.ects or 0.0 # 0 if None
else:
ects = 0.0
decisions_jury_ues[decision.etudid][decision.ue.id] = {
"code": decision.code,
"ects": ects, # 0. si UE non validée
"event_date": decision.event_date.strftime("%d/%m/%Y"),
}
self.decisions_jury_ues = decisions_jury_ues
def formsemestre_get_ue_capitalisees(formsemestre: FormSemestre) -> pd.DataFrame:
"""Liste des UE capitalisées (ADM) utilisables dans ce formsemestre
Recherche dans les semestres des formations de même code, avec le même semestre_id
et une date de début antérieure que celle du formsemestre.
Prend aussi les UE externes validées.
Attention: fonction très coûteuse, cacher le résultat.
Résultat: DataFrame avec
etudid (index)
formsemestre_id : origine de l'UE capitalisée
is_external : vrai si validation effectuée dans un semestre extérieur
ue_id : dans le semestre origine (pas toujours de la même formation)
ue_code : code de l'UE
moy_ue :
event_date :
} ]
"""
query = """
SELECT DISTINCT SFV.*, ue.ue_code
FROM
notes_ue ue,
notes_formations nf,
notes_formations nf2,
scolar_formsemestre_validation SFV,
notes_formsemestre sem,
notes_formsemestre_inscription ins
WHERE ue.formation_id = nf.id
and nf.formation_code = nf2.formation_code
and nf2.id=%(formation_id)s
and ins.etudid = SFV.etudid
and ins.formsemestre_id = %(formsemestre_id)s
and SFV.ue_id = ue.id
and SFV.code = 'ADM'
and ( (sem.id = SFV.formsemestre_id
and sem.date_debut < %(date_debut)s
and sem.semestre_id = %(semestre_id)s )
or (
((SFV.formsemestre_id is NULL) OR (SFV.is_external)) -- les UE externes ou "anterieures"
AND (SFV.semestre_id is NULL OR SFV.semestre_id=%(semestre_id)s)
) )
"""
params = {
"formation_id": formsemestre.formation.id,
"formsemestre_id": formsemestre.id,
"semestre_id": formsemestre.semestre_id,
"date_debut": formsemestre.date_debut,
}
df = pd.read_sql_query(query, db.engine, params=params, index_col="etudid")
return df

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.

View File

@ -12,6 +12,7 @@ from app.comp import moy_ue, moy_sem, inscr_mod
from app.comp.res_common import NotesTableCompat
from app.comp.bonus_spo import BonusSport
from app.models import ScoDocSiteConfig
from app.models.ues import UniteEns
from app.scodoc.sco_codes_parcours import UE_SPORT
@ -93,12 +94,16 @@ class ResultatsSemestreBUT(NotesTableCompat):
self.etud_moy_ue.clip(lower=0.0, upper=20.0, inplace=True)
# Moyenne générale indicative:
# (note: le bonus sport a déjà été appliqué aux moyenens d'UE, et impacte
# (note: le bonus sport a déjà été appliqué aux moyennes d'UE, et impacte
# donc la moyenne indicative)
self.etud_moy_gen = moy_sem.compute_sem_moys_apc(
self.etud_moy_ue, self.modimpl_coefs_df
)
self.etud_moy_gen_ranks = moy_sem.comp_ranks_series(self.etud_moy_gen)
# --- UE capitalisées
self.apply_capitalisation()
# --- Classements:
self.compute_rangs()
def get_etud_mod_moy(self, moduleimpl_id: int, etudid: int) -> float:
"""La moyenne de l'étudiant dans le moduleimpl
@ -109,3 +114,12 @@ class ResultatsSemestreBUT(NotesTableCompat):
etud_idx = self.etud_index[etudid]
# moyenne sur les UE:
return self.sem_cube[etud_idx, mod_idx].mean()
def compute_etud_ue_coef(self, etudid: int, ue: UniteEns) -> float:
"""Détermine le coefficient de l'UE pour cet étudiant.
N'est utilisé que pour l'injection des UE capitalisées dans la
moyenne générale.
En BUT, c'est simple: Coef = somme des coefs des modules vers cette UE.
(ne dépend pas des modules auxquels est inscrit l'étudiant, ).
"""
return self.modimpl_coefs_df.loc[ue.id].sum()

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

@ -6,15 +6,24 @@
"""Résultats semestres classiques (non APC)
"""
import numpy as np
import pandas as pd
from sqlalchemy.sql import text
from app.comp import moy_mod, moy_ue, moy_sem, inscr_mod
from flask import g, url_for
from app import db
from app import log
from app.comp import moy_mod, moy_ue, inscr_mod
from app.comp.res_common import NotesTableCompat
from app.comp.bonus_spo import BonusSport
from app.models import ScoDocSiteConfig
from app.models.etudiants import Identite
from app.models.formsemestre import FormSemestre
from app.models.ues import UniteEns
from app.scodoc.sco_codes_parcours import UE_SPORT
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.sco_utils import ModuleType
@ -104,8 +113,11 @@ class ResultatsSemestreClassic(NotesTableCompat):
self.bonus = (
bonus_mg # compat nt, utilisé pour l'afficher sur les bulletins
)
# --- UE capitalisées
self.apply_capitalisation()
# --- Classements:
self.etud_moy_gen_ranks = moy_sem.comp_ranks_series(self.etud_moy_gen)
self.compute_rangs()
def get_etud_mod_moy(self, moduleimpl_id: int, etudid: int) -> float:
"""La moyenne de l'étudiant dans le moduleimpl
@ -132,6 +144,42 @@ class ResultatsSemestreClassic(NotesTableCompat):
),
}
def compute_etud_ue_coef(self, etudid: int, ue: UniteEns) -> float:
"""Détermine le coefficient de l'UE pour cet étudiant.
N'est utilisé que pour l'injection des UE capitalisées dans la
moyenne générale.
Coef = somme des coefs des modules de l'UE auxquels il est inscrit
"""
c = comp_etud_sum_coef_modules_ue(self.formsemestre.id, etudid, ue["ue_id"])
if c is not None: # inscrit à au moins un module de cette UE
return c
# arfff: aucun moyen de déterminer le coefficient de façon sûre
log(
"* oups: calcul coef UE impossible\nformsemestre_id='%s'\netudid='%s'\nue=%s"
% (self.formsemestre.id, etudid, ue)
)
etud: Identite = Identite.query.get(etudid)
raise ScoValueError(
"""<div class="scovalueerror"><p>Coefficient de l'UE capitalisée %s impossible à déterminer
pour l'étudiant <a href="%s" class="discretelink">%s</a></p>
<p>Il faut <a href="%s">saisir le coefficient de cette UE avant de continuer</a></p>
</div>
"""
% (
ue.acronyme,
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid),
etud.nom_disp(),
url_for(
"notes.formsemestre_edit_uecoefs",
scodoc_dept=g.scodoc_dept,
formsemestre_id=self.formsemestre.id,
err_ue_id=ue["ue_id"],
),
)
)
return 0.0 # ?
def notes_sem_load_matrix(formsemestre: FormSemestre) -> tuple[np.ndarray, dict]:
"""Calcule la matrice des notes du semestre
@ -165,3 +213,29 @@ def notes_sem_assemble_matrix(modimpls_notes: list[pd.Series]) -> np.ndarray:
modimpls_notes = np.stack(modimpls_notes_arr)
# passe de (mod x etud) à (etud x mod)
return modimpls_notes.T
def comp_etud_sum_coef_modules_ue(formsemestre_id, etudid, ue_id):
"""Somme des coefficients des modules de l'UE dans lesquels cet étudiant est inscrit
ou None s'il n'y a aucun module.
"""
# comme l'ancien notes_table.comp_etud_sum_coef_modules_ue
# mais en raw sqlalchemy et la somme en SQL
sql = text(
"""
SELECT sum(mod.coefficient)
FROM notes_modules mod, notes_moduleimpl mi, notes_moduleimpl_inscription ins
WHERE mod.id = mi.module_id
and ins.etudid = :etudid
and ins.moduleimpl_id = mi.id
and mi.formsemestre_id = :formsemestre_id
and mod.ue_id = :ue_id
"""
)
cursor = db.session.execute(
sql, {"etudid": etudid, "formsemestre_id": formsemestre_id, "ue_id": ue_id}
)
r = cursor.fetchone()
if r is None:
return None
return r[0]

View File

@ -8,13 +8,22 @@ from collections import defaultdict, Counter
from functools import cached_property
import numpy as np
import pandas as pd
from flask import g
from app.comp.aux_stats import StatsMoyenne
from app.comp import moy_sem
from app.comp.res_cache import ResultatsCache
from app.comp import res_sem
from app.comp.moy_mod import ModuleImplResults
from app.models import FormSemestre, Identite, ModuleImpl
from app.models import FormSemestreUECoef
from app.models.ues import UniteEns
from app.scodoc import sco_utils as scu
from app.scodoc import sco_evaluations
from app.scodoc.sco_cache import ResultatsSemestreCache
from app.scodoc.sco_codes_parcours import UE_SPORT, ATT, DEF
from app.scodoc.sco_exceptions import ScoValueError
# Il faut bien distinguer
# - ce qui est caché de façon persistente (via redis):
@ -25,7 +34,7 @@ from app.scodoc.sco_codes_parcours import UE_SPORT, ATT, DEF
# (durée de vie de l'instance de ResultatsSemestre)
# qui sont notamment les attributs décorés par `@cached_property``
#
class ResultatsSemestre:
class ResultatsSemestre(ResultatsCache):
_cached_attrs = (
"etud_moy_gen_ranks",
"etud_moy_gen",
@ -36,7 +45,7 @@ class ResultatsSemestre:
)
def __init__(self, formsemestre: FormSemestre):
self.formsemestre: FormSemestre = formsemestre
super().__init__(formsemestre, ResultatsSemestreCache)
# BUT ou standard ? (apc == "approche par compétences")
self.is_apc = formsemestre.formation.is_apc()
# Attributs "virtuels", définis dans les sous-classes
@ -46,26 +55,10 @@ class ResultatsSemestre:
self.etud_moy_gen = {}
self.etud_moy_gen_ranks = {}
self.modimpls_results: ModuleImplResults = None
"Résultats de chaque modimpl: dict { modimpl.id : ModuleImplResults(Classique ou BUT) }"
self.etud_coef_ue_df = None
"""coefs d'UE effectifs pour chaque etudiant (pour form. classiques)"""
# TODO ?
def load_cached(self) -> bool:
"Load cached dataframes, returns False si pas en cache"
data = ResultatsSemestreCache.get(self.formsemestre.id)
if not data:
return False
for attr in self._cached_attrs:
setattr(self, attr, data[attr])
return True
def store(self):
"Cache our data"
ResultatsSemestreCache.set(
self.formsemestre.id,
{attr: getattr(self, attr) for attr in self._cached_attrs},
)
"""coefs d'UE effectifs pour chaque étudiant (pour form. classiques)"""
self.validations = None
def compute(self):
"Charge les notes et inscriptions et calcule toutes les moyennes"
@ -101,7 +94,8 @@ class ResultatsSemestre:
@cached_property
def ues(self) -> list[UniteEns]:
"""Liste des UEs du semestre (avec les UE bonus sport)
(indices des DataFrames)
(indices des DataFrames).
Note: un étudiant n'est pas nécessairement inscrit dans toutes ces UEs.
"""
return self.formsemestre.query_ues(with_sport=True).all()
@ -123,15 +117,34 @@ class ResultatsSemestre:
if m.module.module_type == scu.ModuleType.SAE
]
@cached_property
def ue_validables(self) -> list:
"""Liste des UE du semestre qui doivent être validées
(toutes sauf le sport)
"""
return self.formsemestre.query_ues().filter(UniteEns.type != UE_SPORT).all()
def get_etud_ue_validables(self, etudid: int) -> list[UniteEns]:
"""Liste des UEs du semestre qui doivent être validées
def modimpls_in_ue(self, ue_id, etudid):
"""Liste des modimpl de cet ue auxquels l'étudiant est inscrit"""
Rappel: l'étudiant est inscrit à des modimpls et non à des UEs.
- En BUT: on considère que l'étudiant va (ou non) valider toutes les UEs des modules
du parcours. XXX notion à implémenter, pour l'instant toutes les UE du semestre.
- En classique: toutes les UEs des modimpls auxquels l'étufdiant est inscrit sont
susceptibles d'être validées.
Les UE "bonus" (sport) ne sont jamais "validables".
"""
if self.is_apc:
# TODO: introduire la notion de parcours (#sco93)
return self.formsemestre.query_ues().filter(UniteEns.type != UE_SPORT).all()
else:
# restreint aux UE auxquelles l'étudiant est inscrit (dans l'un des modimpls)
ues = {
modimpl.module.ue
for modimpl in self.formsemestre.modimpls_sorted
if self.modimpl_inscr_df[modimpl.id][etudid]
}
ues = sorted(list(ues), key=lambda x: x.numero or 0)
return ues
def modimpls_in_ue(self, ue_id, etudid) -> list[ModuleImpl]:
"""Liste des modimpl de cette UE auxquels l'étudiant est inscrit"""
# sert pour l'affichage ou non de l'UE sur le bulletin
return [
modimpl
@ -148,6 +161,115 @@ class ResultatsSemestre:
"""
return self.etud_moy_ue > (seuil - scu.NOTES_TOLERANCE)
def apply_capitalisation(self):
"""Recalcule la moyenne générale pour prendre en compte d'éventuelles
UE capitalisées.
"""
# Supposant qu'il y a peu d'UE capitalisées,
# on va soustraire la moyenne d'UE et ajouter celle de l'UE capitalisée.
# return # XXX XXX XXX
if not self.validations:
self.validations = res_sem.load_formsemestre_validations(self.formsemestre)
ue_capitalisees = self.validations.ue_capitalisees
ue_by_code = {}
for etudid in ue_capitalisees.index:
recompute_mg = False
# ue_codes = set(ue_capitalisees.loc[etudid]["ue_code"])
# for ue_code in ue_codes:
# ue = ue_by_code.get(ue_code)
# if ue is None:
# ue = self.formsemestre.query_ues.filter_by(ue_code=ue_code)
# ue_by_code[ue_code] = ue
# Quand il y a une capitalisation, vérifie toutes les UEs
sum_notes_ue = 0.0
sum_coefs_ue = 0.0
for ue in self.formsemestre.query_ues():
ue_cap = self.get_etud_ue_status(etudid, ue.id)
if ue_cap["is_capitalized"]:
recompute_mg = True
coef = ue_cap["coef_ue"]
if not np.isnan(ue_cap["moy"]):
sum_notes_ue += ue_cap["moy"] * coef
sum_coefs_ue += coef
if recompute_mg and sum_coefs_ue > 0.0:
# On doit prendre en compte une ou plusieurs UE capitalisées
# et donc recalculer la moyenne générale
self.etud_moy_gen[etudid] = sum_notes_ue / sum_coefs_ue
def _get_etud_ue_cap(self, etudid, ue):
""""""
capitalisations = self.validations.ue_capitalisees.loc[etudid]
if isinstance(capitalisations, pd.DataFrame):
ue_cap = capitalisations[capitalisations["ue_code"] == ue.ue_code]
if isinstance(ue_cap, pd.DataFrame) and not ue_cap.empty:
# si plusieurs fois capitalisée, prend le max
cap_idx = ue_cap["moy_ue"].values.argmax()
ue_cap = ue_cap.iloc[cap_idx]
else:
if capitalisations["ue_code"] == ue.ue_code:
ue_cap = capitalisations
else:
ue_cap = None
return ue_cap
def get_etud_ue_status(self, etudid: int, ue_id: int) -> dict:
"""L'état de l'UE pour cet étudiant.
Result: dict
"""
if not self.validations:
self.validations = res_sem.load_formsemestre_validations(self.formsemestre)
ue = UniteEns.query.get(ue_id) # TODO cacher nos UEs ?
cur_moy_ue = self.etud_moy_ue[ue_id][etudid]
moy_ue = cur_moy_ue
is_capitalized = False
if etudid in self.validations.ue_capitalisees.index:
ue_cap = self._get_etud_ue_cap(etudid, ue)
if ue_cap is not None and not ue_cap.empty:
if ue_cap["moy_ue"] > cur_moy_ue:
moy_ue = ue_cap["moy_ue"]
is_capitalized = True
if is_capitalized:
coef_ue = 1.0
coef_ue = self.etud_coef_ue_df[ue_id][etudid]
return {
"is_capitalized": is_capitalized,
"is_external": ue_cap["is_external"] if is_capitalized else ue.is_external,
"coef_ue": coef_ue,
"cur_moy_ue": cur_moy_ue,
"moy": moy_ue,
"event_date": ue_cap["event_date"] if is_capitalized else None,
"ue": ue.to_dict(),
"formsemestre_id": ue_cap["formsemestre_id"] if is_capitalized else None,
"capitalized_ue_id": ue_cap["ue_id"] if is_capitalized else None,
}
def get_etud_ue_cap_coef(self, etudid, ue, ue_cap):
"""Calcule le coefficient d'une UE capitalisée, pour cet étudiant,
injectée dans le semestre courant.
ue : ue du semestre courant
ue_cap = resultat de formsemestre_get_etud_capitalisation
{ 'ue_id' (dans le semestre source),
'ue_code', 'moy', 'event_date','formsemestre_id' }
"""
# 1- Coefficient explicitement déclaré dans le semestre courant pour cette UE ?
ue_coef_db = FormSemestreUECoef.query.filter_by(
formsemestre_id=self.formsemestre.id, ue_id=ue.id
).first()
if ue_coef_db is not None:
return ue_coef_db.coefficient
# En APC: somme des coefs des modules vers cette UE
# En classique: Capitalisation UE externe: quel coef appliquer ?
# En ScoDoc 7, calculait la somme des coefs dans l'UE du semestre d'origine
# ici si l'étudiant est inscrit dans le semestre courant,
# somme des coefs des modules de l'UE auxquels il est inscrit
return self.compute_etud_ue_coef(etudid, ue)
# Pour raccorder le code des anciens codes qui attendent une NoteTable
class NotesTableCompat(ResultatsSemestre):
@ -163,6 +285,8 @@ class NotesTableCompat(ResultatsSemestre):
"bonus",
"bonus_ues",
"malus",
"etud_moy_gen_ranks",
"ue_rangs",
)
def __init__(self, formsemestre: FormSemestre):
@ -243,6 +367,35 @@ class NotesTableCompat(ResultatsSemestre):
modimpls_dict.append(d)
return modimpls_dict
def compute_rangs(self):
"""Calcule les classements
Moyenne générale: etud_moy_gen_ranks
Par UE:
"""
self.etud_moy_gen_ranks = moy_sem.comp_ranks_series(self.etud_moy_gen)
for ue in self.formsemestre.query_ues():
moy_ue = self.etud_moy_ue[ue.id]
self.ue_rangs[ue.id] = (
moy_sem.comp_ranks_series(moy_ue),
int(moy_ue.count()),
)
# .count() -> nb of non NaN values
def get_etud_decision_ues(self, etudid: int) -> dict:
"""Decisions du jury pour les UE de cet etudiant, ou None s'il n'y en pas eu.
Ne tient pas compte des UE capitalisées.
{ ue_id : { 'code' : ADM|CMP|AJ, 'event_date' : }
Ne renvoie aucune decision d'UE pour les défaillants
"""
if self.get_etud_etat(etudid) == DEF:
return {}
else:
if not self.validations:
self.validations = res_sem.load_formsemestre_validations(
self.formsemestre
)
return self.validations.decisions_jury_ues.get(etudid, None)
def get_etud_decision_sem(self, etudid: int) -> dict:
"""Decision du jury prise pour cet etudiant, ou None s'il n'y en pas eu.
{ 'code' : None|ATT|..., 'assidu' : 0|1, 'event_date' : , compense_formsemestre_id }
@ -256,12 +409,11 @@ class NotesTableCompat(ResultatsSemestre):
"compense_formsemestre_id": None,
}
else:
return {
"code": ATT, # XXX TODO
"assidu": True, # XXX TODO
"event_date": "",
"compense_formsemestre_id": None,
}
if not self.validations:
self.validations = res_sem.load_formsemestre_validations(
self.formsemestre
)
return self.validations.decisions_jury.get(etudid, None)
def get_etud_etat(self, etudid: int) -> str:
"Etat de l'etudiant: 'I', 'D', DEF ou '' (si pas connu dans ce semestre)"
@ -290,13 +442,29 @@ class NotesTableCompat(ResultatsSemestre):
"""
return self.etud_moy_gen[etudid]
def get_etud_ue_status(self, etudid: int, ue_id: int):
coef_ue = self.etud_coef_ue_df[ue_id][etudid]
def get_etud_ects_pot(self, etudid: int) -> dict:
"""
Un dict avec les champs
ects_pot : (float) nb de crédits ECTS qui seraient validés (sous réserve de validation par le jury),
ects_pot_fond: (float) nb d'ECTS issus d'UE fondamentales (non électives)
Ce sont les ECTS des UE au dessus de la barre (10/20 en principe), avant le jury (donc non
encore enregistrées).
"""
# was nt.get_etud_moy_infos
# XXX pour compat nt, à remplacer ultérieurement
ues = self.get_etud_ue_validables(etudid)
ects_pot = 0.0
for ue in ues:
if (
ue.id in self.etud_moy_ue
and ue.ects is not None
and self.etud_moy_ue[ue.id][etudid] > self.parcours.NOTES_BARRE_VALID_UE
):
ects_pot += ue.ects
return {
"cur_moy_ue": self.etud_moy_ue[ue_id][etudid],
"moy": self.etud_moy_ue[ue_id][etudid],
"is_capitalized": False, # XXX TODO
"coef_ue": coef_ue, # XXX TODO
"ects_pot": ects_pot,
"ects_fond": 0.0, # not implemented (anciennemment pour école ingé)
}
def get_etud_rang(self, etudid: int):
@ -332,8 +500,32 @@ class NotesTableCompat(ResultatsSemestre):
evals_results.append(d)
return evals_results
def get_evaluations_etats(self):
"""[ {...evaluation et son etat...} ]"""
# TODO: à moderniser
if not hasattr(self, "_evaluations_etats"):
self._evaluations_etats = sco_evaluations.do_evaluation_list_in_sem(
self.formsemestre.id
)
return self._evaluations_etats
def get_mod_evaluation_etat_list(self, moduleimpl_id) -> list[dict]:
"""Liste des états des évaluations de ce module"""
# XXX TODO à moderniser: lent, recharge des donénes que l'on a déjà...
return [
e
for e in self.get_evaluations_etats()
if e["moduleimpl_id"] == moduleimpl_id
]
def get_moduleimpls_attente(self):
return [] # XXX TODO
"""Liste des modimpls du semestre ayant des notes en attente"""
return [
modimpl
for modimpl in self.formsemestre.modimpls_sorted
if self.modimpls_results[modimpl.id].en_attente
]
def get_mod_stats(self, moduleimpl_id: int) -> dict:
"""Stats sur les notes obtenues dans un modimpl

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.
"""
@ -427,10 +427,12 @@ class FormSemestreUECoef(db.Model):
formsemestre_id = db.Column(
db.Integer,
db.ForeignKey("notes_formsemestre.id"),
index=True,
)
ue_id = db.Column(
db.Integer,
db.ForeignKey("notes_ue.id"),
index=True,
)
coefficient = db.Column(db.Float, nullable=False)

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

@ -703,11 +703,12 @@ class NotesTable:
ue_status = {
'est_inscrit' : True si étudiant inscrit à au moins un module de cette UE
'moy' : moyenne, avec capitalisation eventuelle
'capitalized_ue_id' : id de l'UE capitalisée
'coef_ue' : coef de l'UE utilisé pour le calcul de la moyenne générale
(la somme des coefs des modules, ou le coef d'UE capitalisée,
ou encore le coef d'UE si l'option use_ue_coefs est active)
'cur_moy_ue' : moyenne de l'UE en cours (sans considérer de capitalisation)
'cur_coef_ue': coefficient de l'UE courante
'cur_coef_ue': coefficient de l'UE courante (inutilisé ?)
'is_capitalized' : True|False,
'ects_pot' : (float) nb de crédits ECTS qui seraient validés (sous réserve de validation par le jury),
'ects_pot_fond': 0. si UE non fondamentale, = ects_pot sinon,
@ -935,7 +936,7 @@ class NotesTable:
"""
return self.moy_gen[etudid]
def get_etud_moy_infos(self, etudid):
def get_etud_moy_infos(self, etudid): # XXX OBSOLETE
"""Infos sur moyennes"""
return self.etud_moy_infos[etudid]
@ -1011,7 +1012,10 @@ class NotesTable:
cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
cursor.execute(
"select etudid, code, assidu, compense_formsemestre_id, event_date from scolar_formsemestre_validation where formsemestre_id=%(formsemestre_id)s and ue_id is NULL;",
"""SELECT etudid, code, assidu, compense_formsemestre_id, event_date
FROM scolar_formsemestre_validation
WHERE formsemestre_id=%(formsemestre_id)s AND ue_id is NULL;
""",
{"formsemestre_id": self.formsemestre_id},
)
decisions_jury = {}
@ -1137,8 +1141,14 @@ class NotesTable:
"""
self.ue_capitalisees = scu.DictDefault(defaultvalue=[])
cnx = None
semestre_id = self.sem["semestre_id"]
for etudid in self.get_etudids():
capital = formsemestre_get_etud_capitalisation(self.sem, etudid)
capital = formsemestre_get_etud_capitalisation(
self.formation["id"],
semestre_id,
ndb.DateDMYtoISO(self.sem["date_debut"]),
etudid,
)
for ue_cap in capital:
# Si la moyenne d'UE n'avait pas été stockée (anciennes versions de ScoDoc)
# il faut la calculer ici et l'enregistrer
@ -1308,7 +1318,7 @@ class NotesTable:
"""Liste des evaluations de ce semestre, avec leur etat"""
return self.get_evaluations_etats()
def get_mod_evaluation_etat_list(self, moduleimpl_id):
def get_mod_evaluation_etat_list(self, moduleimpl_id) -> list[dict]:
"""Liste des évaluations de ce module"""
return [
e

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

@ -123,9 +123,6 @@ def module_create(
Sinon, donne le choix de l'UE de rattachement et utilise la première
matière de cette UE (si elle n'existe pas, la crée).
"""
from app.scodoc import sco_formations
from app.scodoc import sco_edit_ue
if matiere_id:
matiere = Matiere.query.get_or_404(matiere_id)
ue = matiere.ue
@ -160,7 +157,7 @@ def module_create(
else:
H += [
f"""<h2>Création {object_name} dans la matière {matiere.titre},
(UE {ue.acronyme})</h2>
(UE {ue.acronyme}), semestre {ue.semestre_idx}</h2>
"""
]
@ -534,19 +531,15 @@ def module_edit(module_id=None):
formsemestres=FormSemestre.query.filter(
ModuleImpl.formsemestre_id == FormSemestre.id,
ModuleImpl.module_id == module_id,
).all(),
)
.order_by(FormSemestre.date_debut)
.all(),
),
]
if not unlocked:
H.append(
"""<div class="ue_warning"><span>Formation verrouillée, seuls certains éléments peuvent être modifiés</span></div>"""
)
if in_use:
H.append(
"""<div class="ue_warning"><span>Module déjà utilisé dans des semestres,
soyez prudents !
</span></div>"""
)
if is_apc:
module_types = scu.ModuleType # tous les types
else:
@ -728,30 +721,9 @@ def module_edit(module_id=None):
initvalues=module,
submitlabel="Modifier ce module",
)
# Affiche liste des formseemstre utilisant ce module
if in_use:
formsemestre_ids = {modimpl.formsemestre_id for modimpl in a_module.modimpls}
formsemestres = [FormSemestre.query.get(fid) for fid in formsemestre_ids]
formsemestres.sort(key=lambda f: f.date_debut)
items = [
f"""<a class="stdlink" href="{
url_for("notes.formsemestre_status", scodoc_dept=g.scodoc_dept, formsemestre_id=f.id )
}">{f.titre}</a>"""
for f in formsemestres
]
sem_descr = f"""
<div class="ue_warning">
<div>Ce module est utilisé dans les formsemestres suivants:</div>
<ul><li>
{"</li><li>".join( items )}
</li></ul>
</div>
"""
else:
sem_descr = ""
#
if tf[0] == 0:
return "\n".join(H) + tf[1] + sem_descr + html_sco_header.sco_footer()
return "\n".join(H) + tf[1] + html_sco_header.sco_footer()
elif tf[0] == -1:
return flask.redirect(
url_for(

View File

@ -244,18 +244,22 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
"""Formulaire modification ou création d'une UE"""
create = int(create)
if not create:
U = ue_list(args={"ue_id": ue_id})
if not U:
raise ScoValueError("UE inexistante !")
U = U[0]
formation_id = U["formation_id"]
title = "Modification de l'UE %(titre)s" % U
initvalues = U
ue: UniteEns = UniteEns.query.get_or_404(ue_id)
ue_dict = ue.to_dict()
formation_id = ue.formation_id
title = f"Modification de l'UE {ue.acronyme} {ue.titre}"
initvalues = ue_dict
submitlabel = "Modifier les valeurs"
can_change_semestre_id = ue.modules.count() == 0
else:
ue = None
title = "Création d'une UE"
initvalues = {"semestre_idx": default_semestre_idx}
initvalues = {
"semestre_idx": default_semestre_idx,
"color": ue_guess_color_default(formation_id, default_semestre_idx),
}
submitlabel = "Créer cette UE"
can_change_semestre_id = True
formation = Formation.query.get(formation_id)
if not formation:
raise ScoValueError(f"Formation inexistante ! (id={formation_id})")
@ -282,7 +286,7 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
ue_types_names = [sco_codes_parcours.UE_TYPE_NAME[k] for k in ue_types]
ue_types = [str(x) for x in ue_types]
fw = [
form_descr = [
("ue_id", {"input_type": "hidden"}),
("create", {"input_type": "hidden", "default": create}),
("formation_id", {"input_type": "hidden", "default": formation_id}),
@ -296,18 +300,28 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
"type": "int",
},
),
(
"semestre_idx",
{
"input_type": "menu",
"type": "int",
"allow_null": False,
"title": parcours.SESSION_NAME.capitalize(),
"explanation": "%s de l'UE dans la formation" % parcours.SESSION_NAME,
"labels": ["non spécifié"] + [str(x) for x in semestres_indices],
"allowed_values": [""] + semestres_indices,
},
),
]
if can_change_semestre_id:
form_descr += [
(
"semestre_idx",
{
"input_type": "menu",
"type": "int",
"allow_null": False,
"title": parcours.SESSION_NAME.capitalize(),
"explanation": "%s de l'UE dans la formation"
% parcours.SESSION_NAME,
"labels": ["non spécifié"] + [str(x) for x in semestres_indices],
"allowed_values": [""] + semestres_indices,
},
),
]
else:
form_descr += [
("semestre_idx", {"default": ue.semestre_idx, "input_type": "hidden"}),
]
form_descr += [
(
"type",
{
@ -377,7 +391,7 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
),
]
if create and not parcours.UE_IS_MODULE and not is_apc:
fw.append(
form_descr.append(
(
"create_matiere",
{
@ -391,14 +405,33 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
tf = TrivialFormulator(
request.base_url,
scu.get_request_args(),
fw,
form_descr,
initvalues=initvalues,
submitlabel=submitlabel,
)
if tf[0] == 0:
ue_div = """<div id="ue_list_code"></div>"""
if ue and ue.modules.count():
modules_div = f"""<div id="ue_list_modules">
<div><b>{ue.modules.count()} modules sont rattachés
à cette UE</b> du semestre S{ue.semestre_idx},
elle ne peut donc pas être changée de semestre.</div>
<ul>"""
for m in ue.modules:
modules_div += f"""<li><a class="stdlink" href="{url_for(
"notes.module_edit",scodoc_dept=g.scodoc_dept, module_id=m.id)}">{m.code} {m.titre}</a></li>"""
modules_div += """</ul></div>"""
else:
modules_div = ""
bonus_div = """<div id="bonus_description"></div>"""
return "\n".join(H) + tf[1] + bonus_div + ue_div + html_sco_header.sco_footer()
ue_div = """<div id="ue_list_code"></div>"""
return (
"\n".join(H)
+ tf[1]
+ modules_div
+ bonus_div
+ ue_div
+ html_sco_header.sco_footer()
)
else:
if create:
if not tf[2]["ue_code"]:
@ -547,14 +580,15 @@ def ue_table(formation_id=None, semestre_idx=1, msg=""): # was ue_list
formation_id=formation_id, is_external=True
)
if is_apc:
# pour faciliter la transition des anciens programmes non APC
# Pour faciliter la transition des anciens programmes non APC
for ue in ues_obj:
ue.guess_semestre_idx()
# vérifie qu'on a bien au moins une matière dans chaque UE
for ue in ues_obj:
# vérifie qu'on a bien au moins une matière dans chaque UE
if ue.matieres.count() < 1:
mat = Matiere(ue_id=ue.id)
db.session.add(mat)
# donne des couleurs aux UEs crées avant
colorie_anciennes_ues(ues_obj)
db.session.commit()
ues = [ue.to_dict() for ue in ues_obj]
ues_externes = [ue.to_dict() for ue in ues_externes_obj]
@ -1370,7 +1404,7 @@ def formation_table_recap(formation_id, format="html"):
return tab.make_page(format=format)
def ue_list_semestre_ids(ue):
def ue_list_semestre_ids(ue: dict):
"""Liste triée des numeros de semestres des modules dans cette UE
Il est recommandable que tous les modules d'une UE aient le même indice de semestre.
Mais cela n'a pas toujours été le cas dans les programmes pédagogiques officiels,
@ -1378,3 +1412,45 @@ def ue_list_semestre_ids(ue):
"""
modules = sco_edit_module.module_list(args={"ue_id": ue["ue_id"]})
return sorted(list(set([mod["semestre_id"] for mod in modules])))
UE_PALETTE = [
"#EFA00B",
"#99C24D",
"#EC9192",
"#0075C4",
"#D65108",
"#DEC0F1",
"#B02E0C",
"#151E3F",
"#FB3640",
]
def colorie_anciennes_ues(ues: list[UniteEns]) -> None:
"""Avant ScoDoc 9.2, les ue n'avaient pas de couleurs
Met des défauts raisonnables
"""
nb_colors = len(UE_PALETTE)
index = 0
last_sem_idx = 0
for ue in ues:
if ue.semestre_idx != last_sem_idx:
last_sem_idx = ue.semestre_idx
index = 0
if ue.color is None:
ue.color = UE_PALETTE[index % nb_colors]
index += 1
db.session.add(ue)
def ue_guess_color_default(formation_id: int, default_semestre_idx: int) -> str:
"""Un code couleur pour une nouvelle UE dans ce semestre"""
nb_colors = len(UE_PALETTE)
# UE existantes dans ce semestre:
nb_ues = UniteEns.query.filter(
UniteEns.formation_id == formation_id,
UniteEns.semestre_idx == default_semestre_idx,
).count()
index = nb_ues
return UE_PALETTE[index % nb_colors]

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

@ -31,15 +31,21 @@ import time
import flask
from flask import url_for, g, request
from app.api.sco_api import formsemestre
import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu
from app import log
from app.scodoc.scolog import logdb
from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message
from app.scodoc.sco_exceptions import ScoValueError
from app.comp import res_sem
from app.comp.res_common import NotesTableCompat
from app.models import FormSemestre
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.scolog import logdb
from app.scodoc.sco_codes_parcours import *
from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message
from app.scodoc import html_sco_header
from app.scodoc import sco_abs
from app.scodoc import sco_codes_parcours
@ -542,9 +548,9 @@ def formsemestre_recap_parcours_table(
else:
ass = ""
nt = sco_cache.NotesTableCache.get(
sem["formsemestre_id"]
) # > get_ues_stat_dict, get_etud_moy_gen, get_etud_ue_status
formsemestre = FormSemestre.query.get(sem["formsemestre_id"])
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
# nt = sco_cache.NotesTableCache.get(sem["formsemestre_id"])
if is_cur:
type_sem = "*" # now unused
class_sem = "sem_courant"
@ -686,7 +692,7 @@ def formsemestre_recap_parcours_table(
sco_preferences.get_preference("bul_show_ects", sem["formsemestre_id"])
or nt.parcours.ECTS_ONLY
):
etud_moy_infos = nt.get_etud_moy_infos(etudid)
etud_ects_infos = nt.get_etud_ects_pot(etudid)
H.append(
'<tr class="%s rcp_l2 sem_%s">' % (class_sem, sem["formsemestre_id"])
)
@ -697,7 +703,7 @@ def formsemestre_recap_parcours_table(
# total ECTS (affiché sous la moyenne générale)
H.append(
'<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
@ -1056,7 +1062,7 @@ def formsemestre_validate_previous_ue(formsemestre_id, etudid):
"title": "Indice du semestre",
"explanation": "Facultatif: indice du semestre dans la formation",
"allow_null": True,
"allowed_values": [""] + [str(x) for x in range(11)],
"allowed_values": [""] + [x for x in range(11)],
"labels": ["-"] + list(range(11)),
},
),

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,30 +1064,32 @@ def formsemestre_get_etud_capitalisation(sem, etudid):
cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
cursor.execute(
"""select distinct SFV.*, ue.ue_code from notes_ue ue, notes_formations nf,
notes_formations nf2, scolar_formsemestre_validation SFV, notes_formsemestre sem
"""
SELECT DISTINCT SFV.*, ue.ue_code
FROM notes_ue ue, notes_formations nf,
notes_formations nf2, scolar_formsemestre_validation SFV, notes_formsemestre sem
WHERE ue.formation_id = nf.id
WHERE ue.formation_id = nf.id
and nf.formation_code = nf2.formation_code
and nf2.id=%(formation_id)s
and SFV.ue_id = ue.id
and SFV.code = 'ADM'
and SFV.etudid = %(etudid)s
and ( (sem.id = SFV.formsemestre_id
and sem.date_debut < %(date_debut)s
and sem.semestre_id = %(semestre_id)s )
or (
((SFV.formsemestre_id is NULL) OR (SFV.is_external)) -- les UE externes ou "anterieures"
AND (SFV.semestre_id is NULL OR SFV.semestre_id=%(semestre_id)s)
and ( (sem.id = SFV.formsemestre_id
and sem.date_debut < %(date_debut)s
and sem.semestre_id = %(semestre_id)s )
or (
((SFV.formsemestre_id is NULL) OR (SFV.is_external)) -- les UE externes ou "anterieures"
AND (SFV.semestre_id is NULL OR SFV.semestre_id=%(semestre_id)s)
) )
""",
{
"etudid": etudid,
"formation_id": sem["formation_id"],
"semestre_id": sem["semestre_id"],
"date_debut": ndb.DateDMYtoISO(sem["date_debut"]),
"formation_id": formation_id,
"semestre_id": semestre_idx,
"date_debut": date_debut,
},
)

View File

@ -53,6 +53,7 @@ from reportlab.lib import styles
import flask
from flask import url_for, g, request
from app.models.ues import UniteEns
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
@ -140,23 +141,21 @@ def descr_autorisations(autorisations):
return ", ".join(alist)
def _comp_ects_by_ue_code_and_type(nt, decision_ues):
def _comp_ects_by_ue_code(nt, decision_ues):
"""Calcul somme des ECTS validés dans ce semestre (sans les UE capitalisées)
decision_ues est le resultat de nt.get_etud_decision_ues
Chaque resultat est un dict: { ue_code : ects }
"""
if not decision_ues:
return {}, {}
return {}
ects_by_ue_code = {}
ects_by_ue_type = scu.DictDefault(defaultvalue=0) # { ue_type : ects validés }
for ue_id in decision_ues:
d = decision_ues[ue_id]
ue = nt.uedict[ue_id]
ects_by_ue_code[ue["ue_code"]] = d["ects"]
ects_by_ue_type[ue["type"]] += d["ects"]
ue = UniteEns.query.get(ue_id)
ects_by_ue_code[ue.ue_code] = d["ects"]
return ects_by_ue_code, ects_by_ue_type
return ects_by_ue_code
def _comp_ects_capitalises_by_ue_code(nt, etudid):
@ -166,10 +165,7 @@ def _comp_ects_capitalises_by_ue_code(nt, etudid):
for ue in ues:
ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"])
if ue_status["is_capitalized"]:
try:
ects_val = float(ue_status["ue"]["ects"])
except (ValueError, TypeError):
ects_val = 0.0
ects_val = float(ue_status["ue"]["ects"] or 0.0)
ects_by_ue_code[ue["ue_code"]] = ects_val
return ects_by_ue_code
@ -249,11 +245,8 @@ def dict_pvjury(
ects_capitalises_by_ue_code = _comp_ects_capitalises_by_ue_code(nt, etudid)
d["sum_ects_capitalises"] = sum(ects_capitalises_by_ue_code.values())
ects_by_ue_code, ects_by_ue_type = _comp_ects_by_ue_code_and_type(
nt, d["decisions_ue"]
)
ects_by_ue_code = _comp_ects_by_ue_code(nt, d["decisions_ue"])
d["sum_ects"] = _sum_ects_dicts(ects_capitalises_by_ue_code, ects_by_ue_code)
d["sum_ects_by_type"] = ects_by_ue_type
if d["decision_sem"] and sco_codes_parcours.code_semestre_validant(
d["decision_sem"]["code"]

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 = [
@ -649,12 +645,12 @@ def make_formsemestre_recapcomplet(
): # Rang: force tri numerique pour sortable
cls = cls + " sortnumeric"
if F[0][i] in cod2mod: # lien vers etat module
mod = cod2mod[F[0][i]]
modimpl = cod2mod[F[0][i]]
cells += '<td class="%s"><a href="moduleimpl_status?moduleimpl_id=%s" title="%s (%s)">%s</a></td>' % (
cls,
mod["moduleimpl_id"],
mod["module"]["titre"],
sco_users.user_info(mod["responsable_id"])["nomcomplet"],
modimpl.id,
modimpl.module.titre,
sco_users.user_info(modimpl.responsable_id)["nomcomplet"],
F[0][i],
)
else:
@ -838,63 +834,50 @@ def make_formsemestre_recapcomplet(
raise ValueError("unknown format %s" % format)
def _list_notes_evals(evals, etudid):
def _list_notes_evals(evals: list[Evaluation], etudid: int) -> list[str]:
"""Liste des notes des evaluations completes de ce module
(pour table xls avec evals)
"""
L = []
for e in evals:
if (
e["etat"]["evalcomplete"]
or e["etat"]["evalattente"]
or e["publish_incomplete"]
):
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(e["evaluation_id"])
if etudid in notes_db:
val = notes_db[etudid]["value"]
else:
# Note manquante mais prise en compte immédiate: affiche ATT
val = scu.NOTES_ATTENTE
val_fmt = scu.fmt_note(val, keep_numeric=True)
L.append(val_fmt)
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(e.evaluation_id)
if etudid in notes_db:
val = notes_db[etudid]["value"]
else:
# Note manquante mais prise en compte immédiate: affiche ATT
val = scu.NOTES_ATTENTE
val_fmt = scu.fmt_note(val, keep_numeric=True)
L.append(val_fmt)
return L
def _list_notes_evals_titles(codemodule, evals):
def _list_notes_evals_titles(codemodule: str, evals: list[Evaluation]) -> list[str]:
"""Liste des titres des evals completes"""
L = []
eval_index = len(evals) - 1
for e in evals:
if (
e["etat"]["evalcomplete"]
or e["etat"]["evalattente"]
or e["publish_incomplete"]
):
L.append(codemodule + "-" + str(eval_index) + "-" + e["jour"].isoformat())
L.append(codemodule + "-" + str(eval_index) + "-" + e.jour.isoformat())
eval_index -= 1
return L
def _list_notes_evals_stats(evals, key):
def _list_notes_evals_stats(evals: list[Evaluation], key: str) -> list[str]:
"""Liste des stats (moy, ou rien!) des evals completes"""
L = []
for e in evals:
if (
e["etat"]["evalcomplete"]
or e["etat"]["evalattente"]
or e["publish_incomplete"]
):
if key == "moy":
val = e["etat"]["moy_num"]
L.append(scu.fmt_note(val, keep_numeric=True))
elif key == "max":
L.append(e["note_max"])
elif key == "min":
L.append(0.0)
elif key == "coef":
L.append(e["coefficient"])
else:
L.append("") # on n'a pas sous la main min/max
if key == "moy":
# TODO #sco92
# val = e["etat"]["moy_num"]
# L.append(scu.fmt_note(val, keep_numeric=True))
L.append("")
elif key == "max":
L.append(e.note_max)
elif key == "min":
L.append(0.0)
elif key == "coef":
L.append(e.coefficient)
else:
L.append("") # on n'a pas sous la main min/max
return L

View File

@ -1771,40 +1771,54 @@ ul.notes_module_list {
}
div#ue_list_code {
background-color: rgb(220,220,220);
font-size: small;
padding-left: 4px;
padding-bottom: 1px;
background-color: rgb(155, 218, 155);
padding: 10px;
border: 1px solid blue;
border-radius: 10px;
padding: 10px;
margin-top: 10px;
margin: 3ex;
margin-right: 15px;
}
ul.notes_module_list {
list-style-type: none;
}
div#ue_list_modules {
background-color: rgb(191, 242, 255);
border: 1px solid blue;
border-radius: 10px;
padding: 10px;
margin-top: 10px;
margin-right: 15px;
}
div#ue_list_etud_validations {
background-color: rgb(220,250,220);
padding-left: 4px;
padding-bottom: 1px;
margin: 3ex;
background-color: rgb(220,250,220);
padding-left: 4px;
padding-bottom: 1px;
margin: 3ex;
}
div#ue_list_etud_validations span {
font-weight: bold;
}
span.ue_share {
font-weight: bold;
font-weight: bold;
}
div.ue_warning {
border: 1px solid red;
background-color: rgb(250,220,220);
margin: 3ex;
padding-left: 1ex;
padding-right: 1ex;
border: 1px solid red;
border-radius: 10px;
background-color: rgb(250,220,220);
margin-top: 10px;
margin-right: 15px;
margin-bottom: 10px;
padding: 10px;
}
div.ue_warning:first-child {
font-weight: bold;
}
div.ue_warning span:before {
content: url(/ScoDoc/static/icons/warning_img.png);
vertical-align: -80%;

View File

@ -1,3 +1,4 @@
{# -*- mode: jinja-html -*- #}
<div class="help">
<p class="help">
Les modules sont décrits dans le programme pédagogique. Un module est pour ce
@ -24,24 +25,26 @@
<a href="https://scodoc.org/BUT" target="_blank">la documentation</a>.
</p>
{%endif%}
</div>
{% if formsemestres %}
<p class="help">
Ce module est utilisé dans des semestres déjà mis en place, il faut prêter attention
aux conséquences des changements effectués ici: par exemple les coefficients vont modifier
les notes moyennes calculées. Les modules déjà utilisés ne peuvent pas être changés de semestre, ni détruits.
Si vous souhaitez faire cela, allez d'abord modifier les semestres concernés pour déselectionner le module.
</p>
<h4>Semestres utilisant ce module:</h4>
<ul>
{% if formsemestres %}
<div class="ue_warning"> <span><b>Module déjà utilisé dans des semestres, soyez prudents !</b></span>
<p class="help">
Ce module est utilisé dans des semestres déjà mis en place, il faut prêter attention
aux conséquences des changements effectués ici: par exemple les coefficients vont modifier
les notes moyennes calculées. Les modules déjà utilisés ne peuvent pas être changés de semestre, ni détruits.
Si vous souhaitez faire cela, allez d'abord modifier les semestres concernés pour déselectionner le module.
</p>
<h4>Semestres utilisant ce module:</h4>
<ul>
{%for formsemestre in formsemestres %}
<li><a class="stdlink" href="{{
url_for('notes.formsemestre_status',
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id)
}}">{{formsemestre.titre_mois()}}</a>
</li>
<li><a class="stdlink" href="{{
url_for('notes.formsemestre_status',
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id)
}}">{{formsemestre.titre_mois()}}</a>
</li>
{% endfor %}
</ul>
{%endif%}
</ul>
</div>
{%endif%}
</div>

View File

@ -496,6 +496,7 @@ def SignaleAbsenceGrSemestre(
require_module = sco_preferences.get_preference(
"abs_require_module", formsemestre_id
)
sem = sco_formsemestre.do_formsemestre_list({"formsemestre_id": formsemestre_id})[0]
etuds = [
sco_etud.get_etud_info(etudid=m["etudid"], filled=True)[0]
for m in groups_infos.members
@ -526,9 +527,7 @@ def SignaleAbsenceGrSemestre(
if etuds:
nt = sco_cache.NotesTableCache.get(formsemestre_id)
sem = sco_formsemestre.do_formsemestre_list(
{"formsemestre_id": formsemestre_id}
)[0]
work_saturday = sco_abs.is_work_saturday()
jourdebut = sco_abs.ddmmyyyy(datedebut, work_saturday=work_saturday)
jourfin = sco_abs.ddmmyyyy(datefin, work_saturday=work_saturday)

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