forked from ScoDoc/ScoDoc
Génération bulletin BUT json
This commit is contained in:
parent
1a673862aa
commit
3ba30f6250
224
app/but/bulletin_but.py
Normal file
224
app/but/bulletin_but.py
Normal file
@ -0,0 +1,224 @@
|
|||||||
|
##############################################################################
|
||||||
|
# ScoDoc
|
||||||
|
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
|
||||||
|
# See LICENSE
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from app import db
|
||||||
|
|
||||||
|
from app.comp import df_cache, moy_ue, moy_mod, inscr_mod
|
||||||
|
from app.scodoc import sco_utils as scu
|
||||||
|
from app.scodoc.sco_cache import ResultatsSemestreBUTCache
|
||||||
|
from app.scodoc.sco_exceptions import ScoFormatError
|
||||||
|
from app.scodoc.sco_utils import jsnan
|
||||||
|
|
||||||
|
|
||||||
|
class ResultatsSemestreBUT:
|
||||||
|
"""Structure légère pour stocker les résultats du semestre et
|
||||||
|
générer les bulletins.
|
||||||
|
__init__ : charge depuis le cache ou calcule
|
||||||
|
invalidate(): invalide données cachées
|
||||||
|
"""
|
||||||
|
|
||||||
|
_cached_attrs = (
|
||||||
|
"sem_cube",
|
||||||
|
"modimpl_inscr_df",
|
||||||
|
"modimpl_coefs_df",
|
||||||
|
"etud_moy_ue",
|
||||||
|
"modimpls_evals_poids",
|
||||||
|
"modimpls_evals_notes",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, formsemestre):
|
||||||
|
self.formsemestre = formsemestre
|
||||||
|
self.ues = formsemestre.query_ues().all()
|
||||||
|
self.modimpls = formsemestre.modimpls.all()
|
||||||
|
self.etuds = self.formsemestre.etuds.all()
|
||||||
|
self.etud_index = {e.id: idx for idx, e in enumerate(self.etuds)}
|
||||||
|
self.saes = [
|
||||||
|
m for m in self.modimpls if m.module.module_type == scu.ModuleType.SAE
|
||||||
|
]
|
||||||
|
self.ressources = [
|
||||||
|
m for m in self.modimpls if m.module.module_type == scu.ModuleType.RESSOURCE
|
||||||
|
]
|
||||||
|
if not self.load_cached():
|
||||||
|
self.compute()
|
||||||
|
self.store()
|
||||||
|
|
||||||
|
def load_cached(self) -> bool:
|
||||||
|
"Load cached dataframes, returns False si pas en cache"
|
||||||
|
data = ResultatsSemestreBUTCache.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 dataframes"
|
||||||
|
ResultatsSemestreBUTCache.set(
|
||||||
|
self.formsemestre.id,
|
||||||
|
{attr: getattr(self, attr) for attr in self._cached_attrs},
|
||||||
|
)
|
||||||
|
|
||||||
|
def compute(self):
|
||||||
|
"Charge les notes et inscriptions et calcule toutes les moyennes"
|
||||||
|
(
|
||||||
|
self.sem_cube,
|
||||||
|
self.modimpls_evals_poids,
|
||||||
|
self.modimpls_evals_notes,
|
||||||
|
_,
|
||||||
|
) = moy_ue.notes_sem_load_cube(self.formsemestre)
|
||||||
|
self.modimpl_inscr_df = inscr_mod.df_load_modimpl_inscr(self.formsemestre)
|
||||||
|
self.modimpl_coefs_df, _, _ = moy_ue.df_load_modimpl_coefs(
|
||||||
|
self.formsemestre, ues=self.ues, modimpls=self.modimpls
|
||||||
|
)
|
||||||
|
# l'idx de la colonne du mod modimpl.id est
|
||||||
|
# modimpl_coefs_df.columns.get_loc(modimpl.id)
|
||||||
|
# idx de l'UE: modimpl_coefs_df.index.get_loc(ue.id)
|
||||||
|
self.etud_moy_ue = moy_ue.compute_ue_moys(
|
||||||
|
self.sem_cube,
|
||||||
|
self.etuds,
|
||||||
|
self.modimpls,
|
||||||
|
self.ues,
|
||||||
|
self.modimpl_inscr_df,
|
||||||
|
self.modimpl_coefs_df,
|
||||||
|
)
|
||||||
|
|
||||||
|
def etud_ue_mod_results(self, etud, ue, modimpls) -> dict:
|
||||||
|
"dict synthèse résultats dans l'UE pour les modules indiqués"
|
||||||
|
d = {}
|
||||||
|
etud_idx = self.etud_index[etud.id]
|
||||||
|
ue_idx = self.modimpl_coefs_df.index.get_loc(ue.id)
|
||||||
|
etud_moy_module = self.sem_cube[etud_idx] # module x UE
|
||||||
|
for mi in modimpls:
|
||||||
|
d[mi.module.code] = {
|
||||||
|
"id": mi.id,
|
||||||
|
"coef": self.modimpl_coefs_df[mi.id][ue.id],
|
||||||
|
"moyenne": jsnan(
|
||||||
|
etud_moy_module[self.modimpl_coefs_df.columns.get_loc(mi.id)][
|
||||||
|
ue_idx
|
||||||
|
]
|
||||||
|
),
|
||||||
|
}
|
||||||
|
return d
|
||||||
|
|
||||||
|
def etud_ue_results(self, etud, ue):
|
||||||
|
"dict synthèse résultats UE"
|
||||||
|
d = {
|
||||||
|
"id": ue.id,
|
||||||
|
"ECTS": {
|
||||||
|
"acquis": 0, # XXX TODO voir jury
|
||||||
|
"total": ue.ects,
|
||||||
|
},
|
||||||
|
"competence": None, # XXX TODO lien avec référentiel
|
||||||
|
"moyenne": jsnan(self.etud_moy_ue[ue.id].mean()),
|
||||||
|
"bonus": None, # XXX TODO
|
||||||
|
"malus": None, # XXX TODO voir ce qui est ici
|
||||||
|
"capitalise": None, # "AAAA-MM-JJ" TODO
|
||||||
|
"ressources": self.etud_ue_mod_results(etud, ue, self.ressources),
|
||||||
|
"saes": self.etud_ue_mod_results(etud, ue, self.saes),
|
||||||
|
}
|
||||||
|
return d
|
||||||
|
|
||||||
|
def etud_mods_results(self, etud, modimpls) -> dict:
|
||||||
|
"""dict synthèse résultats des modules indiqués,
|
||||||
|
avec évaluations de chacun."""
|
||||||
|
d = {}
|
||||||
|
etud_idx = self.etud_index[etud.id]
|
||||||
|
for mi in modimpls:
|
||||||
|
mod_idx = self.modimpl_coefs_df.columns.get_loc(mi.id)
|
||||||
|
# moyennes indicatives (moyennes de moyennes d'UE)
|
||||||
|
moyennes_etuds = np.nan_to_num(
|
||||||
|
self.sem_cube[:, mod_idx, :].mean(axis=1),
|
||||||
|
copy=False,
|
||||||
|
)
|
||||||
|
d[mi.module.code] = {
|
||||||
|
"id": mi.id,
|
||||||
|
"titre": mi.module.titre,
|
||||||
|
"code_apogee": mi.module.code_apogee,
|
||||||
|
"moyenne": {
|
||||||
|
"value": jsnan(self.sem_cube[etud_idx, mod_idx].mean()),
|
||||||
|
"min": jsnan(moyennes_etuds.min()),
|
||||||
|
"max": jsnan(moyennes_etuds.max()),
|
||||||
|
"moy": jsnan(moyennes_etuds.mean()),
|
||||||
|
},
|
||||||
|
"evaluations": [
|
||||||
|
self.etud_eval_results(etud, e)
|
||||||
|
for e in mi.evaluations
|
||||||
|
if e.visibulletin
|
||||||
|
],
|
||||||
|
}
|
||||||
|
return d
|
||||||
|
|
||||||
|
def etud_eval_results(self, etud, e) -> dict:
|
||||||
|
"dict resultats d'un étudiant à une évaluation"
|
||||||
|
eval_notes = self.modimpls_evals_notes[e.moduleimpl_id][str(e.id)] # pd.Series
|
||||||
|
notes_ok = eval_notes.where(eval_notes > -1000).dropna()
|
||||||
|
d = {
|
||||||
|
"id": e.id,
|
||||||
|
"description": e.description,
|
||||||
|
"date": e.jour.isoformat(),
|
||||||
|
"heure_debut": e.heure_debut.strftime("%H:%M") if e.heure_debut else None,
|
||||||
|
"heure_fin": e.heure_fin.strftime("%H:%M") if e.heure_debut else None,
|
||||||
|
"coef": e.coefficient,
|
||||||
|
"poids": {p.ue.acronyme: p.poids for p in e.ue_poids},
|
||||||
|
"note": {
|
||||||
|
"value": jsnan(
|
||||||
|
self.modimpls_evals_notes[e.moduleimpl_id][str(e.id)][etud.id]
|
||||||
|
),
|
||||||
|
"min": jsnan(notes_ok.min()),
|
||||||
|
"max": jsnan(notes_ok.max()),
|
||||||
|
"moy": jsnan(notes_ok.mean()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return d
|
||||||
|
|
||||||
|
def bulletin_etud(self, etud, formsemestre) -> dict:
|
||||||
|
"""Le bulletin de l'étudiant dans ce semestre"""
|
||||||
|
d = {
|
||||||
|
"version": "0",
|
||||||
|
"type": "BUT",
|
||||||
|
"date": datetime.datetime.utcnow().isoformat() + "Z",
|
||||||
|
"etudiant": etud.to_dict_bul(),
|
||||||
|
"formation": {
|
||||||
|
"id": formsemestre.formation.id,
|
||||||
|
"acronyme": formsemestre.formation.acronyme,
|
||||||
|
"titre_officiel": formsemestre.formation.titre_officiel,
|
||||||
|
"titre": formsemestre.formation.titre,
|
||||||
|
},
|
||||||
|
"formsemestre_id": formsemestre.id,
|
||||||
|
"ressources": None, # XXX TODO
|
||||||
|
"saes": None, # XXX TODO
|
||||||
|
"ues": {ue.acronyme: self.etud_ue_results(etud, ue) for ue in self.ues},
|
||||||
|
"semestre": {
|
||||||
|
"notes": { # moyenne des moyennes générales du semestre
|
||||||
|
"value": jsnan("xxx"), # XXX TODO
|
||||||
|
"min": jsnan("0."),
|
||||||
|
"moy": jsnan("10.0"),
|
||||||
|
"max": jsnan("20.00"),
|
||||||
|
},
|
||||||
|
"rang": { # classement wrt moyenne général, indicatif
|
||||||
|
"value": None, # XXX TODO
|
||||||
|
"total": None,
|
||||||
|
},
|
||||||
|
"absences": { # XXX TODO
|
||||||
|
"injustifie": 1,
|
||||||
|
"total": 33,
|
||||||
|
},
|
||||||
|
"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,
|
||||||
|
"decision": None, # XXX TODO
|
||||||
|
"situation": "Décision jury: Validé. Diplôme obtenu.", # XXX TODO
|
||||||
|
"date_jury": "AAAA-MM-JJ", # XXX TODO
|
||||||
|
"groupes": [], # XXX TODO
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return d
|
@ -14,16 +14,15 @@ from app import models
|
|||||||
# sur test debug 116 etuds, 18 modules, on est autour de 250ms.
|
# sur test debug 116 etuds, 18 modules, on est autour de 250ms.
|
||||||
# On a testé trois approches, ci-dessous (et retenu la 1ere)
|
# On a testé trois approches, ci-dessous (et retenu la 1ere)
|
||||||
#
|
#
|
||||||
def df_load_modimpl_inscr(formsemestre_id: int) -> pd.DataFrame:
|
def df_load_modimpl_inscr(formsemestre) -> pd.DataFrame:
|
||||||
"""Charge la matrice des inscriptions aux modules du semestre
|
"""Charge la matrice des inscriptions aux modules du semestre
|
||||||
rows: etudid
|
rows: etudid
|
||||||
columns: moduleimpl_id (en chaîne)
|
columns: moduleimpl_id (en chaîne)
|
||||||
value: bool (0/1 inscrit ou pas)
|
value: bool (0/1 inscrit ou pas)
|
||||||
"""
|
"""
|
||||||
# méthode la moins lente: une requete par module, merge les dataframes
|
# méthode la moins lente: une requete par module, merge les dataframes
|
||||||
sem = models.FormSemestre.query.get(formsemestre_id)
|
moduleimpl_ids = [m.id for m in formsemestre.modimpls]
|
||||||
moduleimpl_ids = [m.id for m in sem.modimpls]
|
etudids = [i.etudid for i in formsemestre.inscriptions]
|
||||||
etudids = [i.etudid for i in sem.inscriptions]
|
|
||||||
df = pd.DataFrame(index=etudids, dtype=int)
|
df = pd.DataFrame(index=etudids, dtype=int)
|
||||||
for moduleimpl_id in moduleimpl_ids:
|
for moduleimpl_id in moduleimpl_ids:
|
||||||
ins_df = pd.read_sql_query(
|
ins_df = pd.read_sql_query(
|
||||||
@ -46,27 +45,25 @@ def df_load_modimpl_inscr(formsemestre_id: int) -> pd.DataFrame:
|
|||||||
# timeit.timeit('x = df_load_module_inscr_v0(696)', number=100, globals=globals())
|
# timeit.timeit('x = df_load_module_inscr_v0(696)', number=100, globals=globals())
|
||||||
|
|
||||||
|
|
||||||
def df_load_modimpl_inscr_v0(formsemestre_id: int):
|
def df_load_modimpl_inscr_v0(formsemestre):
|
||||||
# methode 0, pur SQL Alchemy, 1.5 à 2 fois plus lente
|
# methode 0, pur SQL Alchemy, 1.5 à 2 fois plus lente
|
||||||
sem = models.FormSemestre.query.get(formsemestre_id)
|
moduleimpl_ids = [m.id for m in formsemestre.modimpls]
|
||||||
moduleimpl_ids = [m.id for m in sem.modimpls]
|
etudids = [i.etudid for i in formsemestre.inscriptions]
|
||||||
etudids = [i.etudid for i in sem.inscriptions]
|
|
||||||
df = pd.DataFrame(False, columns=moduleimpl_ids, index=etudids, dtype=bool)
|
df = pd.DataFrame(False, columns=moduleimpl_ids, index=etudids, dtype=bool)
|
||||||
for modimpl in sem.modimpls:
|
for modimpl in formsemestre.modimpls:
|
||||||
ins_mod = df[modimpl.id]
|
ins_mod = df[modimpl.id]
|
||||||
for inscr in modimpl.inscriptions:
|
for inscr in modimpl.inscriptions:
|
||||||
ins_mod[inscr.etudid] = True
|
ins_mod[inscr.etudid] = True
|
||||||
return df # x100 30.7s 46s 32s
|
return df # x100 30.7s 46s 32s
|
||||||
|
|
||||||
|
|
||||||
def df_load_modimpl_inscr_v2(formsemestre_id):
|
def df_load_modimpl_inscr_v2(formsemestre):
|
||||||
sem = models.FormSemestre.query.get(formsemestre_id)
|
moduleimpl_ids = [m.id for m in formsemestre.modimpls]
|
||||||
moduleimpl_ids = [m.id for m in sem.modimpls]
|
etudids = [i.etudid for i in formsemestre.inscriptions]
|
||||||
etudids = [i.etudid for i in sem.inscriptions]
|
|
||||||
df = pd.DataFrame(False, columns=moduleimpl_ids, index=etudids, dtype=bool)
|
df = pd.DataFrame(False, columns=moduleimpl_ids, index=etudids, dtype=bool)
|
||||||
cursor = db.engine.execute(
|
cursor = db.engine.execute(
|
||||||
"select moduleimpl_id, etudid from notes_moduleimpl_inscription i, notes_moduleimpl m where i.moduleimpl_id = m.id and m.formsemestre_id = %(formsemestre_id)s",
|
"select moduleimpl_id, etudid from notes_moduleimpl_inscription i, notes_moduleimpl m where i.moduleimpl_id = m.id and m.formsemestre_id = %(formsemestre_id)s",
|
||||||
{"formsemestre_id": formsemestre_id},
|
{"formsemestre_id": formsemestre.id},
|
||||||
)
|
)
|
||||||
for moduleimpl_id, etudid in cursor:
|
for moduleimpl_id, etudid in cursor:
|
||||||
df[moduleimpl_id][etudid] = True
|
df[moduleimpl_id][etudid] = True
|
||||||
|
@ -75,18 +75,22 @@ def df_load_module_coefs(formation_id: int, semestre_idx: int = None) -> pd.Data
|
|||||||
return module_coefs_df, ues, modules
|
return module_coefs_df, ues, modules
|
||||||
|
|
||||||
|
|
||||||
def df_load_modimpl_coefs(formsemestre: models.FormSemestre) -> pd.DataFrame:
|
def df_load_modimpl_coefs(
|
||||||
|
formsemestre: models.FormSemestre, ues=None, modimpls=None
|
||||||
|
) -> pd.DataFrame:
|
||||||
"""Charge les coefs des modules du formsemestre indiqué.
|
"""Charge les coefs des modules du formsemestre indiqué.
|
||||||
|
|
||||||
Comme df_load_module_coefs mais prend seulement les UE
|
Comme df_load_module_coefs mais prend seulement les UE
|
||||||
et modules du formsemestre.
|
et modules du formsemestre.
|
||||||
|
Si ues et modimpls sont None, prend tous ceux du formsemestre.
|
||||||
Résultat: (module_coefs_df, ues, modules)
|
Résultat: (module_coefs_df, ues, modules)
|
||||||
DataFrame rows = UEs, columns = modimpl, value = coef.
|
DataFrame rows = UEs, columns = modimpl, value = coef.
|
||||||
"""
|
"""
|
||||||
ues = formsemestre.query_ues().all()
|
if ues is None:
|
||||||
|
ues = formsemestre.query_ues().all()
|
||||||
ue_ids = [x.id for x in ues]
|
ue_ids = [x.id for x in ues]
|
||||||
modimpls = formsemestre.modimpls.all()
|
if modimpls is None:
|
||||||
|
modimpls = formsemestre.modimpls.all()
|
||||||
modimpl_ids = [x.id for x in modimpls]
|
modimpl_ids = [x.id for x in modimpls]
|
||||||
mod2impl = {m.module.id: m.id for m in modimpls}
|
mod2impl = {m.module.id: m.id for m in modimpls}
|
||||||
modimpl_coefs_df = pd.DataFrame(columns=modimpl_ids, index=ue_ids, dtype=float)
|
modimpl_coefs_df = pd.DataFrame(columns=modimpl_ids, index=ue_ids, dtype=float)
|
||||||
@ -115,13 +119,15 @@ def notes_sem_assemble_cube(modimpls_notes: list[pd.DataFrame]) -> np.ndarray:
|
|||||||
return modimpls_notes.swapaxes(0, 1)
|
return modimpls_notes.swapaxes(0, 1)
|
||||||
|
|
||||||
|
|
||||||
def notes_sem_load_cube(formsemestre_id):
|
def notes_sem_load_cube(formsemestre):
|
||||||
"""Calcule le cube des notes du semestre
|
"""Calcule le cube des notes du semestre
|
||||||
(charge toutes les notes, calcule les moyenne des modules
|
(charge toutes les notes, calcule les moyenne des modules
|
||||||
et assemble le cube)
|
et assemble le cube)
|
||||||
Resultat: ndarray (etuds x modimpls x UEs)
|
Resultat: ndarray (etuds x modimpls x UEs)
|
||||||
"""
|
"""
|
||||||
formsemestre = FormSemestre.query.get(formsemestre_id)
|
modimpls_evals_poids = {} # modimpl.id : evals_poids
|
||||||
|
modimpls_evals_notes = {} # modimpl.id : evals_notes
|
||||||
|
modimpls_evaluations = {} # modimpl.id : liste des évaluations
|
||||||
modimpls_notes = []
|
modimpls_notes = []
|
||||||
for modimpl in formsemestre.modimpls:
|
for modimpl in formsemestre.modimpls:
|
||||||
evals_notes, evaluations = moy_mod.df_load_modimpl_notes(modimpl.id)
|
evals_notes, evaluations = moy_mod.df_load_modimpl_notes(modimpl.id)
|
||||||
@ -129,8 +135,16 @@ def notes_sem_load_cube(formsemestre_id):
|
|||||||
etuds_moy_module = moy_mod.compute_module_moy(
|
etuds_moy_module = moy_mod.compute_module_moy(
|
||||||
evals_notes, evals_poids, evaluations
|
evals_notes, evals_poids, evaluations
|
||||||
)
|
)
|
||||||
|
modimpls_evals_poids[modimpl.id] = evals_poids
|
||||||
|
modimpls_evals_notes[modimpl.id] = evals_notes
|
||||||
|
modimpls_evaluations[modimpl.id] = evaluations
|
||||||
modimpls_notes.append(etuds_moy_module)
|
modimpls_notes.append(etuds_moy_module)
|
||||||
return notes_sem_assemble_cube(modimpls_notes)
|
return (
|
||||||
|
notes_sem_assemble_cube(modimpls_notes),
|
||||||
|
modimpls_evals_poids,
|
||||||
|
modimpls_evals_notes,
|
||||||
|
modimpls_evaluations,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def compute_ue_moys(
|
def compute_ue_moys(
|
||||||
|
@ -8,6 +8,7 @@ from app import db
|
|||||||
from app.models import APO_CODE_STR_LEN
|
from app.models import APO_CODE_STR_LEN
|
||||||
from app.models import SHORT_STR_LEN
|
from app.models import SHORT_STR_LEN
|
||||||
from app.models import CODE_STR_LEN
|
from app.models import CODE_STR_LEN
|
||||||
|
from app.scodoc import sco_photos
|
||||||
|
|
||||||
|
|
||||||
class Identite(db.Model):
|
class Identite(db.Model):
|
||||||
@ -44,6 +45,7 @@ class Identite(db.Model):
|
|||||||
# ne pas utiliser après migrate_scodoc7_dept_archives
|
# ne pas utiliser après migrate_scodoc7_dept_archives
|
||||||
scodoc7_id = db.Column(db.Text(), nullable=True)
|
scodoc7_id = db.Column(db.Text(), nullable=True)
|
||||||
#
|
#
|
||||||
|
adresses = db.relationship("Adresse", lazy="dynamic", backref="etud")
|
||||||
billets = db.relationship("BilletAbsence", backref="etudiant", lazy="dynamic")
|
billets = db.relationship("BilletAbsence", backref="etudiant", lazy="dynamic")
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
@ -64,6 +66,27 @@ class Identite(db.Model):
|
|||||||
else:
|
else:
|
||||||
return self.nom
|
return self.nom
|
||||||
|
|
||||||
|
def to_dict_bul(self):
|
||||||
|
"""Infos exportées dans les bulletins"""
|
||||||
|
return {
|
||||||
|
"civilite": self.civilite,
|
||||||
|
"code_ine": self.code_nip,
|
||||||
|
"code_nip": self.code_ine,
|
||||||
|
"date_naissance": self.date_naissance.isoformat()
|
||||||
|
if self.date_naissance
|
||||||
|
else None,
|
||||||
|
"email": self.adresses[0].email or None
|
||||||
|
if self.adresses.count() > 0
|
||||||
|
else None,
|
||||||
|
"emailperso": self.adresses[0].emailperso or None
|
||||||
|
if self.adresses.count() > 0
|
||||||
|
else None,
|
||||||
|
"etudid": self.id,
|
||||||
|
"nom": self.nom_disp(),
|
||||||
|
"photo_url": sco_photos.get_etud_photo_url(self.id),
|
||||||
|
"prenom": self.prenom,
|
||||||
|
}
|
||||||
|
|
||||||
def inscription_courante(self):
|
def inscription_courante(self):
|
||||||
"""La première inscription à un formsemestre _actuellement_ en cours.
|
"""La première inscription à un formsemestre _actuellement_ en cours.
|
||||||
None s'il n'y en a pas (ou plus, ou pas encore).
|
None s'il n'y en a pas (ou plus, ou pas encore).
|
||||||
|
@ -46,7 +46,7 @@ class Evaluation(db.Model):
|
|||||||
ues = db.relationship("UniteEns", secondary="evaluation_ue_poids", viewonly=True)
|
ues = db.relationship("UniteEns", secondary="evaluation_ue_poids", viewonly=True)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<Evaluation {self.id} {self.jour.isoformat() if self.jour else ''}{self.description[:16] if self.description else ''}"
|
return f"""<Evaluation {self.id} {self.jour.isoformat() if self.jour else ''} "{self.description[:16] if self.description else ''}">"""
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
e = dict(self.__dict__)
|
e = dict(self.__dict__)
|
||||||
|
@ -58,6 +58,10 @@ class Formation(db.Model):
|
|||||||
"""get l'instance de TypeParcours de cette formation"""
|
"""get l'instance de TypeParcours de cette formation"""
|
||||||
return sco_codes_parcours.get_parcours_from_code(self.type_parcours)
|
return sco_codes_parcours.get_parcours_from_code(self.type_parcours)
|
||||||
|
|
||||||
|
def is_apc(self):
|
||||||
|
"True si formation APC avec SAE (BUT)"
|
||||||
|
return self.get_parcours().APC_SAE
|
||||||
|
|
||||||
def get_module_coefs(self, semestre_idx: int = None):
|
def get_module_coefs(self, semestre_idx: int = None):
|
||||||
"""Les coefs des modules vers les UE (accès via cache)"""
|
"""Les coefs des modules vers les UE (accès via cache)"""
|
||||||
from app.comp import moy_ue
|
from app.comp import moy_ue
|
||||||
|
@ -178,6 +178,10 @@ class FormSemestre(db.Model):
|
|||||||
else:
|
else:
|
||||||
return ", ".join([u.get_nomcomplet() for u in self.responsables])
|
return ", ".join([u.get_nomcomplet() for u in self.responsables])
|
||||||
|
|
||||||
|
def annee_scolaire_str(self):
|
||||||
|
"2021 - 2022"
|
||||||
|
return scu.annee_scolaire_repr(self.date_debut.year, self.date_debut.month)
|
||||||
|
|
||||||
def session_id(self) -> str:
|
def session_id(self) -> str:
|
||||||
"""identifiant externe de semestre de formation
|
"""identifiant externe de semestre de formation
|
||||||
Exemple: RT-DUT-FI-S1-ANNEE
|
Exemple: RT-DUT-FI-S1-ANNEE
|
||||||
|
@ -155,6 +155,16 @@ class EvaluationCache(ScoDocCache):
|
|||||||
cls.delete_many(evaluation_ids)
|
cls.delete_many(evaluation_ids)
|
||||||
|
|
||||||
|
|
||||||
|
class ResultatsSemestreBUTCache(ScoDocCache):
|
||||||
|
"""Cache pour les résultats ResultatsSemestreBUT.
|
||||||
|
Clé: formsemestre_id
|
||||||
|
Valeur: { un paquet de dataframes }
|
||||||
|
"""
|
||||||
|
|
||||||
|
prefix = "RBUT"
|
||||||
|
timeout = 1 * 60 # ttl 1 minutes (en phase de mise au point)
|
||||||
|
|
||||||
|
|
||||||
class AbsSemEtudCache(ScoDocCache):
|
class AbsSemEtudCache(ScoDocCache):
|
||||||
"""Cache pour les comptes d'absences d'un étudiant dans un semestre.
|
"""Cache pour les comptes d'absences d'un étudiant dans un semestre.
|
||||||
Ce cache étant indépendant des semestres, le compte peut être faux lorsqu'on
|
Ce cache étant indépendant des semestres, le compte peut être faux lorsqu'on
|
||||||
@ -289,6 +299,7 @@ def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=Fa
|
|||||||
SemInscriptionsCache.delete_many(formsemestre_ids)
|
SemInscriptionsCache.delete_many(formsemestre_ids)
|
||||||
|
|
||||||
SemBulletinsPDFCache.invalidate_sems(formsemestre_ids)
|
SemBulletinsPDFCache.invalidate_sems(formsemestre_ids)
|
||||||
|
ResultatsSemestreBUTCache.delete_many(formsemestre_ids)
|
||||||
|
|
||||||
|
|
||||||
class DefferedSemCacheManager:
|
class DefferedSemCacheManager:
|
||||||
|
@ -43,7 +43,7 @@ Les images sont servies par ScoDoc, via la méthode getphotofile?etudid=xxx
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from flask.helpers import make_response
|
from flask.helpers import make_response, url_for
|
||||||
from app.scodoc.sco_exceptions import ScoGenError
|
from app.scodoc.sco_exceptions import ScoGenError
|
||||||
import datetime
|
import datetime
|
||||||
import glob
|
import glob
|
||||||
@ -91,14 +91,17 @@ def photo_portal_url(etud):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_etud_photo_url(etudid, size="small"):
|
||||||
|
return url_for(
|
||||||
|
"scolar.get_photo_image", scodoc_dept=g.scodoc_dept, etudid=etudid, size=size
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def etud_photo_url(etud, size="small", fast=False):
|
def etud_photo_url(etud, size="small", fast=False):
|
||||||
"""url to the image of the student, in "small" size or "orig" size.
|
"""url to the image of the student, in "small" size or "orig" size.
|
||||||
If ScoDoc doesn't have an image and a portal is configured, link to it.
|
If ScoDoc doesn't have an image and a portal is configured, link to it.
|
||||||
"""
|
"""
|
||||||
photo_url = scu.ScoURL() + "/get_photo_image?etudid=%s&size=%s" % (
|
photo_url = get_etud_photo_url(etud["etudid"], size=size)
|
||||||
etud["etudid"],
|
|
||||||
size,
|
|
||||||
)
|
|
||||||
if fast:
|
if fast:
|
||||||
return photo_url
|
return photo_url
|
||||||
path = photo_pathname(etud, size=size)
|
path = photo_pathname(etud, size=size)
|
||||||
|
@ -202,6 +202,13 @@ def isnumber(x):
|
|||||||
return isinstance(x, numbers.Number)
|
return isinstance(x, numbers.Number)
|
||||||
|
|
||||||
|
|
||||||
|
def jsnan(x):
|
||||||
|
"if x is NaN, returns None"
|
||||||
|
if isinstance(x, numbers.Number) and np.isnan(x):
|
||||||
|
return None
|
||||||
|
return x
|
||||||
|
|
||||||
|
|
||||||
def join_words(*words):
|
def join_words(*words):
|
||||||
words = [str(w).strip() for w in words if w is not None]
|
words = [str(w).strip() for w in words if w is not None]
|
||||||
return " ".join([w for w in words if w])
|
return " ".join([w for w in words if w])
|
||||||
|
@ -38,10 +38,11 @@ from operator import itemgetter
|
|||||||
from xml.etree import ElementTree
|
from xml.etree import ElementTree
|
||||||
|
|
||||||
import flask
|
import flask
|
||||||
from flask import url_for
|
from flask import url_for, jsonify
|
||||||
from flask import current_app, g, request
|
from flask import current_app, g, request
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from werkzeug.utils import redirect
|
from werkzeug.utils import redirect
|
||||||
|
from app.models.formsemestre import FormSemestre
|
||||||
|
|
||||||
from config import Config
|
from config import Config
|
||||||
|
|
||||||
@ -49,7 +50,7 @@ from app import api
|
|||||||
from app import db
|
from app import db
|
||||||
from app import models
|
from app import models
|
||||||
from app.auth.models import User
|
from app.auth.models import User
|
||||||
|
from app.but import bulletin_but
|
||||||
from app.decorators import (
|
from app.decorators import (
|
||||||
scodoc,
|
scodoc,
|
||||||
scodoc7func,
|
scodoc7func,
|
||||||
@ -285,6 +286,13 @@ def formsemestre_bulletinetud(
|
|||||||
raise ScoValueError("Paramètre manquant: spécifier code_nip ou etudid")
|
raise ScoValueError("Paramètre manquant: spécifier code_nip ou etudid")
|
||||||
if not formsemestre_id:
|
if not formsemestre_id:
|
||||||
raise ScoValueError("Paramètre manquant: formsemestre_id est requis")
|
raise ScoValueError("Paramètre manquant: formsemestre_id est requis")
|
||||||
|
|
||||||
|
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||||
|
if formsemestre.formation.is_apc():
|
||||||
|
etud = models.Identite.query.get_or_404(etudid)
|
||||||
|
r = bulletin_but.ResultatsSemestreBUT(formsemestre)
|
||||||
|
return jsonify(r.bulletin_etud(etud, formsemestre))
|
||||||
|
|
||||||
return sco_bulletins.formsemestre_bulletinetud(
|
return sco_bulletins.formsemestre_bulletinetud(
|
||||||
etudid=etudid,
|
etudid=etudid,
|
||||||
formsemestre_id=formsemestre_id,
|
formsemestre_id=formsemestre_id,
|
||||||
|
@ -53,7 +53,7 @@ def test_ue_moy(test_client):
|
|||||||
# Les moduleimpls
|
# Les moduleimpls
|
||||||
modimpls = [evaluation1.moduleimpl, evaluation2.moduleimpl]
|
modimpls = [evaluation1.moduleimpl, evaluation2.moduleimpl]
|
||||||
# Check inscriptions modules
|
# Check inscriptions modules
|
||||||
modimpl_inscr_df = inscr_mod.df_load_modimpl_inscr(formsemestre_id)
|
modimpl_inscr_df = inscr_mod.df_load_modimpl_inscr(formsemestre)
|
||||||
assert (modimpl_inscr_df.values == np.array([[1, 1]])).all()
|
assert (modimpl_inscr_df.values == np.array([[1, 1]])).all()
|
||||||
# Coefs des modules vers les UE:
|
# Coefs des modules vers les UE:
|
||||||
modimpl_coefs_df, ues, modimpls = moy_ue.df_load_modimpl_coefs(formsemestre)
|
modimpl_coefs_df, ues, modimpls = moy_ue.df_load_modimpl_coefs(formsemestre)
|
||||||
@ -69,7 +69,7 @@ def test_ue_moy(test_client):
|
|||||||
_ = sco_saisie_notes.notes_add(G.default_user, evaluation1.id, [(etudid, n1)])
|
_ = sco_saisie_notes.notes_add(G.default_user, evaluation1.id, [(etudid, n1)])
|
||||||
_ = sco_saisie_notes.notes_add(G.default_user, evaluation2.id, [(etudid, n2)])
|
_ = sco_saisie_notes.notes_add(G.default_user, evaluation2.id, [(etudid, n2)])
|
||||||
# Recalcul des moyennes
|
# Recalcul des moyennes
|
||||||
sem_cube = moy_ue.notes_sem_load_cube(formsemestre_id)
|
sem_cube, _, _, _ = moy_ue.notes_sem_load_cube(formsemestre)
|
||||||
etuds = formsemestre.etuds.all()
|
etuds = formsemestre.etuds.all()
|
||||||
etud_moy_ue = moy_ue.compute_ue_moys(
|
etud_moy_ue = moy_ue.compute_ue_moys(
|
||||||
sem_cube, etuds, modimpls, ues, modimpl_inscr_df, modimpl_coefs_df
|
sem_cube, etuds, modimpls, ues, modimpl_inscr_df, modimpl_coefs_df
|
||||||
@ -103,7 +103,7 @@ def test_ue_moy(test_client):
|
|||||||
).first()
|
).first()
|
||||||
db.session.delete(inscr)
|
db.session.delete(inscr)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
modimpl_inscr_df = inscr_mod.df_load_modimpl_inscr(formsemestre_id)
|
modimpl_inscr_df = inscr_mod.df_load_modimpl_inscr(formsemestre)
|
||||||
assert (modimpl_inscr_df.values == np.array([[1, 0]])).all()
|
assert (modimpl_inscr_df.values == np.array([[1, 0]])).all()
|
||||||
n1, n2 = 5.0, NOTES_NEUTRALISE
|
n1, n2 = 5.0, NOTES_NEUTRALISE
|
||||||
# On ne doit pas pouvoir saisir de note sans être inscrit:
|
# On ne doit pas pouvoir saisir de note sans être inscrit:
|
||||||
@ -114,7 +114,7 @@ def test_ue_moy(test_client):
|
|||||||
exception_raised = True
|
exception_raised = True
|
||||||
assert exception_raised
|
assert exception_raised
|
||||||
# Recalcule les notes:
|
# Recalcule les notes:
|
||||||
sem_cube = moy_ue.notes_sem_load_cube(formsemestre_id)
|
sem_cube, _, _, _ = moy_ue.notes_sem_load_cube(formsemestre)
|
||||||
etuds = formsemestre.etuds.all()
|
etuds = formsemestre.etuds.all()
|
||||||
etud_moy_ue = moy_ue.compute_ue_moys(
|
etud_moy_ue = moy_ue.compute_ue_moys(
|
||||||
sem_cube, etuds, modimpls, ues, modimpl_inscr_df, modimpl_coefs_df
|
sem_cube, etuds, modimpls, ues, modimpl_inscr_df, modimpl_coefs_df
|
||||||
|
Loading…
Reference in New Issue
Block a user