From 732a4c5ce51397a3035d8783ce3cbf535c6acf23 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 28 Feb 2022 16:25:18 +0100 Subject: [PATCH 01/19] code cleaning --- app/scodoc/sco_cache.py | 76 ++++++++--------------------------------- 1 file changed, 15 insertions(+), 61 deletions(-) diff --git a/app/scodoc/sco_cache.py b/app/scodoc/sco_cache.py index 5fd0cec68..6330849c5 100644 --- a/app/scodoc/sco_cache.py +++ b/app/scodoc/sco_cache.py @@ -33,17 +33,12 @@ """ -# API ScoDoc8 pour les caches: -# sco_cache.NotesTableCache.get( formsemestre_id) -# => sco_cache.NotesTableCache.get(formsemestre_id) +# API pour les caches: +# sco_cache.MyCache.get( formsemestre_id) +# => sco_cache.MyCache.get(formsemestre_id) # -# sco_core.inval_cache(formsemestre_id=None, pdfonly=False, formsemestre_id_list=None) -# => deprecated, NotesTableCache.invalidate_formsemestre(formsemestre_id=None, pdfonly=False) -# -# -# Nouvelles fonctions: -# sco_cache.NotesTableCache.delete(formsemestre_id) -# sco_cache.NotesTableCache.delete_many(formsemestre_id_list) +# sco_cache.MyCache.delete(formsemestre_id) +# sco_cache.MyCache.delete_many(formsemestre_id_list) # # Bulletins PDF: # sco_cache.SemBulletinsPDFCache.get(formsemestre_id, version) @@ -203,49 +198,6 @@ class SemInscriptionsCache(ScoDocCache): 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) formsemestre_id=None, pdfonly=False ): @@ -278,22 +230,24 @@ def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=Fa if not pdfonly: # Delete cached notes and evaluations - NotesTableCache.delete_many(formsemestre_ids) if formsemestre_id: for fid in formsemestre_ids: EvaluationCache.invalidate_sem(fid) - if hasattr(g, "nt_cache") and fid in g.nt_cache: - del g.nt_cache[fid] + if ( + hasattr(g, "formsemestre_results_cache") + and fid in g.formsemestre_results_cache + ): + del g.formsemestre_results_cache[fid] + else: # optimization when we invalidate all evaluations: EvaluationCache.invalidate_all_sems() - if hasattr(g, "nt_cache"): - del g.nt_cache + if hasattr(g, "formsemestre_results_cache"): + del g.formsemestre_results_cache SemInscriptionsCache.delete_many(formsemestre_ids) - + ResultatsSemestreCache.delete_many(formsemestre_ids) + ValidationsSemestreCache.delete_many(formsemestre_ids) SemBulletinsPDFCache.invalidate_sems(formsemestre_ids) - ResultatsSemestreCache.delete_many(formsemestre_ids) - ValidationsSemestreCache.delete_many(formsemestre_ids) class DefferedSemCacheManager: From 8330009dcf1dedf6fb7ad3288375ae3e73171d84 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 28 Feb 2022 16:26:13 +0100 Subject: [PATCH 02/19] =?UTF-8?q?En=20BUT,=20remet=20S1=20si=20semestre=20?= =?UTF-8?q?non=20sp=C3=A9cifi=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/comp/res_sem.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/app/comp/res_sem.py b/app/comp/res_sem.py index 607ad1681..e27a157c1 100644 --- a/app/comp/res_sem.py +++ b/app/comp/res_sem.py @@ -8,11 +8,13 @@ """ from flask import g +from app import db 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 +from app.scodoc import sco_cache 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) 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) if not hasattr(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: return g.formsemestre_results_cache[formsemestre.id] - klass = ( - ResultatsSemestreBUT - if formsemestre.formation.is_apc() - else ResultatsSemestreClassic - ) + klass = ResultatsSemestreBUT if is_apc else ResultatsSemestreClassic g.formsemestre_results_cache[formsemestre.id] = klass(formsemestre) return g.formsemestre_results_cache[formsemestre.id] From 0e7f2f4deb160ead6d595fb6187871fdef8132ea Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 28 Feb 2022 16:27:27 +0100 Subject: [PATCH 03/19] flash --- app/scodoc/sco_dump_db.py | 2 ++ app/scodoc/sco_formsemestre_status.py | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/scodoc/sco_dump_db.py b/app/scodoc/sco_dump_db.py index f9521b292..4b55b41f6 100644 --- a/app/scodoc/sco_dump_db.py +++ b/app/scodoc/sco_dump_db.py @@ -51,6 +51,7 @@ import fcntl import subprocess import requests +from flask import flash from flask_login import current_user import app.scodoc.notesdb as ndb @@ -124,6 +125,7 @@ def sco_dump_and_send_db(): fcntl.flock(x, fcntl.LOCK_UN) 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() diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py index 4bfa7b725..2080e9572 100644 --- a/app/scodoc/sco_formsemestre_status.py +++ b/app/scodoc/sco_formsemestre_status.py @@ -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): """Tableau de bord semestre HTML""" # porté du DTML - cnx = ndb.GetDBConnexion() sem = sco_formsemestre.get_formsemestre(formsemestre_id, raise_soft_exc=True) modimpls = sco_moduleimpl.moduleimpl_withmodule_list( formsemestre_id=formsemestre_id From 8b5a99657106e9f9a67b2ab1f61262641fcce374 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 28 Feb 2022 16:28:08 +0100 Subject: [PATCH 04/19] Semestre BUT: ne propose pas indice -1 --- app/scodoc/sco_formsemestre_edit.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/scodoc/sco_formsemestre_edit.py b/app/scodoc/sco_formsemestre_edit.py index 81dbd5659..3e1c92fbb 100644 --- a/app/scodoc/sco_formsemestre_edit.py +++ b/app/scodoc/sco_formsemestre_edit.py @@ -213,7 +213,10 @@ def do_formsemestre_createwithmodules(edit=False): # en APC, ne permet pas de changer de semestre semestre_id_list = [formsemestre.semestre_id] 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 = [] for sid in semestre_id_list: From e993599b39d15567d07db8cc8de87d9b0011cc89 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 28 Feb 2022 17:57:12 +0100 Subject: [PATCH 05/19] =?UTF-8?q?Restreint=20edition=20modules=20semestres?= =?UTF-8?q?=20BUT=20aux=20module=20du=20m=C3=AAme=20sem.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/scodoc/sco_formsemestre_edit.py | 30 ++++++++++++++++++++--------- app/static/js/formsemestre_edit.js | 14 ++++++++++++++ 2 files changed, 35 insertions(+), 9 deletions(-) create mode 100644 app/static/js/formsemestre_edit.js diff --git a/app/scodoc/sco_formsemestre_edit.py b/app/scodoc/sco_formsemestre_edit.py index 3e1c92fbb..f056a3e62 100644 --- a/app/scodoc/sco_formsemestre_edit.py +++ b/app/scodoc/sco_formsemestre_edit.py @@ -78,7 +78,7 @@ def formsemestre_createwithmodules(): H = [ html_sco_header.sco_header( page_title="Création d'un semestre", - javascripts=["libjs/AutoSuggest.js"], + javascripts=["libjs/AutoSuggest.js", "js/formsemestre_edit.js"], cssstyles=["css/autosuggest_inquisitor.css"], bodyOnLoad="init_tf_form('')", ), @@ -99,7 +99,7 @@ def formsemestre_editwithmodules(formsemestre_id): H = [ html_sco_header.html_sem_header( "Modification du semestre", - javascripts=["libjs/AutoSuggest.js"], + javascripts=["libjs/AutoSuggest.js", "js/formsemestre_edit.js"], cssstyles=["css/autosuggest_inquisitor.css"], bodyOnLoad="init_tf_form('')", ) @@ -344,6 +344,9 @@ def do_formsemestre_createwithmodules(edit=False): "explanation": "en BUT, on ne peut pas modifier le semestre après création" if formation.is_apc() else "", + "attributes": ['onchange="change_semestre_id();"'] + if formation.is_apc() + else "", }, ), ) @@ -496,7 +499,8 @@ def do_formsemestre_createwithmodules(edit=False): { "input_type": "boolcheckbox", "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""", }, ), ( @@ -537,11 +541,19 @@ def do_formsemestre_createwithmodules(edit=False): ] nbmod = 0 - if edit: - templ_sep = "%(label)sResponsableInscrire" - else: - templ_sep = "%(label)sResponsable" + 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"""%(label)sResponsableInscrire""" + else: + templ_sep = ( + f"""%(label)sResponsable""" + ) modform.append( ( "sep", @@ -591,12 +603,12 @@ def do_formsemestre_createwithmodules(edit=False): ) fcg += "" itemtemplate = ( - """%(label)s%(elem)s""" + f"""%(label)s%(elem)s""" + fcg + "" ) else: - itemtemplate = """%(label)s%(elem)s""" + itemtemplate = f"""%(label)s%(elem)s""" modform.append( ( "MI" + str(mod["module_id"]), diff --git a/app/static/js/formsemestre_edit.js b/app/static/js/formsemestre_edit.js new file mode 100644 index 000000000..3394d659c --- /dev/null +++ b/app/static/js/formsemestre_edit.js @@ -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(); +}); \ No newline at end of file From b56a20643d5366322c27839a542aeda604398d1e Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 28 Feb 2022 20:01:24 +0100 Subject: [PATCH 06/19] largeur colonne codes modules --- app/static/css/scodoc.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index b3a80640a..d735077a4 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -1297,7 +1297,7 @@ th.formsemestre_status_inscrits { text-align: center; } td.formsemestre_status_code { - width: 2em; + /* width: 2em; */ padding-right: 1em; } From 13b40936b8cedce19b39b89455a314c8cb49af3e Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 28 Feb 2022 20:02:10 +0100 Subject: [PATCH 07/19] =?UTF-8?q?Nouveau=20calcul=20(correct=3F)=20de=20la?= =?UTF-8?q?=20moyenne=20de=20mati=C3=A8re=20en=20classic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/comp/moy_mat.py | 6 ++-- app/comp/moy_ue.py | 67 +++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 67 insertions(+), 6 deletions(-) diff --git a/app/comp/moy_mat.py b/app/comp/moy_mat.py index e5ba903c2..0a7522637 100644 --- a/app/comp/moy_mat.py +++ b/app/comp/moy_mat.py @@ -40,13 +40,11 @@ def compute_mat_moys_classic( modimpl_mask = np.array( [m.module.matiere.id == matiere_id for m in formsemestre.modimpls_sorted] ) - etud_moy_gen, _, _ = moy_ue.compute_ue_moys_classic( - formsemestre, + etud_moy_mat = moy_ue.compute_mat_moys_classic( sem_matrix=sem_matrix, - ues=ues, modimpl_inscr_df=modimpl_inscr_df, modimpl_coefs=modimpl_coefs, modimpl_mask=modimpl_mask, ) - matiere_moy[matiere_id] = etud_moy_gen + matiere_moy[matiere_id] = etud_moy_mat return matiere_moy diff --git a/app/comp/moy_ue.py b/app/comp/moy_ue.py index 563fb3b1d..efbe7cd34 100644 --- a/app/comp/moy_ue.py +++ b/app/comp/moy_ue.py @@ -294,7 +294,8 @@ def compute_ue_moys_classic( modimpl_coefs: np.array, modimpl_mask: np.array, ) -> 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 NI non inscrit à (au moins un) module de cette UE 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.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) if coefs.dtype == np.object: # arrive sur des tableaux vides 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 +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( formsemestre: FormSemestre, sem_modimpl_moys: np.array, From 7edd0511835ca58d8bcb551541632808ac56b38a Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 1 Mar 2022 09:34:05 +0100 Subject: [PATCH 08/19] Fix: bonus St Quentin / Ville d'Avray --- app/comp/bonus_spo.py | 17 +++++++++++------ sco_version.py | 2 +- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/app/comp/bonus_spo.py b/app/comp/bonus_spo.py index 99738336e..cb3d93e4f 100644 --- a/app/comp/bonus_spo.py +++ b/app/comp/bonus_spo.py @@ -338,9 +338,12 @@ class BonusAisneStQuentin(BonusSportAdditif): # pas d'étudiants ou pas d'UE ou pas de module... return # Calcule moyenne pondérée des notes de sport: - bonus_moy_arr = np.sum( - sem_modimpl_moys_inscrits * modimpl_coefs_etuds_no_nan, axis=1 - ) / np.sum(modimpl_coefs_etuds_no_nan, axis=1) + with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN) + bonus_moy_arr = np.sum( + sem_modimpl_moys_inscrits * modimpl_coefs_etuds_no_nan, axis=1 + ) / np.sum(modimpl_coefs_etuds_no_nan, axis=1) + np.nan_to_num(bonus_moy_arr, nan=0.0, copy=False) + bonus_moy_arr[bonus_moy_arr < 10.0] = 0.0 bonus_moy_arr[bonus_moy_arr >= 18.1] = 0.5 bonus_moy_arr[bonus_moy_arr >= 16.1] = 0.4 @@ -823,9 +826,11 @@ class BonusVilleAvray(BonusSport): # pas d'étudiants ou pas d'UE ou pas de module... return # Calcule moyenne pondérée des notes de sport: - bonus_moy_arr = np.sum( - sem_modimpl_moys_inscrits * modimpl_coefs_etuds_no_nan, axis=1 - ) / np.sum(modimpl_coefs_etuds_no_nan, axis=1) + with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN) + bonus_moy_arr = np.sum( + sem_modimpl_moys_inscrits * modimpl_coefs_etuds_no_nan, axis=1 + ) / np.sum(modimpl_coefs_etuds_no_nan, axis=1) + np.nan_to_num(bonus_moy_arr, nan=0.0, copy=False) bonus_moy_arr[bonus_moy_arr < 10.0] = 0.0 bonus_moy_arr[bonus_moy_arr >= 16.0] = 0.3 bonus_moy_arr[bonus_moy_arr >= 12.0] = 0.2 diff --git a/sco_version.py b/sco_version.py index b47a266d7..fbbf5bc82 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.1.66" +SCOVERSION = "9.1.67" SCONAME = "ScoDoc" From c5c0b510ec599a2486d4d39d9bc4519c7c51d41d Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 1 Mar 2022 09:48:37 +0100 Subject: [PATCH 09/19] filename export formations --- app/scodoc/sco_formations.py | 8 +++++++- app/scodoc/sco_utils.py | 21 ++++++++++++++++----- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/app/scodoc/sco_formations.py b/app/scodoc/sco_formations.py index caeeb707b..573c95dfb 100644 --- a/app/scodoc/sco_formations.py +++ b/app/scodoc/sco_formations.py @@ -151,8 +151,14 @@ def formation_export( if mod["ects"] is None: del mod["ects"] + filename = f"scodoc_formation_{formation.departement.acronym}_{formation.acronyme or ''}_v{formation.version}" return scu.sendResult( - F, name="formation", format=format, force_outer_xml_tag=False, attached=True + F, + name="formation", + format=format, + force_outer_xml_tag=False, + attached=True, + filename=filename, ) diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index b3d99ac3e..7af19bc19 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -645,21 +645,30 @@ class ScoDocJSONEncoder(json.JSONEncoder): return json.JSONEncoder.default(self, o) -def sendJSON(data, attached=False): +def sendJSON(data, attached=False, filename=None): js = json.dumps(data, indent=1, cls=ScoDocJSONEncoder) return send_file( - js, filename="sco_data.json", mime=JSON_MIMETYPE, attached=attached + js, filename=filename or "sco_data.json", mime=JSON_MIMETYPE, attached=attached ) -def sendXML(data, tagname=None, force_outer_xml_tag=True, attached=False, quote=True): +def sendXML( + data, + tagname=None, + force_outer_xml_tag=True, + attached=False, + quote=True, + filename=None, +): if type(data) != list: data = [data] # always list-of-dicts if force_outer_xml_tag: data = [{tagname: data}] tagname += "_list" doc = sco_xml.simple_dictlist2xml(data, tagname=tagname, quote=quote) - return send_file(doc, filename="sco_data.xml", mime=XML_MIMETYPE, attached=attached) + return send_file( + doc, filename=filename or "sco_data.xml", mime=XML_MIMETYPE, attached=attached + ) def sendResult( @@ -669,6 +678,7 @@ def sendResult( force_outer_xml_tag=True, attached=False, quote_xml=True, + filename=None, ): if (format is None) or (format == "html"): return data @@ -679,9 +689,10 @@ def sendResult( force_outer_xml_tag=force_outer_xml_tag, attached=attached, quote=quote_xml, + filename=filename, ) elif format == "json": - return sendJSON(data, attached=attached) + return sendJSON(data, attached=attached, filename=filename) else: raise ValueError("invalid format: %s" % format) From 6943ccb8729823ad9f73e8b76e6f03ae5db2b8ad Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 1 Mar 2022 10:16:34 +0100 Subject: [PATCH 10/19] typo (sel. modules BUT) --- app/scodoc/sco_formsemestre_edit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/scodoc/sco_formsemestre_edit.py b/app/scodoc/sco_formsemestre_edit.py index f056a3e62..7174cfc3d 100644 --- a/app/scodoc/sco_formsemestre_edit.py +++ b/app/scodoc/sco_formsemestre_edit.py @@ -545,7 +545,7 @@ def do_formsemestre_createwithmodules(edit=False): 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}"' + tr_class = f'class="sem{semestre_id}"' else: tr_class = "" if edit: From 10c96ad683fa0ca1a899f160588e1e6b769023c8 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 1 Mar 2022 10:21:15 +0100 Subject: [PATCH 11/19] PE: check submitted template (utf8) --- app/pe/pe_view.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/app/pe/pe_view.py b/app/pe/pe_view.py index 5a98d375f..5af1a5754 100644 --- a/app/pe/pe_view.py +++ b/app/pe/pe_view.py @@ -36,6 +36,7 @@ """ from flask import send_file, request +from app.scodoc.sco_exceptions import ScoValueError import app.scodoc.sco_utils as scu from app.scodoc import sco_formsemestre @@ -97,8 +98,12 @@ def pe_view_sem_recap( template_latex = "" # template fourni via le formulaire Web if avis_tmpl_file: - template_latex = avis_tmpl_file.read().decode('utf-8') - template_latex = template_latex + try: + template_latex = avis_tmpl_file.read().decode("utf-8") + except UnicodeDecodeError as e: + raise ScoValueError( + "Données (template) invalides (caractères non UTF8 ?)" + ) from e else: # template indiqué dans préférences ScoDoc ? template_latex = pe_avislatex.get_code_latex_from_scodoc_preference( @@ -114,7 +119,7 @@ def pe_view_sem_recap( footer_latex = "" # template fourni via le formulaire Web if footer_tmpl_file: - footer_latex = footer_tmpl_file.read().decode('utf-8') + footer_latex = footer_tmpl_file.read().decode("utf-8") footer_latex = footer_latex else: footer_latex = pe_avislatex.get_code_latex_from_scodoc_preference( From f0e731d151fbb2a28e0e65a3a55650f0065ec27a Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 1 Mar 2022 10:33:53 +0100 Subject: [PATCH 12/19] Fix: bulletin classique quand coef UE None --- app/scodoc/sco_bulletins.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/scodoc/sco_bulletins.py b/app/scodoc/sco_bulletins.py index a5d84cf4f..47947bd36 100644 --- a/app/scodoc/sco_bulletins.py +++ b/app/scodoc/sco_bulletins.py @@ -323,9 +323,7 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"): if ue_status["coef_ue"] != None: u["coef_ue_txt"] = scu.fmt_coef(ue_status["coef_ue"]) else: - # C'est un bug: - log("u=" + pprint.pformat(u)) - raise Exception("invalid None coef for ue") + u["coef_ue_txt"] = "-" if ( dpv From 523ad7ad2a19caed963f9a5170ba6930a35afa67 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 1 Mar 2022 10:40:38 +0100 Subject: [PATCH 13/19] Modif bonus La Rochelle --- app/comp/bonus_spo.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/app/comp/bonus_spo.py b/app/comp/bonus_spo.py index cb3d93e4f..aa7697d5a 100644 --- a/app/comp/bonus_spo.py +++ b/app/comp/bonus_spo.py @@ -198,7 +198,10 @@ class BonusSportAdditif(BonusSport): à la moyenne générale du semestre déjà obtenue par l'étudiant. """ - seuil_moy_gen = 10.0 # seuls les points au dessus du seuil sont comptés + seuil_moy_gen = 10.0 # seuls les bonus au dessus du seuil sont pris en compte + seuil_comptage = ( + None # les points au dessus du seuil sont comptés (defaut: seuil_moy_gen) + ) proportion_point = 0.05 # multiplie les points au dessus du seuil def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan): @@ -211,10 +214,13 @@ class BonusSportAdditif(BonusSport): if 0 in sem_modimpl_moys_inscrits.shape: # pas d'étudiants ou pas d'UE ou pas de module... return + seuil_comptage = ( + self.seuil_moy_gen if self.seuil_comptage is None else self.seuil_comptage + ) bonus_moy_arr = np.sum( np.where( sem_modimpl_moys_inscrits > self.seuil_moy_gen, - (sem_modimpl_moys_inscrits - self.seuil_moy_gen) + (sem_modimpl_moys_inscrits - self.seuil_comptage) * self.proportion_point, 0.0, ), @@ -607,8 +613,9 @@ class BonusLaRochelle(BonusSportAdditif): name = "bonus_iutlr" displayed_name = "IUT de La Rochelle" - seuil_moy_gen = 10.0 # tous les points sont comptés - proportion_point = 0.01 + seuil_moy_gen = 10.0 # si bonus > 10, + seuil_comptage = 0.0 # tous les points sont comptés + proportion_point = 0.01 # 1% class BonusLeHavre(BonusSportMultiplicatif): From c0719df0c0f0777c79fa3ff27dbda6250c30e194 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 1 Mar 2022 19:27:03 +0100 Subject: [PATCH 14/19] noms modules sur menu saisie absences --- app/scodoc/sco_edit_module.py | 7 +++++-- app/views/absences.py | 5 ++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/app/scodoc/sco_edit_module.py b/app/scodoc/sco_edit_module.py index 9ea4e2fe7..63bcb72ab 100644 --- a/app/scodoc/sco_edit_module.py +++ b/app/scodoc/sco_edit_module.py @@ -562,7 +562,7 @@ def module_edit(module_id=None): "code", { "size": 10, - "explanation": "code du module (doit être unique dans la formation)", + "explanation": "code du module (issu du programme, exemple M1203 ou R2.01. Doit être unique dans la formation)", "allow_null": False, "validator": lambda val, field, formation_id=formation_id: check_module_code_unicity( val, field, formation_id, module_id=module_id @@ -701,7 +701,10 @@ def module_edit(module_id=None): { "title": "Code Apogée", "size": 25, - "explanation": "(optionnel) code élément pédagogique Apogée ou liste de codes ELP séparés par des virgules", + "explanation": """(optionnel) code élément pédagogique Apogée ou liste de codes ELP + séparés par des virgules (ce code est propre à chaque établissement, se rapprocher + du référent Apogée). + """, "validator": lambda val, _: len(val) < APO_CODE_STR_LEN, }, ), diff --git a/app/views/absences.py b/app/views/absences.py index cf8de2c18..bdbae1458 100644 --- a/app/views/absences.py +++ b/app/views/absences.py @@ -611,8 +611,7 @@ def SignaleAbsenceGrSemestre( """\n""" % { "modimpl_id": modimpl["moduleimpl_id"], - "modname": modimpl["module"]["code"] - or "" + "modname": (modimpl["module"]["code"] or "") + " " + (modimpl["module"]["abbrev"] or modimpl["module"]["titre"]), "sel": sel, @@ -624,7 +623,7 @@ def SignaleAbsenceGrSemestre( sel = "selected" # aucun module specifie H.append( """

-Module concerné par ces absences (%(optionel_txt)s): +Module concerné par ces absences (%(optionel_txt)s):