# -*- mode: python -*- # -*- coding: utf-8 -*- ############################################################################## # # Gestion scolarite IUT # # Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Emmanuel Viennet emmanuel.viennet@viennet.net # ############################################################################## """Form. pour inscription rapide des etudiants d'un semestre dans un autre Utilise les autorisations d'inscription délivrées en jury. """ import datetime from operator import itemgetter from flask import url_for, g, request import app.scodoc.notesdb as ndb import app.scodoc.sco_utils as scu from app import db, log from app.comp import res_sem from app.comp.res_compat import NotesTableCompat from app.models import Formation, FormSemestre, GroupDescr, Identite from app.scodoc.gen_tables import GenTable from app.scodoc import html_sco_header from app.scodoc import sco_cache from app.scodoc import codes_cursus from app.scodoc import sco_etud from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_groups from app.scodoc import sco_preferences from app.scodoc import sco_pv_dict from app.scodoc.sco_exceptions import ScoValueError def _list_authorized_etuds_by_sem( formsemestre: FormSemestre, ignore_jury=False ) -> tuple[dict[int, dict], list[dict], dict[int, Identite]]: """Liste des etudiants autorisés à s'inscrire dans sem. delai = nb de jours max entre la date de l'autorisation et celle de debut du semestre cible. ignore_jury: si vrai, considère tous les étudiants comme autorisés, même s'ils n'ont pas de décision de jury. """ src_sems = _list_source_sems(formsemestre) inscrits = list_inscrits(formsemestre.id) r = {} candidats = {} # etudid : etud (tous les etudiants candidats) nb = 0 # debug src_formsemestre: FormSemestre for src_formsemestre in src_sems: if ignore_jury: # liste de tous les inscrits au semestre (sans dems) etud_list = list_inscrits(src_formsemestre.id).values() else: # liste des étudiants autorisés par le jury à s'inscrire ici etud_list = _list_etuds_from_sem(src_formsemestre, formsemestre) liste_filtree = [] for e in etud_list: # Filtre ceux qui se sont déjà inscrit dans un semestre APRES le semestre src auth_used = False # autorisation deja utilisée ? etud = Identite.get_etud(e["etudid"]) for inscription in etud.inscriptions(): if inscription.formsemestre.date_debut >= src_formsemestre.date_fin: auth_used = True if not auth_used: candidats[e["etudid"]] = etud liste_filtree.append(e) nb += 1 r[src_formsemestre.id] = { "etuds": liste_filtree, "infos": { "id": src_formsemestre.id, "title": src_formsemestre.titre_annee(), "title_target": url_for( "notes.formsemestre_status", scodoc_dept=g.scodoc_dept, formsemestre_id=src_formsemestre.id, ), "filename": "etud_autorises", }, } # ajoute attribut inscrit qui indique si l'étudiant est déjà inscrit dans le semestre dest. for e in r[src_formsemestre.id]["etuds"]: e["inscrit"] = e["etudid"] in inscrits # Ajoute liste des etudiants actuellement inscrits for e in inscrits.values(): e["inscrit"] = True r[formsemestre.id] = { "etuds": list(inscrits.values()), "infos": { "id": formsemestre.id, "title": "Semestre cible: " + formsemestre.titre_annee(), "title_target": url_for( "notes.formsemestre_status", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id, ), "comment": " actuellement inscrits dans ce semestre", "help": "Ces étudiants sont actuellement inscrits dans ce semestre. Si vous les décochez, il seront désinscrits.", "filename": "etud_inscrits", }, } return r, inscrits, candidats def list_inscrits(formsemestre_id: int, with_dems=False) -> list[dict]: """Étudiants déjà inscrits à ce semestre { etudid : etud } """ if not with_dems: ins = sco_formsemestre_inscriptions.do_formsemestre_inscription_listinscrits( formsemestre_id ) # optimized else: args = {"formsemestre_id": formsemestre_id} ins = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(args=args) inscr = {} for i in ins: etudid = i["etudid"] inscr[etudid] = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] return inscr def _list_etuds_from_sem(src: FormSemestre, dst: FormSemestre) -> list[dict]: """Liste des étudiants du semestre src qui sont autorisés à passer dans le semestre dst.""" target_semestre_id = dst.semestre_id dpv = sco_pv_dict.dict_pvjury(src.id) if not dpv: return [] etuds = [ x["identite"] for x in dpv["decisions"] if target_semestre_id in [a["semestre_id"] for a in x["autorisations"]] ] return etuds def list_inscrits_date(formsemestre: FormSemestre): """Liste les etudiants inscrits à la date de début de formsemestre dans n'importe quel semestre du même département SAUF formsemestre """ cnx = ndb.GetDBConnexion() cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) cursor.execute( """SELECT ins.etudid FROM notes_formsemestre_inscription ins, notes_formsemestre S WHERE ins.formsemestre_id = S.id AND S.id != %(formsemestre_id)s AND S.date_debut <= %(date_debut_iso)s AND S.date_fin >= %(date_debut_iso)s AND S.dept_id = %(dept_id)s """, { "formsemestre_id": formsemestre.id, "date_debut_iso": formsemestre.date_debut.isoformat(), "dept_id": formsemestre.dept_id, }, ) return [x[0] for x in cursor.fetchall()] def do_inscrit( formsemestre: FormSemestre, etudids, inscrit_groupes=False, inscrit_parcours=False ): """Inscrit ces etudiants dans ce semestre (la liste doit avoir été vérifiée au préalable) En option: - Si inscrit_groupes, inscrit aux mêmes groupes que dans le semestre origine (toutes partitions, y compris parcours) - Si inscrit_parcours, inscrit au même groupe de parcours (mais ignore les autres partitions) (si les deux sont vrais, inscrit_parcours n'a pas d'effet) """ # TODO à ré-écrire pour utiliser les modèles, notamment GroupDescr formsemestre.setup_parcours_groups() log(f"do_inscrit (inscrit_groupes={inscrit_groupes}): {etudids}") for etudid in etudids: sco_formsemestre_inscriptions.do_formsemestre_inscription_with_modules( formsemestre.id, etudid, etat=scu.INSCRIT, method="formsemestre_inscr_passage", ) if inscrit_groupes or inscrit_parcours: # Inscription dans les mêmes groupes que ceux du semestre d'origine, # s'ils existent. # (mise en correspondance à partir du nom du groupe, sans tenir compte # du nom de la partition: évidemment, cela ne marche pas si on a les # même noms de groupes dans des partitions différentes) etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] # recherche le semestre origine (il serait plus propre de l'avoir conservé!) if len(etud["sems"]) < 2: continue prev_formsemestre = etud["sems"][1] sco_groups.etud_add_group_infos( etud, prev_formsemestre["formsemestre_id"] if prev_formsemestre else None, ) cursem_groups_by_name = { g["group_name"]: g for g in sco_groups.get_sem_groups(formsemestre.id) if g["group_name"] } # forme la liste des groupes présents dans les deux semestres: partition_groups = [] # [ partition+group ] (ds nouveau sem.) for partition_id in etud["partitions"]: prev_group_name = etud["partitions"][partition_id]["group_name"] if prev_group_name in cursem_groups_by_name: new_group = cursem_groups_by_name[prev_group_name] partition_groups.append(new_group) # Inscrit aux groupes for partition_group in partition_groups: group: GroupDescr = db.session.get( GroupDescr, partition_group["group_id"] ) if inscrit_groupes or ( group.partition.partition_name == scu.PARTITION_PARCOURS and inscrit_parcours ): sco_groups.change_etud_group_in_partition(etudid, group) def do_desinscrit( formsemestre: FormSemestre, etudids: list[int], check_has_dec_jury=True ): "désinscrit les étudiants indiqués du formsemestre" log(f"do_desinscrit: {etudids}") for etudid in etudids: sco_formsemestre_inscriptions.do_formsemestre_desinscription( etudid, formsemestre.id, check_has_dec_jury=check_has_dec_jury ) def _list_source_sems(formsemestre: FormSemestre) -> list[FormSemestre]: """Liste des semestres sources formsemestre est le semestre destination """ # liste des semestres du même type de cursus terminant # pas trop loin de la date de début du semestre destination date_fin_min = formsemestre.date_debut - datetime.timedelta(days=275) date_fin_max = formsemestre.date_debut + datetime.timedelta(days=45) return ( FormSemestre.query.filter( FormSemestre.dept_id == formsemestre.dept_id, # saute le semestre destination: FormSemestre.id != formsemestre.id, # et les semestres de formations speciales (monosemestres): FormSemestre.semestre_id != codes_cursus.NO_SEMESTRE_ID, # semestre pas trop dans le futur FormSemestre.date_fin <= date_fin_max, # ni trop loin dans le passé FormSemestre.date_fin >= date_fin_min, ) .join(Formation) .filter_by(type_parcours=formsemestre.formation.type_parcours) ).all() # view, GET, POST def formsemestre_inscr_passage( formsemestre_id, etuds: str | list[int] | list[str] | int | None = None, inscrit_groupes=False, inscrit_parcours=False, submitted=False, dialog_confirmed=False, ignore_jury=False, ) -> str: """Page Form. pour inscription des etudiants d'un semestre dans un autre (donné par formsemestre_id). Permet de selectionner parmi les etudiants autorisés à s'inscrire. Principe: - trouver liste d'etud, par semestre - afficher chaque semestre "boites" avec cases à cocher - si l'étudiant est déjà inscrit, le signaler (gras, nom de groupes): il peut être désinscrit - on peut choisir les groupes TD, TP, TA - seuls les étudiants non inscrits changent (de groupe) - les étudiants inscrit qui se trouvent décochés sont désinscrits - Confirmation: indiquer les étudiants inscrits et ceux désinscrits, le total courant. """ formsemestre = FormSemestre.get_formsemestre(formsemestre_id) inscrit_groupes = int(inscrit_groupes) inscrit_parcours = int(inscrit_parcours) ignore_jury = int(ignore_jury) # -- check lock if not formsemestre.etat: raise ScoValueError("opération impossible: semestre verrouille") H = [ html_sco_header.sco_header( page_title="Passage des étudiants", init_qtip=True, javascripts=["js/etud_info.js"], ) ] footer = html_sco_header.sco_footer() etuds = [] if etuds is None else etuds if isinstance(etuds, str): # string, vient du form de confirmation etuds = [int(x) for x in etuds.split(",") if x] elif isinstance(etuds, int): etuds = [etuds] elif etuds and isinstance(etuds[0], str): etuds = [int(x) for x in etuds] auth_etuds_by_sem, inscrits, candidats = _list_authorized_etuds_by_sem( formsemestre, ignore_jury=ignore_jury ) etuds_set = set(etuds) candidats_set = set(candidats) inscrits_set = set(inscrits) candidats_non_inscrits = candidats_set - inscrits_set inscrits_ailleurs = set(list_inscrits_date(formsemestre)) def set_to_sorted_etud_list(etudset) -> list[Identite]: etuds = [candidats[etudid] for etudid in etudset] etuds.sort(key=lambda e: e.sort_key) return etuds if submitted: a_inscrire = etuds_set.intersection(candidats_set) - inscrits_set a_desinscrire = inscrits_set - etuds_set else: a_inscrire = a_desinscrire = [] if not submitted: H += _build_page( formsemestre, auth_etuds_by_sem, inscrits, candidats_non_inscrits, inscrits_ailleurs, inscrit_groupes=inscrit_groupes, inscrit_parcours=inscrit_parcours, ignore_jury=ignore_jury, ) else: if not dialog_confirmed: # Confirmation if a_inscrire: H.append("<h3>Étudiants à inscrire</h3><ol>") for etud in set_to_sorted_etud_list(a_inscrire): H.append(f"<li>{etud.nomprenom}</li>") H.append("</ol>") a_inscrire_en_double = inscrits_ailleurs.intersection(a_inscrire) if a_inscrire_en_double: H.append("<h3>dont étudiants déjà inscrits:</h3><ul>") for etud in set_to_sorted_etud_list(a_inscrire_en_double): H.append(f'<li class="inscrit-ailleurs">{etud.nomprenom}</li>') H.append("</ul>") if a_desinscrire: H.append("<h3>Étudiants à désinscrire</h3><ol>") a_desinscrire_ident = sorted( (Identite.query.get(eid) for eid in a_desinscrire), key=lambda x: x.sort_key, ) for etud in a_desinscrire_ident: H.append(f'<li class="desinscription">{etud.nomprenom}</li>') H.append("</ol>") todo = a_inscrire or a_desinscrire if not todo: H.append("""<h3>Il n'y a rien à modifier !</h3>""") H.append( scu.confirm_dialog( dest_url=( "formsemestre_inscr_passage" if todo else "formsemestre_status" ), message="<p>Confirmer ?</p>" if todo else "", add_headers=False, cancel_url="formsemestre_inscr_passage?formsemestre_id=" + str(formsemestre_id), OK="Effectuer l'opération" if todo else "", parameters={ "formsemestre_id": formsemestre_id, "etuds": ",".join([str(x) for x in etuds]), "inscrit_groupes": inscrit_groupes, "inscrit_parcours": inscrit_parcours, "ignore_jury": ignore_jury, "submitted": 1, }, ) ) else: # check decisions jury ici pour éviter de recontruire le cache # après chaque desinscription sco_formsemestre_inscriptions.check_if_has_decision_jury( formsemestre, a_desinscrire ) # check decisions jury ici pour éviter de recontruire le cache # après chaque desinscription sco_formsemestre_inscriptions.check_if_has_decision_jury( formsemestre, a_desinscrire ) with sco_cache.DeferredSemCacheManager(): # Inscription des étudiants au nouveau semestre: do_inscrit( formsemestre, a_inscrire, inscrit_groupes=inscrit_groupes, inscrit_parcours=inscrit_parcours, ) # Désinscriptions: do_desinscrit(formsemestre, a_desinscrire, check_has_dec_jury=False) H.append( f"""<h3>Opération effectuée</h3> <ul> <li><a class="stdlink" href="{ url_for("notes.formsemestre_inscr_passage", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id) }">Continuer les inscriptions</a> </li> <li><a class="stdlink" href="{ url_for("notes.formsemestre_status", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id) }">Tableau de bord du semestre</a> </li>""" ) partition = sco_groups.formsemestre_get_main_partition(formsemestre_id) if ( partition["partition_id"] != sco_groups.formsemestre_get_main_partition(formsemestre_id)[ "partition_id" ] ): # il y a au moins une vraie partition H.append( f"""<li><a class="stdlink" href="{ url_for("scolar.partition_editor", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id) }">Répartir les groupes de {partition["partition_name"]}</a></li> """ ) # H.append(footer) return "\n".join(H) def _build_page( formsemestre: FormSemestre, auth_etuds_by_sem, inscrits, candidats_non_inscrits, inscrits_ailleurs, inscrit_groupes=False, inscrit_parcours=False, ignore_jury=False, ): inscrit_groupes = int(inscrit_groupes) inscrit_parcours = int(inscrit_parcours) ignore_jury = int(ignore_jury) if inscrit_groupes: inscrit_groupes_checked = " checked" else: inscrit_groupes_checked = "" if inscrit_parcours: inscrit_parcours_checked = " checked" else: inscrit_parcours_checked = "" if ignore_jury: ignore_jury_checked = " checked" else: ignore_jury_checked = "" H = [ html_sco_header.html_sem_header( "Passages dans le semestre", with_page_header=False ), f"""<form name="f" method="post" action="{request.base_url}"> <input type="hidden" name="formsemestre_id" value="{formsemestre.id}"/> <input type="submit" name="submitted" value="Appliquer les modifications"/> <a href="#help">aide</a> <input name="inscrit_groupes" type="checkbox" value="1" {inscrit_groupes_checked}>inscrire aux mêmes groupes (y compris parcours)</input> <input name="inscrit_parcours" type="checkbox" value="1" {inscrit_parcours_checked}>inscrire aux mêmes parcours</input> <input name="ignore_jury" type="checkbox" value="1" onchange="document.f.submit()" {ignore_jury_checked}>inclure tous les étudiants (même sans décision de jury)</input> <div class="pas_recap">Actuellement <span id="nbinscrits">{len(inscrits)}</span> inscrits et {len(candidats_non_inscrits)} candidats supplémentaires. </div> <div>{scu.EMO_WARNING} <em>Seuls les semestres dont la date de fin est proche de la date de début de ce semestre ({formsemestre.date_debut.strftime(scu.DATE_FMT)}) sont pris en compte.</em> </div> {etuds_select_boxes(auth_etuds_by_sem, inscrits_ailleurs)} <input type="submit" name="submitted" value="Appliquer les modifications"/> {formsemestre_inscr_passage_help(formsemestre)} </form> """, ] # Semestres sans étudiants autorisés empty_sems = [] for formsemestre_id in auth_etuds_by_sem.keys(): if not auth_etuds_by_sem[formsemestre_id]["etuds"]: empty_sems.append(auth_etuds_by_sem[formsemestre_id]["infos"]) if empty_sems: H.append( """<div class="pas_empty_sems"><h3>Autres semestres sans candidats :</h3><ul>""" ) for infos in empty_sems: H.append( """<li><a class="stdlink" href="%(title_target)s">%(title)s</a></li>""" % infos ) H.append("""</ul></div>""") return H def formsemestre_inscr_passage_help(formsemestre: FormSemestre): "texte d'aide en bas de la page passage des étudiants" return f"""<div class="pas_help"><h3><a name="help">Explications</a></h3> <p>Cette page permet d'inscrire des étudiants dans le semestre destination <a class="stdlink" href="{ url_for("notes.formsemestre_status", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id ) }">{formsemestre.titre_annee()}</a>, et d'en désincrire si besoin. </p> <p>Les étudiants sont groupés par semestre d'origine. Ceux qui sont en caractères <span class="deja-inscrit">gras</span> sont déjà inscrits dans le semestre destination. Ceux qui sont en <span class="inscrit-ailleurs">gras et en rouge</span> sont inscrits dans un <em>autre</em> semestre. </p> <p>Au départ, les étudiants déjà inscrits sont sélectionnés; vous pouvez ajouter d'autres étudiants à inscrire dans le semestre destination. </p> <p>Si vous dé-selectionnez un étudiant déjà inscrit (en gras), il sera désinscrit. </p> <p>Le bouton <em>inscrire aux mêmes groupes</em> ne prend en compte que les groupes qui existent dans les deux semestres: pensez à créer les partitions et groupes que vous souhaitez conserver <b>avant</b> d'inscrire les étudiants. </p> <p>Les parcours de BUT sont gérés comme des groupes de la partition parcours: si on conserve les groupes, on conserve les parcours (là aussi, pensez à les cocher dans <a class="stdlink" href="{ url_for("notes.formsemestre_editwithmodules", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id ) }">modifier le semestre</a> avant de faire passer les étudiants). </a> <p class="help">Aucune action ne sera effectuée si vous n'appuyez pas sur le bouton "Appliquer les modifications" ! </p> </div> """ def etuds_select_boxes( auth_etuds_by_cat, inscrits_ailleurs={}, sel_inscrits=True, show_empty_boxes=False, export_cat_xls=None, base_url="", read_only=False, ): """Boites pour selection étudiants par catégorie auth_etuds_by_cat = { category : { 'info' : {}, 'etuds' : ... } inscrits_ailleurs = sel_inscrits= export_cat_xls = """ if export_cat_xls: return etuds_select_box_xls(auth_etuds_by_cat[export_cat_xls]) H = [ """<script type="text/javascript"> function sem_select(formsemestre_id, state) { var elems = document.getElementById(formsemestre_id).getElementsByTagName("input"); for (var i =0; i < elems.length; i++) { elems[i].checked=state; } } function sem_select_inscrits(formsemestre_id) { var elems = document.getElementById(formsemestre_id).getElementsByTagName("input"); for (var i =0; i < elems.length; i++) { if (elems[i].parentNode.className.indexOf('inscrit') >= 0) { elems[i].checked=true; } else { elems[i].checked=false; } } } </script> <div class="etuds_select_boxes">""" ] # " # Élimine les boites vides: auth_etuds_by_cat = { k: auth_etuds_by_cat[k] for k in auth_etuds_by_cat if auth_etuds_by_cat[k]["etuds"] } for src_cat in auth_etuds_by_cat.keys(): infos = auth_etuds_by_cat[src_cat]["infos"] infos["comment"] = infos.get("comment", "") # commentaire dans sous-titre boite help = infos.get("help", "") etuds = auth_etuds_by_cat[src_cat]["etuds"] etuds.sort(key=itemgetter("nom")) with_checkbox = (not read_only) and auth_etuds_by_cat[src_cat]["infos"].get( "with_checkbox", True ) checkbox_name = auth_etuds_by_cat[src_cat]["infos"].get( "checkbox_name", "etuds" ) etud_key = auth_etuds_by_cat[src_cat]["infos"].get("etud_key", "etudid") if etuds or show_empty_boxes: infos["nbetuds"] = len(etuds) H.append( """<div class="pas_sembox" id="%(id)s"> <div class="pas_sembox_title"><a href="%(title_target)s" """ % infos ) if help: # bubble H.append('title="%s"' % help) H.append( """>%(title)s</a></div> <div class="pas_sembox_subtitle">(%(nbetuds)d étudiants%(comment)s)""" % infos ) if with_checkbox: H.append( """ (Select. <a href="#" class="stdlink" onclick="sem_select('%(id)s', true);">tous</a> <a href="#" class="stdlink" onclick="sem_select('%(id)s', false );">aucun</a>""" # " % infos ) if sel_inscrits: H.append( """<a href="#" class="stdlink" onclick="sem_select_inscrits('%(id)s');">inscrits</a>""" % infos ) if with_checkbox or sel_inscrits: H.append(")") if base_url and etuds: url = scu.build_url_query(base_url, export_cat_xls=src_cat) H.append(f'<a href="{url}">{scu.ICON_XLS}</a> ') H.append("</div>") for etud in etuds: if etud.get("inscrit", False): c = " deja-inscrit" checked = 'checked="checked"' else: checked = "" if etud["etudid"] in inscrits_ailleurs: c = " inscrit-ailleurs" else: c = "" sco_etud.format_etud_ident(etud) if etud["etudid"]: elink = f"""<a id="{etud['etudid']}" class="discretelink etudinfo {c}" href="{ url_for( 'scolar.fiche_etud', scodoc_dept=g.scodoc_dept, etudid=etud['etudid'], ) }">{etud['nomprenom']}</a> """ else: # ce n'est pas un etudiant ScoDoc elink = etud["nomprenom"] if etud.get("datefinalisationinscription", None): elink += ( '<span class="finalisationinscription">' + " : inscription finalisée le " + etud["datefinalisationinscription"].strftime(scu.DATE_FMT) + "</span>" ) if not etud.get("paiementinscription", True): elink += '<span class="paspaye"> (non paiement)</span>' H.append("""<div class="pas_etud%s">""" % c) if "etape" in etud: etape_str = etud["etape"] or "" else: etape_str = "" H.append("""<span class="sp_etape">%s</span>""" % etape_str) if with_checkbox: H.append( """<input type="checkbox" name="%s:list" value="%s" %s>""" % (checkbox_name, etud[etud_key], checked) ) H.append(elink) if with_checkbox: H.append("""</input>""") H.append("</div>") H.append("</div>") H.append("</div>") return "\n".join(H) def etuds_select_box_xls(src_cat): "export a box to excel" etuds = src_cat["etuds"] columns_ids = ["etudid", "civilite_str", "nom", "prenom", "etape"] titles = {x: x for x in columns_ids} # Ajoute colonne paiement inscription columns_ids.append("paiementinscription_str") titles["paiementinscription_str"] = "paiement inscription" for e in etuds: if not e.get("paiementinscription", True): e["paiementinscription_str"] = "NON" else: e["paiementinscription_str"] = "-" tab = GenTable( titles=titles, columns_ids=columns_ids, rows=etuds, caption="%(title)s. %(help)s" % src_cat["infos"], preferences=sco_preferences.SemPreferences(), ) return tab.excel() # tab.make_page(filename=src_cat["infos"]["filename"])