Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into dev92

This commit is contained in:
Emmanuel Viennet 2022-02-28 20:23:25 +01:00
commit 23ea53294d
9 changed files with 134 additions and 84 deletions

View File

@ -40,13 +40,11 @@ def compute_mat_moys_classic(
modimpl_mask = np.array( modimpl_mask = np.array(
[m.module.matiere.id == matiere_id for m in formsemestre.modimpls_sorted] [m.module.matiere.id == matiere_id for m in formsemestre.modimpls_sorted]
) )
etud_moy_gen, _, _ = moy_ue.compute_ue_moys_classic( etud_moy_mat = moy_ue.compute_mat_moys_classic(
formsemestre,
sem_matrix=sem_matrix, sem_matrix=sem_matrix,
ues=ues,
modimpl_inscr_df=modimpl_inscr_df, modimpl_inscr_df=modimpl_inscr_df,
modimpl_coefs=modimpl_coefs, modimpl_coefs=modimpl_coefs,
modimpl_mask=modimpl_mask, modimpl_mask=modimpl_mask,
) )
matiere_moy[matiere_id] = etud_moy_gen matiere_moy[matiere_id] = etud_moy_mat
return matiere_moy return matiere_moy

View File

@ -294,7 +294,8 @@ def compute_ue_moys_classic(
modimpl_coefs: np.array, modimpl_coefs: np.array,
modimpl_mask: np.array, modimpl_mask: np.array,
) -> tuple[pd.Series, pd.DataFrame, pd.DataFrame]: ) -> tuple[pd.Series, pd.DataFrame, pd.DataFrame]:
"""Calcul de la moyenne d'UE en mode classique. """Calcul de la moyenne d'UE et de la moy. générale en mode classique (DUT, LMD, ...).
La moyenne d'UE est un nombre (note/20), ou NI ou NA ou ERR La moyenne d'UE est un nombre (note/20), ou NI ou NA ou ERR
NI non inscrit à (au moins un) module de cette UE NI non inscrit à (au moins un) module de cette UE
NA pas de notes disponibles NA pas de notes disponibles
@ -363,7 +364,7 @@ def compute_ue_moys_classic(
modimpl_coefs_etuds_no_nan_stacked = np.stack( modimpl_coefs_etuds_no_nan_stacked = np.stack(
[modimpl_coefs_etuds_no_nan.T] * nb_ues [modimpl_coefs_etuds_no_nan.T] * nb_ues
) )
# nb_ue x nb_etuds x nb_mods : coefs prenant en compte NaN et inscriptions # nb_ue x nb_etuds x nb_mods : coefs prenant en compte NaN et inscriptions:
coefs = (modimpl_coefs_etuds_no_nan_stacked * ue_modules).swapaxes(1, 2) coefs = (modimpl_coefs_etuds_no_nan_stacked * ue_modules).swapaxes(1, 2)
if coefs.dtype == np.object: # arrive sur des tableaux vides if coefs.dtype == np.object: # arrive sur des tableaux vides
coefs = coefs.astype(np.float) coefs = coefs.astype(np.float)
@ -408,6 +409,68 @@ def compute_ue_moys_classic(
return etud_moy_gen_s, etud_moy_ue_df, etud_coef_ue_df return etud_moy_gen_s, etud_moy_ue_df, etud_coef_ue_df
def compute_mat_moys_classic(
sem_matrix: np.array,
modimpl_inscr_df: pd.DataFrame,
modimpl_coefs: np.array,
modimpl_mask: np.array,
) -> pd.Series:
"""Calcul de la moyenne sur un sous-enemble de modules en formation CLASSIQUE
La moyenne est un nombre (note/20 ou NaN.
Le masque modimpl_mask est un tableau de booléens (un par modimpl) qui
permet de sélectionner un sous-ensemble de modules (ceux de la matière d'intérêt).
sem_matrix: notes moyennes aux modules (tous les étuds x tous les modimpls)
ndarray (etuds x modimpls)
(floats avec des NaN)
etuds : listes des étudiants (dim. 0 de la matrice)
modimpl_inscr_df: matrice d'inscription du semestre (etud x modimpl)
modimpl_coefs: vecteur des coefficients de modules
modimpl_mask: masque des modimpls à prendre en compte
Résultat:
- moyennes: pd.Series, index etudid
"""
if (not len(modimpl_mask)) or (
sem_matrix.shape[0] == 0
): # aucun module ou aucun étudiant
# etud_moy_gen_s, etud_moy_ue_df, etud_coef_ue_df
return pd.Series(
[0.0] * len(modimpl_inscr_df.index), index=modimpl_inscr_df.index
)
# Restreint aux modules sélectionnés:
sem_matrix = sem_matrix[:, modimpl_mask]
modimpl_inscr = modimpl_inscr_df.values[:, modimpl_mask]
modimpl_coefs = modimpl_coefs[modimpl_mask]
nb_etuds, nb_modules = sem_matrix.shape
assert len(modimpl_coefs) == nb_modules
# Enlève les NaN du numérateur:
sem_matrix_no_nan = np.nan_to_num(sem_matrix, nan=0.0)
# Ne prend pas en compte les notes des étudiants non inscrits au module:
# Annule les notes:
sem_matrix_inscrits = np.where(modimpl_inscr, sem_matrix_no_nan, 0.0)
# Annule les coefs des modules où l'étudiant n'est pas inscrit:
modimpl_coefs_etuds = np.where(
modimpl_inscr, np.stack([modimpl_coefs.T] * nb_etuds), 0.0
)
# Annule les coefs des modules NaN (nb_etuds x nb_mods)
modimpl_coefs_etuds_no_nan = np.where(
np.isnan(sem_matrix), 0.0, modimpl_coefs_etuds
)
if modimpl_coefs_etuds_no_nan.dtype == np.object: # arrive sur des tableaux vides
modimpl_coefs_etuds_no_nan = modimpl_coefs_etuds_no_nan.astype(np.float)
etud_moy_mat = (modimpl_coefs_etuds_no_nan * sem_matrix_inscrits).sum(
axis=1
) / modimpl_coefs_etuds_no_nan.sum(axis=1)
return pd.Series(etud_moy_mat, index=modimpl_inscr_df.index)
def compute_malus( def compute_malus(
formsemestre: FormSemestre, formsemestre: FormSemestre,
sem_modimpl_moys: np.array, sem_modimpl_moys: np.array,

View File

@ -8,11 +8,13 @@
""" """
from flask import g from flask import g
from app import db
from app.comp.jury import ValidationsSemestre from app.comp.jury import ValidationsSemestre
from app.comp.res_common import ResultatsSemestre from app.comp.res_common import ResultatsSemestre
from app.comp.res_classic import ResultatsSemestreClassic from app.comp.res_classic import ResultatsSemestreClassic
from app.comp.res_but import ResultatsSemestreBUT from app.comp.res_but import ResultatsSemestreBUT
from app.models.formsemestre import FormSemestre from app.models.formsemestre import FormSemestre
from app.scodoc import sco_cache
def load_formsemestre_results(formsemestre: FormSemestre) -> ResultatsSemestre: def load_formsemestre_results(formsemestre: FormSemestre) -> ResultatsSemestre:
@ -23,6 +25,13 @@ def load_formsemestre_results(formsemestre: FormSemestre) -> ResultatsSemestre:
Search in local cache (g.formsemestre_result_cache) Search in local cache (g.formsemestre_result_cache)
If not in cache, build it and cache it. If not in cache, build it and cache it.
""" """
is_apc = formsemestre.formation.is_apc()
if is_apc and formsemestre.semestre_id == -1:
formsemestre.semestre_id = 1
db.session.add(formsemestre)
db.session.commit()
sco_cache.invalidate_formsemestre(formsemestre.id)
# --- Try local cache (within the same request context) # --- Try local cache (within the same request context)
if not hasattr(g, "formsemestre_results_cache"): if not hasattr(g, "formsemestre_results_cache"):
g.formsemestre_results_cache = {} g.formsemestre_results_cache = {}
@ -30,11 +39,7 @@ def load_formsemestre_results(formsemestre: FormSemestre) -> ResultatsSemestre:
if formsemestre.id in g.formsemestre_results_cache: if formsemestre.id in g.formsemestre_results_cache:
return g.formsemestre_results_cache[formsemestre.id] return g.formsemestre_results_cache[formsemestre.id]
klass = ( klass = ResultatsSemestreBUT if is_apc else ResultatsSemestreClassic
ResultatsSemestreBUT
if formsemestre.formation.is_apc()
else ResultatsSemestreClassic
)
g.formsemestre_results_cache[formsemestre.id] = klass(formsemestre) g.formsemestre_results_cache[formsemestre.id] = klass(formsemestre)
return g.formsemestre_results_cache[formsemestre.id] return g.formsemestre_results_cache[formsemestre.id]

View File

@ -33,17 +33,12 @@
""" """
# API ScoDoc8 pour les caches: # API pour les caches:
# sco_cache.NotesTableCache.get( formsemestre_id) # sco_cache.MyCache.get( formsemestre_id)
# => sco_cache.NotesTableCache.get(formsemestre_id) # => sco_cache.MyCache.get(formsemestre_id)
# #
# sco_core.inval_cache(formsemestre_id=None, pdfonly=False, formsemestre_id_list=None) # sco_cache.MyCache.delete(formsemestre_id)
# => deprecated, NotesTableCache.invalidate_formsemestre(formsemestre_id=None, pdfonly=False) # sco_cache.MyCache.delete_many(formsemestre_id_list)
#
#
# Nouvelles fonctions:
# sco_cache.NotesTableCache.delete(formsemestre_id)
# sco_cache.NotesTableCache.delete_many(formsemestre_id_list)
# #
# Bulletins PDF: # Bulletins PDF:
# sco_cache.SemBulletinsPDFCache.get(formsemestre_id, version) # sco_cache.SemBulletinsPDFCache.get(formsemestre_id, version)
@ -203,49 +198,6 @@ class SemInscriptionsCache(ScoDocCache):
duration = 12 * 60 * 60 # ttl 12h duration = 12 * 60 * 60 # ttl 12h
class NotesTableCache(ScoDocCache):
"""Cache pour les NotesTable
Clé: formsemestre_id
Valeur: NotesTable instance
"""
prefix = "NT"
@classmethod
def get(cls, formsemestre_id, compute=True):
"""Returns NotesTable for this formsemestre
Search in local cache (g.nt_cache) or global app cache (eg REDIS)
If not in cache:
If compute is True, build it and cache it
Else return None
"""
# try local cache (same request)
if not hasattr(g, "nt_cache"):
g.nt_cache = {}
else:
if formsemestre_id in g.nt_cache:
return g.nt_cache[formsemestre_id]
# try REDIS
key = cls._get_key(formsemestre_id)
nt = CACHE.get(key)
if nt:
g.nt_cache[formsemestre_id] = nt # cache locally (same request)
return nt
if not compute:
return None
# Recompute requested table:
from app.scodoc import notes_table
t0 = time.time()
nt = notes_table.NotesTable(formsemestre_id)
t1 = time.time()
_ = cls.set(formsemestre_id, nt) # cache in REDIS
t2 = time.time()
log(f"cached formsemestre_id={formsemestre_id} ({(t1-t0):g}s +{(t2-t1):g}s)")
g.nt_cache[formsemestre_id] = nt
return nt
def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=False) def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=False)
formsemestre_id=None, pdfonly=False formsemestre_id=None, pdfonly=False
): ):
@ -278,22 +230,24 @@ def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=Fa
if not pdfonly: if not pdfonly:
# Delete cached notes and evaluations # Delete cached notes and evaluations
NotesTableCache.delete_many(formsemestre_ids)
if formsemestre_id: if formsemestre_id:
for fid in formsemestre_ids: for fid in formsemestre_ids:
EvaluationCache.invalidate_sem(fid) EvaluationCache.invalidate_sem(fid)
if hasattr(g, "nt_cache") and fid in g.nt_cache: if (
del g.nt_cache[fid] hasattr(g, "formsemestre_results_cache")
and fid in g.formsemestre_results_cache
):
del g.formsemestre_results_cache[fid]
else: else:
# optimization when we invalidate all evaluations: # optimization when we invalidate all evaluations:
EvaluationCache.invalidate_all_sems() EvaluationCache.invalidate_all_sems()
if hasattr(g, "nt_cache"): if hasattr(g, "formsemestre_results_cache"):
del g.nt_cache del g.formsemestre_results_cache
SemInscriptionsCache.delete_many(formsemestre_ids) SemInscriptionsCache.delete_many(formsemestre_ids)
SemBulletinsPDFCache.invalidate_sems(formsemestre_ids)
ResultatsSemestreCache.delete_many(formsemestre_ids) ResultatsSemestreCache.delete_many(formsemestre_ids)
ValidationsSemestreCache.delete_many(formsemestre_ids) ValidationsSemestreCache.delete_many(formsemestre_ids)
SemBulletinsPDFCache.invalidate_sems(formsemestre_ids)
class DefferedSemCacheManager: class DefferedSemCacheManager:

View File

@ -51,6 +51,7 @@ import fcntl
import subprocess import subprocess
import requests import requests
from flask import flash
from flask_login import current_user from flask_login import current_user
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
@ -124,6 +125,7 @@ def sco_dump_and_send_db():
fcntl.flock(x, fcntl.LOCK_UN) fcntl.flock(x, fcntl.LOCK_UN)
log("sco_dump_and_send_db: done.") log("sco_dump_and_send_db: done.")
flash("Données envoyées au serveur d'assistance")
return "\n".join(H) + html_sco_header.sco_footer() return "\n".join(H) + html_sco_header.sco_footer()

View File

@ -78,7 +78,7 @@ def formsemestre_createwithmodules():
H = [ H = [
html_sco_header.sco_header( html_sco_header.sco_header(
page_title="Création d'un semestre", page_title="Création d'un semestre",
javascripts=["libjs/AutoSuggest.js"], javascripts=["libjs/AutoSuggest.js", "js/formsemestre_edit.js"],
cssstyles=["css/autosuggest_inquisitor.css"], cssstyles=["css/autosuggest_inquisitor.css"],
bodyOnLoad="init_tf_form('')", bodyOnLoad="init_tf_form('')",
), ),
@ -99,7 +99,7 @@ def formsemestre_editwithmodules(formsemestre_id):
H = [ H = [
html_sco_header.html_sem_header( html_sco_header.html_sem_header(
"Modification du semestre", "Modification du semestre",
javascripts=["libjs/AutoSuggest.js"], javascripts=["libjs/AutoSuggest.js", "js/formsemestre_edit.js"],
cssstyles=["css/autosuggest_inquisitor.css"], cssstyles=["css/autosuggest_inquisitor.css"],
bodyOnLoad="init_tf_form('')", bodyOnLoad="init_tf_form('')",
) )
@ -213,7 +213,10 @@ def do_formsemestre_createwithmodules(edit=False):
# en APC, ne permet pas de changer de semestre # en APC, ne permet pas de changer de semestre
semestre_id_list = [formsemestre.semestre_id] semestre_id_list = [formsemestre.semestre_id]
else: else:
semestre_id_list = [-1] + list(range(1, NB_SEM + 1)) semestre_id_list = list(range(1, NB_SEM + 1))
if not formation.is_apc():
# propose "pas de semestre" seulement en classique
semestre_id_list.insert(0, -1)
semestre_id_labels = [] semestre_id_labels = []
for sid in semestre_id_list: for sid in semestre_id_list:
@ -341,6 +344,9 @@ def do_formsemestre_createwithmodules(edit=False):
"explanation": "en BUT, on ne peut pas modifier le semestre après création" "explanation": "en BUT, on ne peut pas modifier le semestre après création"
if formation.is_apc() if formation.is_apc()
else "", else "",
"attributes": ['onchange="change_semestre_id();"']
if formation.is_apc()
else "",
}, },
), ),
) )
@ -493,7 +499,8 @@ def do_formsemestre_createwithmodules(edit=False):
{ {
"input_type": "boolcheckbox", "input_type": "boolcheckbox",
"title": "", "title": "",
"explanation": "Autoriser tous les enseignants associés à un module à y créer des évaluations", "explanation": """Autoriser tous les enseignants associés
à un module à y créer des évaluations""",
}, },
), ),
( (
@ -534,11 +541,19 @@ def do_formsemestre_createwithmodules(edit=False):
] ]
nbmod = 0 nbmod = 0
if edit:
templ_sep = "<tr><td>%(label)s</td><td><b>Responsable</b></td><td><b>Inscrire</b></td></tr>"
else:
templ_sep = "<tr><td>%(label)s</td><td><b>Responsable</b></td></tr>"
for semestre_id in semestre_ids: for semestre_id in semestre_ids:
if formation.is_apc():
# pour restreindre l'édition aux module du semestre sélectionné
tr_class = 'class="sem{semestre_id}"'
else:
tr_class = ""
if edit:
templ_sep = f"""<tr {tr_class}><td>%(label)s</td><td><b>Responsable</b></td><td><b>Inscrire</b></td></tr>"""
else:
templ_sep = (
f"""<tr {tr_class}><td>%(label)s</td><td><b>Responsable</b></td></tr>"""
)
modform.append( modform.append(
( (
"sep", "sep",
@ -588,12 +603,12 @@ def do_formsemestre_createwithmodules(edit=False):
) )
fcg += "</select>" fcg += "</select>"
itemtemplate = ( itemtemplate = (
"""<tr><td class="tf-fieldlabel">%(label)s</td><td class="tf-field">%(elem)s</td><td>""" f"""<tr {tr_class}><td class="tf-fieldlabel">%(label)s</td><td class="tf-field">%(elem)s</td><td>"""
+ fcg + fcg
+ "</td></tr>" + "</td></tr>"
) )
else: else:
itemtemplate = """<tr><td class="tf-fieldlabel">%(label)s</td><td class="tf-field">%(elem)s</td></tr>""" itemtemplate = f"""<tr {tr_class}><td class="tf-fieldlabel">%(label)s</td><td class="tf-field">%(elem)s</td></tr>"""
modform.append( modform.append(
( (
"MI" + str(mod["module_id"]), "MI" + str(mod["module_id"]),

View File

@ -987,7 +987,6 @@ Il y a des notes en attente ! Le classement des étudiants n'a qu'une valeur ind
def formsemestre_status(formsemestre_id=None): def formsemestre_status(formsemestre_id=None):
"""Tableau de bord semestre HTML""" """Tableau de bord semestre HTML"""
# porté du DTML # porté du DTML
cnx = ndb.GetDBConnexion()
sem = sco_formsemestre.get_formsemestre(formsemestre_id, raise_soft_exc=True) sem = sco_formsemestre.get_formsemestre(formsemestre_id, raise_soft_exc=True)
modimpls = sco_moduleimpl.moduleimpl_withmodule_list( modimpls = sco_moduleimpl.moduleimpl_withmodule_list(
formsemestre_id=formsemestre_id formsemestre_id=formsemestre_id

View File

@ -1297,7 +1297,7 @@ th.formsemestre_status_inscrits {
text-align: center; text-align: center;
} }
td.formsemestre_status_code { td.formsemestre_status_code {
width: 2em; /* width: 2em; */
padding-right: 1em; padding-right: 1em;
} }

View File

@ -0,0 +1,14 @@
// Formulaire formsemestre_createwithmodules
function change_semestre_id() {
var semestre_id = $("#tf_semestre_id")[0].value;
for (var i = -1; i < 12; i++) {
$(".sem" + i).hide();
}
$(".sem" + semestre_id).show();
}
$(window).on('load', function () {
change_semestre_id();
});