diff --git a/app/comp/moy_sem.py b/app/comp/moy_sem.py index ae167d4e77..5caa3d3934 100644 --- a/app/comp/moy_sem.py +++ b/app/comp/moy_sem.py @@ -48,14 +48,15 @@ def compute_sem_moys_apc( return moy_gen -def comp_ranks_series(notes: pd.Series) -> dict[int, str]: +def comp_ranks_series(notes: pd.Series) -> (pd.Series, pd.Series): """Calcul rangs à partir d'une séries ("vecteur") de notes (index etudid, valeur numérique) en tenant compte des ex-aequos. - Result: { etudid : rang:str } où rang est une chaine decrivant le rang. + Result: Series { etudid : rang:str } où rang est une chaine decrivant le rang. """ notes = notes.sort_values(ascending=False) # Serie, tri par ordre décroissant - rangs = pd.Series(index=notes.index, dtype=str) # le rang est une chaîne + rangs_str = pd.Series(index=notes.index, dtype=str) # le rang est une chaîne + rangs_int = pd.Series(index=notes.index, dtype=int) # le rang numérique pour tris N = len(notes) nb_ex = 0 # nb d'ex-aequo consécutifs en cours notes_i = notes.iat @@ -67,6 +68,7 @@ def comp_ranks_series(notes: pd.Series) -> dict[int, str]: next = None val = notes_i[i] if nb_ex: + rangs_int[etudid] = i + 1 - nb_ex srang = "%d ex" % (i + 1 - nb_ex) if val == next: nb_ex += 1 @@ -74,9 +76,11 @@ def comp_ranks_series(notes: pd.Series) -> dict[int, str]: nb_ex = 0 else: if val == next: + rangs_int[etudid] = i + 1 - nb_ex srang = "%d ex" % (i + 1 - nb_ex) nb_ex = 1 else: + rangs_int[etudid] = i + 1 srang = "%d" % (i + 1) - rangs[etudid] = srang - return rangs + rangs_str[etudid] = srang + return rangs_str, rangs_int diff --git a/app/comp/res_common.py b/app/comp/res_common.py index 006cbe6b9c..2e6429cb16 100644 --- a/app/comp/res_common.py +++ b/app/comp/res_common.py @@ -51,6 +51,7 @@ class ResultatsSemestre(ResultatsCache): "etud_moy_ue: DataFrame columns UE, rows etudid" self.etud_moy_gen = {} self.etud_moy_gen_ranks = {} + self.etud_moy_gen_ranks_int = {} self.modimpls_results: ModuleImplResults = None "Résultats de chaque modimpl: dict { modimpl.id : ModuleImplResults(Classique ou BUT) }" self.etud_coef_ue_df = None @@ -302,6 +303,7 @@ class NotesTableCompat(ResultatsSemestre): "bonus_ues", "malus", "etud_moy_gen_ranks", + "etud_moy_gen_ranks_int", "ue_rangs", ) @@ -319,17 +321,33 @@ class NotesTableCompat(ResultatsSemestre): self.expr_diagnostics = "" self.parcours = self.formsemestre.formation.get_parcours() - def get_etudids(self, sorted=False) -> list[int]: - """Liste des etudids inscrits, incluant les démissionnaires. - Si sorted, triée par moy. générale décroissante - Sinon, triée par ordre alphabetique de NOM + def get_inscrits(self, include_demdef=True, order_by=False) -> list[Identite]: + """Liste des étudiants inscrits + order_by = False|'nom'|'moy' tri sur nom ou sur moyenne générale (indicative) + + Note: pour récupérer les etudids des inscrits, non triés, il est plus efficace + d'utiliser `[ ins.etudid for ins in nt.formsemestre.inscriptions ]` + """ + etuds = self.formsemestre.get_inscrits( + include_demdef=include_demdef, order=(order_by == "nom") + ) + if order_by == "moy": + etuds.sort( + key=lambda e: ( + self.etud_moy_gen_ranks_int.get(e.id, 100000), + e.sort_key, + ) + ) + return etuds + + def get_etudids(self) -> list[int]: + """(deprecated) + Liste des etudids inscrits, incluant les démissionnaires. + triée par ordre alphabetique de NOM + (à éviter: renvoie les etudids, mais est moins efficace que get_inscrits) """ # Note: pour avoir les inscrits non triés, # utiliser [ ins.etudid for ins in self.formsemestre.inscriptions ] - if sorted: - # Tri par moy. generale décroissante - return [x[-1] for x in self.T] - return [x["etudid"] for x in self.inscrlist] @cached_property @@ -385,11 +403,14 @@ class NotesTableCompat(ResultatsSemestre): Moyenne générale: etud_moy_gen_ranks Par UE (sauf ue bonus) """ - self.etud_moy_gen_ranks = moy_sem.comp_ranks_series(self.etud_moy_gen) + ( + self.etud_moy_gen_ranks, + self.etud_moy_gen_ranks_int, + ) = 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), + moy_sem.comp_ranks_series(moy_ue)[0], # juste en chaine int(moy_ue.count()), ) # .count() -> nb of non NaN values diff --git a/app/models/etudiants.py b/app/models/etudiants.py index d919ee97c9..18f13380d6 100644 --- a/app/models/etudiants.py +++ b/app/models/etudiants.py @@ -134,6 +134,7 @@ class Identite(db.Model): # ScoDoc7 output_formators: (backward compat) e["etudid"] = self.id e["date_naissance"] = ndb.DateISOtoDMY(e["date_naissance"]) + e["ne"] = {"M": "", "F": "ne"}.get(self.civilite, "(e)") return {k: e[k] or "" for k in e} # convert_null_outputs_to_empty def to_dict_bul(self, include_urls=True): diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index 2d6006c203..c2850c2dcd 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -363,17 +363,17 @@ class FormSemestre(db.Model): etudid, self.date_debut.isoformat(), self.date_fin.isoformat() ) - def get_inscrits(self, include_demdef=False, sorted=False) -> list[Identite]: + def get_inscrits(self, include_demdef=False, order=False) -> list[Identite]: """Liste des étudiants inscrits à ce semestre Si include_demdef, tous les étudiants, avec les démissionnaires et défaillants. - Si sorted, tri par clé sort_key + Si order, tri par clé sort_key """ if include_demdef: etuds = [ins.etud for ins in self.inscriptions] else: etuds = [ins.etud for ins in self.inscriptions if ins.etat == scu.INSCRIT] - if sorted: + if order: etuds.sort(key=lambda e: e.sort_key) return etuds diff --git a/app/scodoc/sco_bulletins_pdf.py b/app/scodoc/sco_bulletins_pdf.py index fa599fa56b..94cfcf6bc2 100644 --- a/app/scodoc/sco_bulletins_pdf.py +++ b/app/scodoc/sco_bulletins_pdf.py @@ -188,7 +188,7 @@ def get_formsemestre_bulletins_pdf(formsemestre_id, version="selectedevals"): bookmarks = {} filigrannes = {} i = 1 - for etud in formsemestre.get_inscrits(include_demdef=True, sorted=True): + for etud in formsemestre.get_inscrits(include_demdef=True, order=True): frag, filigranne = sco_bulletins.do_formsemestre_bulletinetud( formsemestre_id, etud.id, diff --git a/app/scodoc/sco_etud.py b/app/scodoc/sco_etud.py index b4af85fda6..d019d2df24 100644 --- a/app/scodoc/sco_etud.py +++ b/app/scodoc/sco_etud.py @@ -867,11 +867,7 @@ def fill_etuds_info(etuds, add_admission=True): Si add_admission: ajoute au dict le schamps "admission" s'il n'y sont pas déjà. """ - from app.scodoc import sco_formsemestre - from app.scodoc import sco_formsemestre_inscriptions - cnx = ndb.GetDBConnexion() - # open('/tmp/t','w').write( str(etuds) ) for etud in etuds: etudid = etud["etudid"] etud["dept"] = g.scodoc_dept @@ -894,49 +890,7 @@ def fill_etuds_info(etuds, add_admission=True): etud.update(adr) format_etud_ident(etud) - # Semestres dans lesquel il est inscrit - ins = sco_formsemestre_inscriptions.do_formsemestre_inscription_list( - {"etudid": etudid} - ) - etud["ins"] = ins - sems = [] - cursem = None # semestre "courant" ou il est inscrit - for i in ins: - sem = sco_formsemestre.get_formsemestre(i["formsemestre_id"]) - if sco_formsemestre.sem_est_courant(sem): - cursem = sem - curi = i - sem["ins"] = i - sems.append(sem) - # trie les semestres par date de debut, le plus recent d'abord - # (important, ne pas changer (suivi cohortes)) - sems.sort(key=itemgetter("dateord"), reverse=True) - etud["sems"] = sems - etud["cursem"] = cursem - if cursem: - etud["inscription"] = cursem["titremois"] - etud["inscriptionstr"] = "Inscrit en " + cursem["titremois"] - etud["inscription_formsemestre_id"] = cursem["formsemestre_id"] - etud["etatincursem"] = curi["etat"] - etud["situation"] = descr_situation_etud(etudid, etud["ne"]) - # XXX est-ce utile ? sco_groups.etud_add_group_infos( etud, cursem) - else: - if etud["sems"]: - if etud["sems"][0]["dateord"] > time.strftime( - "%Y-%m-%d", time.localtime() - ): - etud["inscription"] = "futur" - etud["situation"] = "futur élève" - else: - etud["inscription"] = "ancien" - etud["situation"] = "ancien élève" - else: - etud["inscription"] = "non inscrit" - etud["situation"] = etud["inscription"] - etud["inscriptionstr"] = etud["inscription"] - etud["inscription_formsemestre_id"] = None - # XXXetud['partitions'] = {} # ne va pas chercher les groupes des anciens semestres - etud["etatincursem"] = "?" + etud.update(etud_inscriptions_infos(etudid, etud["ne"])) # nettoyage champs souvent vides if etud.get("nomlycee"): @@ -974,8 +928,56 @@ def fill_etuds_info(etuds, add_admission=True): etud["telephonemobilestr"] = "" -def descr_situation_etud(etudid, ne=""): - """chaine decrivant la situation actuelle de l'etudiant""" +def etud_inscriptions_infos(etudid: int, ne="") -> dict: + """Dict avec les informations sur les semestres passés et courant""" + from app.scodoc import sco_formsemestre + from app.scodoc import sco_formsemestre_inscriptions + + etud = {} + # Semestres dans lesquel il est inscrit + ins = sco_formsemestre_inscriptions.do_formsemestre_inscription_list( + {"etudid": etudid} + ) + etud["ins"] = ins + sems = [] + cursem = None # semestre "courant" ou il est inscrit + for i in ins: + sem = sco_formsemestre.get_formsemestre(i["formsemestre_id"]) + if sco_formsemestre.sem_est_courant(sem): + cursem = sem + curi = i + sem["ins"] = i + sems.append(sem) + # trie les semestres par date de debut, le plus recent d'abord + # (important, ne pas changer (suivi cohortes)) + sems.sort(key=itemgetter("dateord"), reverse=True) + etud["sems"] = sems + etud["cursem"] = cursem + if cursem: + etud["inscription"] = cursem["titremois"] + etud["inscriptionstr"] = "Inscrit en " + cursem["titremois"] + etud["inscription_formsemestre_id"] = cursem["formsemestre_id"] + etud["etatincursem"] = curi["etat"] + etud["situation"] = descr_situation_etud(etudid, ne) + else: + if etud["sems"]: + if etud["sems"][0]["dateord"] > time.strftime("%Y-%m-%d", time.localtime()): + etud["inscription"] = "futur" + etud["situation"] = "futur élève" + else: + etud["inscription"] = "ancien" + etud["situation"] = "ancien élève" + else: + etud["inscription"] = "non inscrit" + etud["situation"] = etud["inscription"] + etud["inscriptionstr"] = etud["inscription"] + etud["inscription_formsemestre_id"] = None + etud["etatincursem"] = "?" + return etud + + +def descr_situation_etud(etudid: int, ne="") -> str: + """chaîne décrivant la situation actuelle de l'étudiant""" from app.scodoc import sco_formsemestre cnx = ndb.GetDBConnexion() @@ -992,7 +994,7 @@ def descr_situation_etud(etudid, ne=""): ) r = cursor.dictfetchone() if not r: - situation = "non inscrit" + situation = "non inscrit" + ne else: sem = sco_formsemestre.get_formsemestre(r["formsemestre_id"]) if r["etat"] == "I": diff --git a/app/scodoc/sco_parcours_dut.py b/app/scodoc/sco_parcours_dut.py index 1969dbbcb4..32c1cb3aca 100644 --- a/app/scodoc/sco_parcours_dut.py +++ b/app/scodoc/sco_parcours_dut.py @@ -36,7 +36,7 @@ import app.scodoc.sco_utils as scu import app.scodoc.notesdb as ndb from app import log from app.scodoc.scolog import logdb -from app.scodoc import sco_cache +from app.scodoc import sco_cache, sco_etud from app.scodoc import sco_formsemestre from app.scodoc import sco_formations from app.scodoc.sco_codes_parcours import ( @@ -319,9 +319,6 @@ class SituationEtudParcoursGeneric(object): sont validés. En sortie, sem_idx_set contient ceux qui n'ont pas été validés.""" for sem in self.get_semestres(): if sem["formation_code"] == self.formation.formation_code: - # nt = sco_cache.NotesTableCache.get( - # sem["formsemestre_id"] - # ) # > get_etud_decision_sem formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"]) nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) decision = nt.get_etud_decision_sem(self.etudid) @@ -333,15 +330,16 @@ class SituationEtudParcoursGeneric(object): def _comp_semestres(self): # etud['sems'] est trie par date decroissante (voir fill_etuds_info) + if not "sems" in self.etud: + self.etud["sems"] = sco_etud.etud_inscriptions_infos( + self.etud["etudid"], self.etud["ne"] + )["sems"] sems = self.etud["sems"][:] # copy sems.reverse() # Nb max d'UE et acronymes ue_acros = {} # acronyme ue : 1 nb_max_ue = 0 for sem in sems: - # nt = sco_cache.NotesTableCache.get( - # sem["formsemestre_id"] - # ) # > get_ues_stat_dict formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"]) nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) ues = nt.get_ues_stat_dict(filter_sport=True) diff --git a/app/scodoc/sco_prepajury.py b/app/scodoc/sco_prepajury.py index 1a68373799..a15b9b0db9 100644 --- a/app/scodoc/sco_prepajury.py +++ b/app/scodoc/sco_prepajury.py @@ -31,27 +31,31 @@ import time from openpyxl.styles.numbers import FORMAT_NUMBER_00 +from flask import flash from flask import request from flask_login import current_user -import app.scodoc.sco_utils as scu +from app.comp import res_sem +from app.comp.res_common import NotesTableCompat +from app.models import FormSemestre, Identite from app.scodoc import sco_abs +from app.scodoc import sco_codes_parcours from app.scodoc import sco_groups -from app.scodoc import sco_cache +from app.scodoc import sco_etud from app.scodoc import sco_excel from app.scodoc import sco_formsemestre from app.scodoc import sco_parcours_dut -from app.scodoc import sco_codes_parcours -import sco_version -from app.scodoc import sco_etud from app.scodoc import sco_preferences from app.scodoc.sco_excel import ScoExcelSheet +import app.scodoc.sco_utils as scu +import sco_version def feuille_preparation_jury(formsemestre_id): "Feuille excel pour preparation des jurys" - nt = sco_cache.NotesTableCache.get(formsemestre_id) - etudids = nt.get_etudids(sorted=True) # tri par moy gen + formsemestre = FormSemestre.query.get_or_404(formsemestre_id) + nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) + etuds: Identite = nt.get_inscrits(order_by="moy") # tri par moy gen sem = sco_formsemestre.get_formsemestre(formsemestre_id) etud_groups = sco_groups.formsemestre_get_etud_groupnames(formsemestre_id) @@ -74,64 +78,65 @@ def feuille_preparation_jury(formsemestre_id): groupestd = {} # etudid : nom groupe principal nbabs = {} nbabsjust = {} - for etudid in etudids: - info = sco_etud.get_etud_info(etudid=etudid, filled=True) - if not info: - continue # should not occur... - etud = info[0] - Se = sco_parcours_dut.SituationEtudParcours(etud, formsemestre_id) + for etud in etuds: + Se = sco_parcours_dut.SituationEtudParcours( + etud.to_dict_scodoc7(), formsemestre_id + ) if Se.prev: - ntp = sco_cache.NotesTableCache.get(Se.prev["formsemestre_id"]) + formsemestre_prev = FormSemestre.query.get_or_404( + Se.prev["formsemestre_id"] + ) + ntp: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre_prev) for ue in ntp.get_ues_stat_dict(filter_sport=True): - ue_status = ntp.get_etud_ue_status(etudid, ue["ue_id"]) + ue_status = ntp.get_etud_ue_status(etud.id, ue["ue_id"]) ue_code_s = ( ue["ue_code"] + "_%s" % ntp.sem["semestre_id"] ) # code indentifiant l'UE - prev_moy_ue[ue_code_s][etudid] = ue_status["moy"] if ue_status else "" + prev_moy_ue[ue_code_s][etud.id] = ue_status["moy"] if ue_status else "" prev_ue_acro[ue_code_s] = (ue["numero"], ue["acronyme"], ue["titre"]) - prev_moy[etudid] = ntp.get_etud_moy_gen(etudid) - prev_decision = ntp.get_etud_decision_sem(etudid) + prev_moy[etud.id] = ntp.get_etud_moy_gen(etud.id) + prev_decision = ntp.get_etud_decision_sem(etud.id) if prev_decision: - prev_code[etudid] = prev_decision["code"] + prev_code[etud.id] = prev_decision["code"] if prev_decision["compense_formsemestre_id"]: - prev_code[etudid] += "+" # indique qu'il a servi a compenser + prev_code[etud.id] += "+" # indique qu'il a servi a compenser - moy[etudid] = nt.get_etud_moy_gen(etudid) + moy[etud.id] = nt.get_etud_moy_gen(etud.id) for ue in nt.get_ues_stat_dict(filter_sport=True): - ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"]) + ue_status = nt.get_etud_ue_status(etud.id, ue["ue_id"]) ue_code_s = ue["ue_code"] + "_%s" % nt.sem["semestre_id"] - moy_ue[ue_code_s][etudid] = ue_status["moy"] if ue_status else "" + moy_ue[ue_code_s][etud.id] = ue_status["moy"] if ue_status else "" ue_acro[ue_code_s] = (ue["numero"], ue["acronyme"], ue["titre"]) if Se.prev: try: - moy_inter[etudid] = (moy[etudid] + prev_moy[etudid]) / 2.0 + moy_inter[etud.id] = (moy[etud.id] + prev_moy[etud.id]) / 2.0 except: pass - decision = nt.get_etud_decision_sem(etudid) + decision = nt.get_etud_decision_sem(etud.id) if decision: - code[etudid] = decision["code"] + code[etud.id] = decision["code"] if decision["compense_formsemestre_id"]: - code[etudid] += "+" # indique qu'il a servi a compenser - assidu[etudid] = {False: "Non", True: "Oui"}.get(decision["assidu"], "") + code[etud.id] += "+" # indique qu'il a servi a compenser + assidu[etud.id] = {False: "Non", True: "Oui"}.get(decision["assidu"], "") aut_list = sco_parcours_dut.formsemestre_get_autorisation_inscription( - etudid, formsemestre_id + etud.id, formsemestre_id ) - autorisations[etudid] = ", ".join(["S%s" % x["semestre_id"] for x in aut_list]) + autorisations[etud.id] = ", ".join(["S%s" % x["semestre_id"] for x in aut_list]) # parcours: - parcours[etudid] = Se.get_parcours_descr() + parcours[etud.id] = Se.get_parcours_descr() # groupe principal (td) - groupestd[etudid] = "" - for s in etud["sems"]: + groupestd[etud.id] = "" + for s in Se.etud["sems"]: if s["formsemestre_id"] == formsemestre_id: - groupestd[etudid] = etud_groups.get(etudid, {}).get( + groupestd[etud.id] = etud_groups.get(etud.id, {}).get( main_partition_id, "" ) # absences: - e_nbabs, e_nbabsjust = sco_abs.get_abs_count(etudid, sem) - nbabs[etudid] = e_nbabs - nbabsjust[etudid] = e_nbabs - e_nbabsjust + e_nbabs, e_nbabsjust = sco_abs.get_abs_count(etud.id, sem) + nbabs[etud.id] = e_nbabs + nbabsjust[etud.id] = e_nbabs - e_nbabsjust # Codes des UE "semestre précédent": ue_prev_codes = list(prev_moy_ue.keys()) @@ -223,26 +228,26 @@ def feuille_preparation_jury(formsemestre_id): return x i = 1 # numero etudiant - for etudid in etudids: + for etud in etuds: cells = [] - etud = nt.identdict[etudid] cells.append(ws.make_cell(str(i))) if sco_preferences.get_preference("prepa_jury_nip"): - cells.append(ws.make_cell(etud["code_nip"])) + cells.append(ws.make_cell(etud.code_nip)) if sco_preferences.get_preference("prepa_jury_ine"): - cells.append(ws.make_cell(etud["code_ine"])) + cells.append(ws.make_cell(etud.code_ine)) + admission = etud.admission.first() cells += ws.make_row( [ - etudid, - etud["civilite_str"], - sco_etud.format_nom(etud["nom"]), - sco_etud.format_prenom(etud["prenom"]), - etud["date_naissance"], - etud["bac"], - etud["specialite"], - etud["classement"], - parcours[etudid], - groupestd[etudid], + etud.id, + etud.civilite_str, + sco_etud.format_nom(etud.nom), + sco_etud.format_prenom(etud.prenom), + etud.date_naissance, + admission.bac, + admission.specialite, + admission.classement, + parcours[etud.id], + groupestd[etud.id], ] ) co = len(cells) @@ -250,33 +255,35 @@ def feuille_preparation_jury(formsemestre_id): for ue_acro in ue_prev_codes: cells.append( ws.make_cell( - fmt(prev_moy_ue.get(ue_acro, {}).get(etudid, "")), style_note + fmt(prev_moy_ue.get(ue_acro, {}).get(etud.id, "")), style_note ) ) co += 1 cells.append( - ws.make_cell(fmt(prev_moy.get(etudid, "")), style_bold) + ws.make_cell(fmt(prev_moy.get(etud.id, "")), style_bold) ) # moy gen prev cells.append( - ws.make_cell(fmt(prev_code.get(etudid, "")), style_moy) + ws.make_cell(fmt(prev_code.get(etud.id, "")), style_moy) ) # decision prev co += 2 for ue_acro in ue_codes: cells.append( - ws.make_cell(fmt(moy_ue.get(ue_acro, {}).get(etudid, "")), style_note) + ws.make_cell(fmt(moy_ue.get(ue_acro, {}).get(etud.id, "")), style_note) ) co += 1 - cells.append(ws.make_cell(fmt(moy.get(etudid, "")), style_note_bold)) # moy gen + cells.append( + ws.make_cell(fmt(moy.get(etud.id, "")), style_note_bold) + ) # moy gen co += 1 if moy_inter: - cells.append(ws.make_cell(fmt(moy_inter.get(etudid, "")), style_note)) - cells.append(ws.make_cell(str(nbabs.get(etudid, "")), style_center)) - cells.append(ws.make_cell(str(nbabsjust.get(etudid, "")), style_center)) + cells.append(ws.make_cell(fmt(moy_inter.get(etud.id, "")), style_note)) + cells.append(ws.make_cell(str(nbabs.get(etud.id, "")), style_center)) + cells.append(ws.make_cell(str(nbabsjust.get(etud.id, "")), style_center)) if code: - cells.append(ws.make_cell(code.get(etudid, ""), style_moy)) - cells.append(ws.make_cell(autorisations.get(etudid, ""), style_moy)) - # l.append(assidu.get(etudid, '')) + cells.append(ws.make_cell(code.get(etud.id, ""), style_moy)) + cells.append(ws.make_cell(autorisations.get(etud.id, ""), style_moy)) + # l.append(assidu.get(etud.id, '')) ws.append_row(cells) i += 1 # @@ -320,6 +327,7 @@ def feuille_preparation_jury(formsemestre_id): ) ) xls = ws.generate() + flash("Feuille préparation jury générée") return scu.send_file( xls, f"PrepaJury{sn}",