# -*- 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
#
##############################################################################

"""Affichage étudiants d'un ou plusieurs groupes
   sous forme: de liste html (table exportable), de trombinoscope (exportable en pdf)
"""

# Re-ecriture en 2014 (re-organisation de l'interface, modernisation du code)

import datetime
from urllib.parse import parse_qs


from flask import url_for, g, request
from flask_login import current_user

from app import db
from app.models import FormSemestre
import app.scodoc.sco_utils as scu
from app.scodoc import html_sco_header
from app.scodoc import sco_excel
from app.scodoc import sco_formsemestre
from app.scodoc import sco_groups
from app.scodoc import sco_moduleimpl
from app.scodoc import sco_cursus
from app.scodoc import sco_portal_apogee
from app.scodoc import sco_preferences
from app.scodoc import sco_etud
from app.scodoc.sco_etud import etud_sort_key
from app.scodoc.gen_tables import GenTable
from app.scodoc.sco_exceptions import ScoValueError, ScoPermissionDenied
from app.scodoc.sco_permissions import Permission

JAVASCRIPTS = html_sco_header.BOOTSTRAP_MULTISELECT_JS + [
    "js/etud_info.js",
    "js/groups_view.js",
]

CSSSTYLES = html_sco_header.BOOTSTRAP_MULTISELECT_CSS


# view:
def groups_view(
    group_ids=(),
    fmt="html",
    with_codes=0,
    etat=None,
    with_paiement=0,
    with_archives=0,
    with_annotations=0,
    with_bourse=0,
    formsemestre_id=None,
):
    """Affichage des étudiants des groupes indiqués
    group_ids: liste de group_id
    fmt: csv, json, xml, xls, allxls, xlsappel, moodlecsv, pdf

    Options pour listes:
    with_paiement: si vrai, ajoute colonnes infos paiement droits
                    et finalisation inscription (lent car interrogation portail)
    with_archives: ajoute colonne avec noms fichiers archivés

    formsemestre_id est utilisé si aucun groupe selectionné pour construire la liste des groupes.
    """
    # Informations sur les groupes à afficher:
    groups_infos = DisplayedGroupsInfos(
        group_ids,
        formsemestre_id=formsemestre_id,
        etat=etat,
        select_all_when_unspecified=True,
    )
    # Formats spéciaux: download direct
    if fmt != "html":
        return groups_table(
            groups_infos=groups_infos,
            fmt=fmt,
            with_codes=with_codes,
            etat=etat,
            with_paiement=with_paiement,
            with_archives=with_archives,
            with_annotations=with_annotations,
            with_bourse=with_bourse,
        )

    # Note: le formulaire est soumis a chaque modif des groupes
    # on pourrait faire comme pour le form de saisie des notes. Il faudrait pour cela:
    #  - charger tous les etudiants au debut, quels que soient les groupes selectionnés
    #  - ajouter du JS pour modifier les liens (arguments group_ids) quand le menu change

    return f"""
    { html_sco_header.sco_header(
            javascripts=JAVASCRIPTS,
            cssstyles=CSSSTYLES,
            init_qtip=True,
        )
    }
    <style>
    div.multiselect-container.dropdown-menu {{
        min-width: 180px;
    }}
    span.warning_unauthorized {{
        color: pink;
        font-style: italic;
        margin-left: 12px;
    }}
    </style>
    <div id="group-tabs">
        <!-- Menu choix groupe -->
        {form_groups_choice(groups_infos, submit_on_change=True)}
        <ul class="nav nav-tabs">
            <li class="active"><a href="#tab-listes" data-toggle="tab">Listes</a></li>
            <li><a href="#tab-photos" data-toggle="tab">Photos</a></li>
            <li><a href="#tab-abs" data-toggle="tab">Absences et feuilles...</a></li>
        </ul>
    </div>
    <!-- Tab panes -->
    <div class="tab-content">
        <div class="tab-pane active" id="tab-listes">
            {
                groups_table(
                    groups_infos=groups_infos,
                    fmt=fmt,
                    with_codes=with_codes,
                    etat=etat,
                    with_paiement=with_paiement,
                    with_archives=with_archives,
                    with_annotations=with_annotations,
                    with_bourse=with_bourse,
                )
            }
        </div>
        <div class="tab-pane" id="tab-photos">
            { tab_photos_html(groups_infos, etat=etat) }
        </div>
        <div class="tab-pane" id="tab-abs">
            { tab_absences_html(groups_infos, etat=etat) }
        </div>
    </div>
    { html_sco_header.sco_footer() }
    """


def form_groups_choice(
    groups_infos,
    with_selectall_butt=False,
    with_deselect_butt=False,
    submit_on_change=False,
    default_deselect_others=True,
):
    """form pour selection groupes
    group_ids est la liste des groupes actuellement sélectionnés
    et doit comporter au moins un élément, sauf si formsemestre_id est spécifié.
    (utilisé pour retrouver le semestre et proposer la liste des autres groupes)

    Si submit_on_change, soumet (recharge la page) à chaque modif.
    Si default_deselect_others, désélectionne le groupe "Tous" quand on sélectionne un autre groupe.

    Ces deux options ajoutent des classes utilisées en JS pour la gestion du formulaire.
    """
    default_group_id = sco_groups.get_default_group(groups_infos.formsemestre_id)

    H = [
        f"""
    <form id="group_selector" method="get">
        <input type="hidden" name="formsemestre_id" id="formsemestre_id"
            value="{groups_infos.formsemestre_id}"/>
        <input type="hidden" name="default_group_id" id="default_group_id"
            value="{default_group_id}"/>
    Groupes:
    {
        menu_groups_choice(
            groups_infos,
            submit_on_change=submit_on_change,
            default_deselect_others=default_deselect_others,
        )
    }
    """
    ]
    if with_selectall_butt:
        H.append(
            """<input type="button" value="sélectionner tous" onmousedown="select_groupe_tous();"/>"""
        )
    if with_deselect_butt:
        H.append(
            """<input type="button" value="ne pas filtrer" onmousedown="remove_group_filter();"/>"""
        )
    H.append("</form>")

    return "\n".join(H)


def menu_groups_choice(
    groups_infos, submit_on_change=False, default_deselect_others=True
):
    """menu pour selection groupes
    group_ids est la liste des groupes actuellement sélectionnés
    et doit comporter au moins un élément, sauf si formsemestre_id est spécifié.
    (utilisé pour retrouver le semestre et proposer la liste des autres groupes)
    """
    default_group_id = sco_groups.get_default_group(groups_infos.formsemestre_id)
    n_members = len(sco_groups.get_group_members(default_group_id))

    H = [
        f"""<select name="group_ids" id="group_ids_sel"
            class="multiselect
                {'submit_on_change' if submit_on_change else ''}
                {'default_deselect_others' if default_deselect_others else ''}
                "
            multiple="multiple">
                <option class="default_group"
                    value="{default_group_id}"
                    {'selected' if default_group_id in groups_infos.group_ids else ''}
                >Tous ({n_members})</option>
        """
    ]

    for partition in groups_infos.partitions:
        H.append('<optgroup label="%s">' % partition["partition_name"])
        # Les groupes dans cette partition:
        for g in sco_groups.get_partition_groups(partition):
            if g["group_id"] in groups_infos.group_ids:
                selected = "selected"
            else:
                selected = ""
            if g["group_name"]:
                n_members = len(sco_groups.get_group_members(g["group_id"]))
                H.append(
                    '<option value="%s" %s>%s (%s)</option>'
                    % (g["group_id"], selected, g["group_name"], n_members)
                )
        H.append("</optgroup>")
    H.append("</select> ")
    return "\n".join(H)


def menu_group_choice(group_id=None, formsemestre_id=None):
    """Un bête menu pour choisir un seul groupe
    group_id est le groupe actuellement sélectionné.
    Si aucun groupe selectionné, utilise formsemestre_id pour lister les groupes.
    """
    if group_id:
        group = sco_groups.get_group(group_id)
        formsemestre_id = group["formsemestre_id"]
    elif not formsemestre_id:
        raise ValueError("missing formsemestre_id")
    H = [
        """
    <select id="group_selector_u" name="group_id" onchange="reload_selected_group();">
    """
    ]
    if not group_id:
        H.append('<option value="">choisir...</option>')
    for partition in sco_groups.get_partitions_list(formsemestre_id):
        if partition["partition_name"]:
            H.append('<optgroup label="%s">' % partition["partition_name"])
        groups = sco_groups.get_partition_groups(partition)
        for group in groups:
            if group["group_id"] == group_id:
                selected = "selected"
            else:
                selected = ""
            name = group["group_name"] or "Tous"
            n_members = len(sco_groups.get_group_members(group["group_id"]))
            H.append(
                '<option value="%s" %s>%s (%s)</option>'
                % (group["group_id"], selected, name, n_members)
            )
        if partition["partition_name"]:
            H.append("</optgroup>")
    H.append(
        """</select>
    <script>
function reload_selected_group() {
var url = $.url();
var group_id = $("#group_selector_u").val();
if (group_id) {
  url.param()['group_id'] = group_id;
  var query_string = $.param(url.param(), traditional=true );
  window.location = url.attr('base') + url.attr('path') + '?' + query_string;
}
}
    </script>
    """
    )
    return "\n".join(H)


class DisplayedGroupsInfos:
    """Container with attributes describing groups to display in the page
    .groups_query_args : 'group_ids=xxx&group_ids=yyy'
    .base_url : url de la requete, avec les groupes, sans les autres paramètres
    .formsemestre_id : semestre "principal" (en fait celui du 1er groupe de la liste)
    .members :
    .groups_titles

    etat: filtrage selon l'état de l'inscription
    select_all_when_unspecified : sélectionne le groupe "tous" si aucun groupe n'est indiqué.
    empty_list_select_all: si vrai (défaut) on sélectionne le groupe tous si aucun groupe indiqué.
    """

    def __init__(
        self,
        group_ids=(),  # groupes specifies dans l'URL, ou un seul int
        formsemestre_id: int | None = None,
        etat: str | None = None,
        select_all_when_unspecified=False,
        empty_list_select_all=True,
        moduleimpl_id=None,  # used to find formsemestre when unspecified
    ):
        if isinstance(group_ids, int):
            if group_ids:
                group_ids = [group_ids]  # cas ou un seul parametre, pas de liste
        else:
            try:
                group_ids = [int(g) for g in group_ids]
            except ValueError as exc:
                raise ScoValueError(
                    "identifiant de groupe invalide (mettre à jour vos bookmarks ?)"
                ) from exc
        if not formsemestre_id and moduleimpl_id:
            mods = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)
            if len(mods) != 1:
                raise ValueError("invalid moduleimpl_id")
            formsemestre_id = mods[0]["formsemestre_id"]

        if not group_ids:  # appel sans groupe (eg page accueil)
            if not formsemestre_id:
                raise ValueError("missing parameter formsemestre_id or group_ids")
            if empty_list_select_all:
                if select_all_when_unspecified:
                    group_ids = [
                        sco_groups.get_default_group(
                            formsemestre_id, fix_if_missing=True
                        )
                    ]
                else:
                    # selectionne le premier groupe trouvé, s'il y en a un
                    partition = sco_groups.get_partitions_list(
                        formsemestre_id, with_default=True
                    )[0]
                    groups = sco_groups.get_partition_groups(partition)
                    if groups:
                        group_ids = [groups[0]["group_id"]]
                    else:
                        group_ids = [sco_groups.get_default_group(formsemestre_id)]
            else:
                group_ids = []

        gq = []
        for group_id in group_ids:
            gq.append("group_ids=" + str(group_id))
        self.groups_query_args = "&".join(gq)
        self.base_url = request.base_url + "?" + self.groups_query_args
        self.group_ids = group_ids
        self.groups = []
        groups_titles = []
        self.members = []
        self.tous_les_etuds_du_sem = (
            False  # affiche tous les etuds du semestre ? (si un seul semestre)
        )
        self.sems = {}  # formsemestre_id : sem
        self.formsemestre = None
        self.formsemestre_id = formsemestre_id
        self.nbdem = 0  # nombre d'étudiants démissionnaires en tout
        sem = None
        selected_partitions = set()
        for group_id in group_ids:
            group_members, group, group_tit, sem, nbdem = sco_groups.get_group_infos(
                group_id, etat=etat
            )
            self.groups.append(group)
            self.nbdem += nbdem
            self.sems[sem["formsemestre_id"]] = sem
            if not self.formsemestre_id:
                self.formsemestre_id = sem["formsemestre_id"]
                self.formsemestre = sem
            self.members.extend(group_members)
            groups_titles.append(group_tit)
            if group["partition_name"] is None:
                self.tous_les_etuds_du_sem = True
            else:
                # liste les partitions explicitement sélectionnés (= des groupes de group_ids)
                selected_partitions.add((group["numero"] or 0, group["partition_id"]))

        self.selected_partitions = [
            x[1] for x in sorted(list(selected_partitions))
        ]  # -> [ partition_id ]

        if not self.formsemestre:  # aucun groupe selectionne
            self.formsemestre = sco_formsemestre.get_formsemestre(formsemestre_id)
            if formsemestre_id not in self.sems:
                self.sems[formsemestre_id] = self.formsemestre
        self.sortuniq()

        if len(self.sems) > 1:
            self.tous_les_etuds_du_sem = False  # plusieurs semestres
        if self.tous_les_etuds_du_sem:
            if sem and sem["semestre_id"] >= 0:
                self.groups_titles = "S%d" % sem["semestre_id"]
            else:
                self.groups_titles = "tous"
            self.groups_filename = self.groups_titles
        else:
            self.groups_titles = ", ".join(groups_titles)
            self.groups_filename = "_".join(groups_titles).replace(" ", "_")
            # Sanitize filename:
            self.groups_filename = scu.make_filename(self.groups_filename)

        # colonnes pour affichages nom des groupes:
        # gère le cas où les étudiants appartiennent à des semestres différents
        self.partitions = []  # les partitions, sans celle par defaut
        for formsemestre_id in self.sems:
            for partition in sco_groups.get_partitions_list(formsemestre_id):
                if partition["partition_name"]:
                    self.partitions.append(partition)

    def sortuniq(self):
        "Trie les étudiants (de plusieurs groupes) et élimine les doublons"
        if (len(self.group_ids) <= 1) or len(self.members) <= 1:
            return  # on suppose que les etudiants d'un groupe sont deja triés
        # tri selon nom_usuel ou nom, sans accents
        self.members.sort(key=etud_sort_key)
        to_remove = []
        T = self.members
        for i in range(len(T) - 1, 0, -1):
            if T[i - 1]["etudid"] == T[i]["etudid"]:
                to_remove.append(i)
        for i in to_remove:
            del T[i]

    def get_form_elem(self):
        """html hidden input with groups"""
        H = []
        for group_id in self.group_ids:
            H.append(f'<input type="hidden" name="group_ids" value="{group_id}"/>')
        return "\n".join(H)

    def get_formsemestre(self) -> FormSemestre:
        return (
            db.session.get(FormSemestre, self.formsemestre_id)
            if self.formsemestre_id
            else None
        )


# Ancien ZScolar.group_list renommé ici en group_table
def groups_table(
    groups_infos: DisplayedGroupsInfos = None,
    with_codes=0,
    etat=None,
    fmt="html",
    with_paiement=0,  # si vrai, ajoute colonnes infos paiement droits et finalisation inscription (lent car interrogation portail)
    with_archives=0,  # ajoute colonne avec noms fichiers archivés
    with_annotations=0,
    with_bourse=0,
):
    """liste etudiants inscrits dans ce semestre
    fmt: csv, json, xml, xls, allxls, xlsappel, moodlecsv, pdf
    Si with_codes, ajoute 4 colonnes avec les codes etudid, NIP, INE et etape
    """
    from app.scodoc import sco_report

    can_view_etud_data = int(current_user.has_permission(Permission.ViewEtudData))
    with_codes = int(with_codes)
    with_paiement = int(with_paiement) and can_view_etud_data
    with_archives = int(with_archives) and can_view_etud_data
    with_annotations = int(with_annotations) and can_view_etud_data
    with_bourse = int(with_bourse) and can_view_etud_data

    base_url_np = groups_infos.base_url + f"&with_codes={with_codes}"
    base_url = (
        base_url_np
        + f"""&with_paiement={with_paiement}&with_archives={
            with_archives}&with_annotations={with_annotations
            }&with_bourse={with_bourse}"""
    )
    #
    columns_ids = ["civilite_str", "nom_disp", "prenom"]  # colonnes a inclure
    titles = {
        "civilite_str": "Civ.",
        "nom_disp": "Nom",
        "prenom": "Prénom",
        "email": "Mail",
        "emailperso": "Personnel",
        "etat": "Etat",
        "etudid": "etudid",
        "code_nip": "code_nip",
        "code_ine": "code_ine",
        "datefinalisationinscription_str": "Finalisation inscr.",
        "paiementinscription_str": "Paiement",
        "etudarchive": "Fichiers",
        "annotations_str": "Annotations",
        "bourse_str": "Boursier",
        "etape": "Etape",
        "semestre_groupe": "Semestre-Groupe",  # pour Moodle
        "annee": "annee_admission",
    }

    # ajoute colonnes pour groupes
    columns_ids.extend([p["partition_id"] for p in groups_infos.partitions])
    titles.update(
        dict(
            [(p["partition_id"], p["partition_name"]) for p in groups_infos.partitions]
        )
    )
    partitions_name = {
        p["partition_id"]: p["partition_name"] for p in groups_infos.partitions
    }

    if fmt != "html":  # ne mentionne l'état que en Excel (style en html)
        columns_ids.append("etat")
    columns_ids.append("email")
    if can_view_etud_data:
        columns_ids.append("emailperso")

    if fmt == "moodlecsv":
        columns_ids = ["email", "semestre_groupe"]

    if with_codes:
        columns_ids += ["etape", "etudid", "code_nip", "code_ine"]
    if with_paiement:
        columns_ids += ["datefinalisationinscription_str", "paiementinscription_str"]
    if with_paiement:  #  or with_codes:
        sco_portal_apogee.check_paiement_etuds(groups_infos.members)
    if with_archives:
        from app.scodoc import sco_archives_etud

        sco_archives_etud.add_archives_info_to_etud_list(groups_infos.members)
        columns_ids += ["etudarchive"]
    if with_annotations:
        sco_etud.add_annotations_to_etud_list(groups_infos.members)
        columns_ids += ["annotations_str"]
    if with_bourse:
        columns_ids += ["bourse_str"]
    moodle_sem_name = groups_infos.formsemestre["session_id"]
    moodle_groupenames = set()
    # ajoute liens
    for etud in groups_infos.members:
        if etud["email"]:
            etud["_email_target"] = "mailto:" + etud["email"]
        else:
            etud["_email_target"] = ""
        if etud["emailperso"]:
            etud["_emailperso_target"] = "mailto:" + etud["emailperso"]
        else:
            etud["_emailperso_target"] = ""
        fiche_url = url_for(
            "scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etud["etudid"]
        )
        etud["_nom_disp_target"] = fiche_url
        etud["_nom_disp_order"] = etud_sort_key(etud)
        etud["_prenom_target"] = fiche_url

        etud["_nom_disp_td_attrs"] = 'id="%s" class="etudinfo"' % (etud["etudid"])
        etud["bourse_str"] = "oui" if etud["boursier"] else "non"
        if etud["etat"] == "D":
            etud["_css_row_class"] = "etuddem"
        # et groupes:
        for partition_id in etud["partitions"]:
            etud[partition_id] = etud["partitions"][partition_id]["group_name"]
        # Ajoute colonne pour moodle: semestre_groupe, de la forme
        #     RT-DUT-FI-S3-2021-PARTITION-GROUPE
        moodle_groupename = []
        if groups_infos.selected_partitions:
            # il y a des groupes selectionnes, utilise leurs partitions
            for partition_id in groups_infos.selected_partitions:
                if partition_id in etud["partitions"]:
                    moodle_groupename.append(
                        partitions_name[partition_id]
                        + "-"
                        + etud["partitions"][partition_id]["group_name"]
                    )
        else:
            # pas de groupes sélectionnés: prend le premier s'il y en a un
            moodle_groupename = ["tous"]
            if etud["partitions"]:
                for p in etud["partitions"].items():  # partitions is an OrderedDict
                    moodle_groupename = [
                        partitions_name[p[0]] + "-" + p[1]["group_name"]
                    ]
                    break

        moodle_groupenames |= set(moodle_groupename)
        etud["semestre_groupe"] = moodle_sem_name + "-" + "+".join(moodle_groupename)

    if groups_infos.nbdem > 1:
        s = "s"
    else:
        s = ""

    if fmt == "moodlecsv":
        # de la forme S1-[FI][FA]-groupe.csv
        if not moodle_groupenames:
            moodle_groupenames = {"tous"}
        filename = (
            moodle_sem_name
            + "-"
            + groups_infos.formsemestre["modalite"]
            + "-"
            + "+".join(sorted(moodle_groupenames))
        )
    else:
        filename = f"etudiants_{groups_infos.groups_filename}"

    prefs = sco_preferences.SemPreferences(groups_infos.formsemestre_id)
    tab = GenTable(
        rows=groups_infos.members,
        columns_ids=columns_ids,
        titles=titles,
        caption="soit %d étudiants inscrits et %d démissionaire%s."
        % (len(groups_infos.members) - groups_infos.nbdem, groups_infos.nbdem, s),
        base_url=base_url,
        filename=filename,
        pdf_link=False,  # pas d'export pdf
        html_sortable=True,
        html_class="table_leftalign table_listegroupe",
        xml_outer_tag="group_list",
        xml_row_tag="etud",
        text_fields_separator=prefs["moodle_csv_separator"],
        text_with_titles=prefs["moodle_csv_with_headerline"],
        preferences=prefs,
    )
    #
    if fmt == "html":
        amail_inst = [
            x["email"] for x in groups_infos.members if x["email"] and x["etat"] != "D"
        ]
        amail_perso = [
            x["emailperso"]
            for x in groups_infos.members
            if x["emailperso"] and x["etat"] != "D"
        ]

        if len(groups_infos.members):
            if groups_infos.tous_les_etuds_du_sem:
                htitle = "Les %d étudiants inscrits" % len(groups_infos.members)
            else:
                htitle = "Groupe %s (%d étudiants)" % (
                    groups_infos.groups_titles,
                    len(groups_infos.members),
                )
        else:
            htitle = "Aucun étudiant !"
        H = [
            f"""<div class="tab-content">
            <form>
                <span style="font-weight:bold; font-size:120%;">{htitle}</span>
            """
        ]
        if groups_infos.members:
            menu_options = []
            options = {
                "with_codes": "Affiche codes",
            }
            if can_view_etud_data:
                options.update(
                    {
                        "with_paiement": "Paiement inscription",
                        "with_archives": "Fichiers archivés",
                        "with_annotations": "Annotations",
                        "with_bourse": "Statut boursier",
                    }
                )
            for option, label in options.items():
                if locals().get(option, False):
                    selected = "selected"
                else:
                    selected = ""
                menu_options.append(
                    f"""<option value="{option}" {selected}>{label}</option>"""
                )

            H.extend(
                [
                    """<span style="margin-left: 2em;">
                    <select name="group_list_options" id="group_list_options" class="multiselect" multiple="multiple">""",
                    "\n".join(menu_options),
                    """</select></span>
                    <script type="text/javascript">
                    $(document).ready(function() {
                    $('#group_list_options').multiselect(
                    {
                    includeSelectAllOption: false,
                    nonSelectedText:'Options...',
                    onChange: function(element, checked){
                        change_list_options();
                    }
                    }
                    );
                    });
                    </script>
                    """,
                    """<span class="warning_unauthorized">accès aux données personnelles interdit</span>"""
                    if not can_view_etud_data
                    else "",
                ]
            )
        H.append("</div></form>")
        if groups_infos.members:
            H.extend(
                [
                    tab.html(),
                    f"""
                    <ul>
                    <li><a class="stdlink" href="{tab.base_url}&fmt=xlsappel">Feuille d'appel Excel</a>
                    </li>
                    <li><a class="stdlink" href="{tab.base_url}&fmt=xls">Table Excel</a>
                    </li>
                    <li><a class="stdlink" href="{tab.base_url}&fmt=moodlecsv">Fichier CSV pour Moodle (groupe sélectionné)</a>
                    </li>
                    <li>
                    <a class="stdlink" href="export_groups_as_moodle_csv?formsemestre_id={groups_infos.formsemestre_id}">
                    Fichier CSV pour Moodle (tous les groupes)</a>
                    <em>(voir le paramétrage pour modifier le format des fichiers Moodle exportés)</em>
                    </li>""",
                ]
            )
            if amail_inst:
                H.append(
                    f"""<li>
                    <a class="stdlink" href="mailto:?bcc={','.join(amail_inst)
                    }">Envoyer un mail collectif au groupe de {groups_infos.groups_titles}
                    (via {len(amail_inst)} adresses institutionnelles)</a>
                    </li>"""
                )

            if can_view_etud_data:
                if amail_perso:
                    H.append(
                        f"""<li>
                        <a class="stdlink" href="mailto:?bcc={','.join(amail_perso)
                        }">Envoyer un mail collectif au groupe de {groups_infos.groups_titles}
                        (via {len(amail_perso)} adresses personnelles)</a>
                        </li>"""
                    )
                else:
                    H.append("<li><em>Adresses personnelles non renseignées</em></li>")
            else:
                H.append(
                    """<li class="unauthorized">adresses mail personnelles protégées</li>"""
                )

            H.append("</ul>")

        return "".join(H)

    elif (
        fmt == "pdf"
        or fmt == "xml"
        or fmt == "json"
        or fmt == "xls"
        or fmt == "moodlecsv"
    ):
        if fmt == "moodlecsv":
            fmt = "csv"
        return tab.make_page(fmt=fmt)

    elif fmt == "xlsappel":
        xls = sco_excel.excel_feuille_listeappel(
            groups_infos.formsemestre,
            groups_infos.groups_titles,
            groups_infos.members,
            partitions=groups_infos.partitions,
            with_codes=with_codes,
            with_paiement=with_paiement,
            server_name=request.url_root,
        )
        filename = "liste_%s" % groups_infos.groups_filename
        return scu.send_file(xls, filename, scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE)
    elif fmt == "allxls":
        if not can_view_etud_data:
            raise ScoPermissionDenied(
                "Vous n'avez pas la permission requise (ViewEtudData)"
            )
        # feuille Excel avec toutes les infos etudiants
        if not groups_infos.members:
            return ""
        keys = [
            "etudid",
            "code_nip",
            "etat",
            "civilite_str",
            "nom",
            "nom_usuel",
            "prenom",
            "inscriptionstr",
        ]
        if with_paiement:
            keys.append("paiementinscription")
        keys += [
            "email",
            "emailperso",
            "domicile",
            "villedomicile",
            "codepostaldomicile",
            "paysdomicile",
            "telephone",
            "telephonemobile",
            "fax",
            "date_naissance",
            "lieu_naissance",
            "bac",
            "specialite",
            "annee_bac",
            "nomlycee",
            "villelycee",
            "codepostallycee",
            "codelycee",
            "annee",
            "type_admission",
            "boursier_prec",
            "boursier",
            "debouche",
            "parcours",
            "code_cursus",
        ]
        titles = keys[:]
        other_partitions = sco_groups.get_group_other_partitions(groups_infos.groups[0])
        keys += [p["partition_id"] for p in other_partitions]
        titles += [p["partition_name"] for p in other_partitions]
        # remplis infos lycee si on a que le code lycée
        # et ajoute infos inscription
        for m in groups_infos.members:
            etud = sco_etud.get_etud_info(m["etudid"], filled=True)[0]
            m.update(etud)
            sco_etud.etud_add_lycee_infos(etud)
            # et ajoute le parcours
            Se = sco_cursus.get_situation_etud_cursus(
                etud, groups_infos.formsemestre_id
            )
            m["parcours"] = Se.get_cursus_descr()
            m["code_cursus"], _ = sco_report.get_code_cursus_etud(
                etud["etudid"], sems=etud["sems"]
            )
        rows = [[m.get(k, "") for k in keys] for m in groups_infos.members]
        title = "etudiants_%s" % groups_infos.groups_filename
        xls = sco_excel.excel_simple_table(titles=titles, lines=rows, sheet_name=title)
        filename = title
        return scu.send_file(xls, filename, scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE)
    else:
        raise ScoValueError("unsupported format")


def tab_absences_html(groups_infos, etat=None):
    """contenu du tab "absences et feuilles diverses" """
    authuser = current_user
    H = ['<div class="tab-content">']
    if not groups_infos.members:
        return "".join(H) + "<h3>Aucun étudiant !</h3></div>"

    group_ids: str = ",".join(map(str, groups_infos.group_ids))
    formsemestre: FormSemestre = groups_infos.get_formsemestre()

    H.extend(
        [
            "<h3>Assiduité</h3>",
            '<ul class="ul_abs">',
            "<li>",
            form_choix_saisie_semaine(groups_infos),  # Ajout Le Havre
            "</li>",
            "<li>",
            form_choix_jour_saisie_hebdo(groups_infos),
            "</li>",
            f"""<li><a class="stdlink" href="{
                    url_for("assiduites.visu_assi_group", scodoc_dept=g.scodoc_dept,
                            group_ids=group_ids,
                            date_debut=formsemestre.date_debut.isoformat(),
                            date_fin=formsemestre.date_fin.isoformat()
                    )
                }">État de l'assiduité du groupe</a></li>""",
            "</ul>",
            "<h3>Feuilles</h3>",
            '<ul class="ul_feuilles">',
            """<li><a class="stdlink" href="%s&fmt=xlsappel">Feuille d'émargement %s (Excel)</a></li>"""
            % (groups_infos.base_url, groups_infos.groups_titles),
            """<li><a class="stdlink" href="trombino?%s&fmt=pdf">Trombinoscope en PDF</a></li>"""
            % groups_infos.groups_query_args,
            """<li><a class="stdlink" href="pdf_trombino_tours?%s&fmt=pdf">Trombinoscope en PDF (format "IUT de Tours", beta)</a></li>"""
            % groups_infos.groups_query_args,
            """<li><a class="stdlink" href="pdf_feuille_releve_absences?%s&fmt=pdf">Feuille relevé absences hebdomadaire (beta)</a></li>"""
            % groups_infos.groups_query_args,
            """<li><a class="stdlink" href="trombino?%s&fmt=pdflist">Liste d'appel avec photos</a></li>"""
            % groups_infos.groups_query_args,
            f"""<li><a class="stdlink" href="groups_export_annotations?{groups_infos.groups_query_args}">Liste des annotations</a></li>"""
            if authuser.has_permission(Permission.ViewEtudData)
            else """<li class="unauthorized" title="non autorisé">Liste des annotations</li>""",
            "</ul>",
        ]
    )

    H.append('<h3>Opérations diverses</h3><ul class="ul_misc">')
    # Lien pour verif codes INE/NIP
    # (pour tous les etudiants du semestre)
    group_id = sco_groups.get_default_group(groups_infos.formsemestre_id)
    if authuser.has_permission(Permission.EtudInscrit):
        H.append(
            f"""<li><a class="stdlink" href="{
                url_for('scolar.check_group_apogee',
                        scodoc_dept=g.scodoc_dept,
                        group_id=group_id, etat=etat or "")
                }">Vérifier codes Apogée</a> (de tous les groupes)</li>
        """
        )
    # Lien pour ajout fichiers étudiants
    text = "Télécharger des fichiers associés aux étudiants (e.g. dossiers d'admission)"
    if authuser.has_permission(
        Permission.EtudAddAnnotations
    ) and authuser.has_permission(Permission.ViewEtudData):
        H.append(
            f"""<li><a class="stdlink" href="{
                url_for('scolar.etudarchive_import_files_form',
                        scodoc_dept=g.scodoc_dept,
                        group_id=group_id
        )}">{text}</a></li>"""
        )
    else:
        H.append(f"""<li class="unauthorized" title="non autorisé">{text}</li>""")

    H.append("</ul></div>")
    return "".join(H)


def tab_photos_html(groups_infos, etat=None):
    """contenu du tab "photos" """
    from app.scodoc import sco_trombino

    if not groups_infos.members:
        return '<div class="tab-content"><h3>Aucun étudiant !</h3></div>'

    return sco_trombino.trombino_html(groups_infos)


def form_choix_jour_saisie_hebdo(groups_infos, moduleimpl_id=None):
    """Formulaire choix jour semaine pour saisie."""
    authuser = current_user
    if not authuser.has_permission(Permission.AbsChange):
        return ""
    return f"""
    <button onclick="window.location='{
        url_for(
            "assiduites.signal_assiduites_group",
            scodoc_dept=g.scodoc_dept,
            group_ids=",".join(map(str,groups_infos.group_ids)),
            jour=datetime.date.today().isoformat(),
            formsemestre_id=groups_infos.formsemestre_id,
            moduleimpl_id="" if moduleimpl_id is None else moduleimpl_id
            )
    }';">Saisie du jour ({datetime.date.today().strftime('%d/%m/%Y')})</button>
    """


# Saisie de l'assiduité par semaine
def form_choix_saisie_semaine(groups_infos):
    authuser = current_user
    if not authuser.has_permission(Permission.AbsChange):
        return ""
    query_args = parse_qs(request.query_string)
    moduleimpl_id = query_args.get("moduleimpl_id", [None])[0]
    semaine = datetime.date.today().isocalendar().week
    return f"""
    <button onclick="window.location='{url_for(
        "assiduites.signal_assiduites_diff",
        group_ids=",".join(map(str,groups_infos.group_ids)),
        semaine=semaine,
        scodoc_dept=g.scodoc_dept,
        formsemestre_id=groups_infos.formsemestre_id,
        moduleimpl_id=moduleimpl_id
    )}';">Saisie à la semaine</button>
    """


def export_groups_as_moodle_csv(formsemestre_id=None):
    """Export all students/groups, in a CSV format suitable for Moodle
    Each (student,group) will be listed on a separate line
    jo@univ.fr,S3-A
    jo@univ.fr,S3-B1
    if jo belongs to group A in a partition, and B1 in another one.
    Caution: if groups in different partitions share the same name, there will be
    duplicates... should we prefix the group names with the partition's name ?
    """
    if not formsemestre_id:
        raise ScoValueError("missing parameter: formsemestre_id")
    _, partitions_etud_groups = sco_groups.get_formsemestre_groups(
        formsemestre_id, with_default=True
    )
    sem = sco_formsemestre.get_formsemestre(formsemestre_id)
    moodle_sem_name = sem["session_id"]

    columns_ids = ("email", "semestre_groupe")
    T = []
    for partition_id in partitions_etud_groups:
        partition = sco_groups.get_partition(partition_id)
        members = partitions_etud_groups[partition_id]
        for etudid in members:
            etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
            group_name = members[etudid]["group_name"]
            elts = [moodle_sem_name]
            if partition["partition_name"]:
                elts.append(partition["partition_name"])
            if group_name:
                elts.append(group_name)
            T.append({"email": etud["email"], "semestre_groupe": "-".join(elts)})
    # Make table
    prefs = sco_preferences.SemPreferences(formsemestre_id)
    tab = GenTable(
        rows=T,
        columns_ids=("email", "semestre_groupe"),
        filename=moodle_sem_name + "-moodle",
        titles={x: x for x in columns_ids},
        text_fields_separator=prefs["moodle_csv_separator"],
        text_with_titles=prefs["moodle_csv_with_headerline"],
        preferences=prefs,
    )
    return tab.make_page(fmt="csv")