# -*- 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"/>
        &nbsp;<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>&nbsp;')
            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"])