# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2023 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
#   module codé par Matthias Hartmann, 2023
#
##############################################################################

import datetime
import re

from flask import g, request, render_template, flash
from flask import abort, url_for, redirect
from flask_login import current_user

from app import db
from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat
from app.decorators import (
    scodoc,
    permission_required,
)
from app.models import (
    FormSemestre,
    Identite,
    ScoDocSiteConfig,
    Assiduite,
    Justificatif,
    Departement,
    Evaluation,
)
from app.views import assiduites_bp as bp
from app.views import ScoData

# ---------------
from app.scodoc.sco_permissions import Permission
from app.scodoc import html_sco_header
from app.scodoc import safehtml
from app.scodoc import sco_moduleimpl
from app.scodoc import sco_preferences
from app.scodoc import sco_groups_view
from app.scodoc import sco_etud
from app.scodoc import sco_find_etud
from app.scodoc import sco_assiduites as scass
from app.scodoc import sco_utils as scu
from app.scodoc.sco_exceptions import ScoValueError


from app.tables.visu_assiduites import TableAssi, etuds_sorted_from_ids


CSSSTYLES = html_sco_header.BOOTSTRAP_MULTISELECT_CSS

# --- UTILS ---


class HTMLElement:
    """Représentation d'un HTMLElement version Python"""

    def __init__(self, tag: str, *attr, **kattr) -> None:
        self.tag: str = tag
        self.children: list["HTMLElement"] = []
        self.self_close: bool = kattr.get("self_close", False)
        self.text_content: str = kattr.get("text_content", "")
        self.key_attributes: dict[str, any] = kattr
        self.attributes: list[str] = list(attr)

    def add(self, *child: "HTMLElement") -> None:
        """add child element to self"""
        for kid in child:
            self.children.append(kid)

    def remove(self, child: "HTMLElement") -> None:
        """Remove child element from self"""
        if child in self.children:
            self.children.remove(child)

    def __str__(self) -> str:
        attr: list[str] = self.attributes

        for att, val in self.key_attributes.items():
            if att in ("self_close", "text_content"):
                continue

            if att != "cls":
                attr.append(f'{att}="{val}"')
            else:
                attr.append(f'class="{val}"')

        if not self.self_close:
            head: str = f"<{self.tag} {' '.join(attr)}>{self.text_content}"
            body: str = "\n".join(map(str, self.children))
            foot: str = f"</{self.tag}>"
            return head + body + foot
        return f"<{self.tag} {' '.join(attr)}/>"

    def __add__(self, other: str):
        return str(self) + other

    def __radd__(self, other: str):
        return other + str(self)


class HTMLStringElement(HTMLElement):
    """Utilisation d'une chaine de caracètres pour représenter un element"""

    def __init__(self, text: str) -> None:
        self.text: str = text
        HTMLElement.__init__(self, "textnode")

    def __str__(self) -> str:
        return self.text


class HTMLBuilder:
    def __init__(self, *content: HTMLElement | str) -> None:
        self.content: list[HTMLElement | str] = list(content)

    def add(self, *element: HTMLElement | str):
        self.content.extend(element)

    def remove(self, element: HTMLElement | str):
        if element in self.content:
            self.content.remove(element)

    def __str__(self) -> str:
        return "\n".join(map(str, self.content))

    def build(self) -> str:
        return self.__str__()


# --------------------------------------------------------------------
#
#   Assiduité (/ScoDoc/<dept>/Scolarite/Assiduites/...)
#
# --------------------------------------------------------------------


@bp.route("/")
@bp.route("/BilanDept")
@scodoc
@permission_required(Permission.AbsChange)
def bilan_dept():
    """Gestionnaire assiduités, page principale"""

    # Préparation de la page
    H = [
        html_sco_header.sco_header(
            page_title="Saisie de l'assiduité",
            javascripts=[
                "js/assiduites.js",
                "js/date_utils.js",
            ],
            cssstyles=[
                "css/assiduites.css",
            ],
        ),
        """<h2>Traitement de l'assiduité</h2>
        <p class="help">
        Pour saisir l'assiduité ou consulter les états, il est recommandé de passer par
        le semestre concerné (saisie par jour ou saisie différée).
        </p>
        """,
    ]
    H.append(
        """<p class="help">Pour signaler, annuler ou justifier l'assiduité d'un seul étudiant,
        choisissez d'abord la personne concernée&nbsp;:</p>"""
    )
    # Ajout de la barre de recherche d'étudiant (redirection vers bilan etud)
    H.append(sco_find_etud.form_search_etud(dest_url="assiduites.bilan_etud"))

    # Gestion des billets d'absences
    if current_user.has_permission(
        Permission.AbsChange
    ) and sco_preferences.get_preference("handle_billets_abs"):
        H.append(
            f"""
            <h2 style="margin-top: 30px;">Billets d'absence</h2>
            <ul><li><a href="{url_for("absences.list_billets", scodoc_dept=g.scodoc_dept)
            }">Traitement des billets d'absence en attente</a>
            </li></ul>
            """
        )

    # Récupération des années d'étude du département
    # (afin de sélectionner une année)
    dept: Departement = Departement.query.filter_by(id=g.scodoc_dept_id).first()
    annees: list[int] = sorted(
        [f.date_debut.year for f in dept.formsemestres],
        reverse=True,
    )
    annee = scu.annee_scolaire()  # Année courante, sera utilisée par défaut
    # Génération d'une liste "json" d'années
    annees_str: str = "["
    for ann in annees:
        annees_str += f"{ann},"
    annees_str += "]"

    # Récupération d'un formsemestre
    # (pour n'afficher que les assiduites/justificatifs liés au formsemestre)
    formsemestre_id = request.args.get("formsemestre_id", "")
    if formsemestre_id:
        try:
            formsemestre: FormSemestre = FormSemestre.get_formsemestre(formsemestre_id)
            annee = formsemestre.annee_scolaire()
        except AttributeError:
            formsemestre_id = ""

    # Peuplement du template jinja
    H.append(
        render_template(
            "assiduites/pages/bilan_dept.j2",
            dept_id=g.scodoc_dept_id,
            annee=annee,
            annees=annees_str,
            formsemestre_id=formsemestre_id,
            group_id=request.args.get("group_id", ""),
        ),
    )
    H.append(html_sco_header.sco_footer())
    return "\n".join(H)


@bp.route("/SignaleAssiduiteEtud")
@scodoc
@permission_required(Permission.AbsChange)
def signal_assiduites_etud():
    """
    signal_assiduites_etud Saisie de l'assiduité d'un étudiant

    Args:
        etudid (int): l'identifiant de l'étudiant

    Returns:
        str: l'html généré
    """

    # Récupération de l'étudiant concerné
    etudid = request.args.get("etudid", -1)
    etud: Identite = Identite.query.get_or_404(etudid)
    if etud.dept_id != g.scodoc_dept_id:
        abort(404, "étudiant inexistant dans ce département")

    # Récupération de la date (par défaut la date du jour)
    date = request.args.get("date", datetime.date.today().isoformat())
    heures: list[str] = [
        request.args.get("heure_deb", ""),
        request.args.get("heure_fin", ""),
    ]

    # gestion évaluations (Appel à la page depuis les évaluations)

    saisie_eval: bool = request.args.get("saisie_eval") is not None
    date_deb: str = request.args.get("date_deb")
    date_fin: str = request.args.get("date_fin")
    moduleimpl_id: int = request.args.get("moduleimpl_id", "")
    evaluation_id: int = request.args.get("evaluation_id")
    redirect_url: str = (
        "#"
        if not saisie_eval
        else url_for(
            "notes.evaluation_check_absences_html",
            evaluation_id=evaluation_id,
            scodoc_dept=g.scodoc_dept,
        )
    )

    # Préparation de la page (Header)
    header: str = html_sco_header.sco_header(
        page_title="Saisie assiduité",
        init_qtip=True,
        javascripts=[
            "js/assiduites.js",
            "js/date_utils.js",
            "js/etud_info.js",
        ],
        cssstyles=CSSSTYLES
        + [
            "css/assiduites.css",
        ],
    )

    # Gestion des horaires (journée, matin, soir)

    morning = ScoDocSiteConfig.assi_get_rounded_time("assi_morning_time", "08:00:00")
    lunch = ScoDocSiteConfig.assi_get_rounded_time("assi_lunch_time", "13:00:00")
    afternoon = ScoDocSiteConfig.assi_get_rounded_time(
        "assi_afternoon_time", "18:00:00"
    )

    # Gestion du selecteur de moduleimpl (pour le tableau différé)
    select = f"""
    <select class="dynaSelect">
        {render_template("assiduites/widgets/simplemoduleimpl_select.j2")}
    </select>
    """

    # Génération de la page
    return HTMLBuilder(
        header,
        _mini_timeline(),
        render_template(
            "assiduites/pages/ajout_assiduites.j2",
            sco=ScoData(etud),
            assi_limit_annee=sco_preferences.get_preference(
                "assi_limit_annee",
                dept_id=g.scodoc_dept_id,
            ),
            assi_morning=ScoDocSiteConfig.get("assi_morning_time", "08:00"),
            assi_evening=ScoDocSiteConfig.get("assi_afternoon_time", "18:00"),

            saisie_eval=saisie_eval,
            date_deb=date_deb,
            date_fin=date_fin,
            redirect_url=redirect_url,
            moduleimpl_id=moduleimpl_id,
        ),
        # render_template(
        #     "assiduites/pages/signal_assiduites_etud.j2",
        #     sco=ScoData(etud),
        #     date=_dateiso_to_datefr(date),
        #     morning=morning,
        #     lunch=lunch,
        #     timeline=_timeline(heures=",".join([f"'{s}'" for s in heures])),
        #     afternoon=afternoon,
        #     nonworkdays=_non_work_days(),
        #     forcer_module=sco_preferences.get_preference(
        #         "forcer_module", dept_id=g.scodoc_dept_id
        #     ),
        #     diff=_differee(
        #         etudiants=[sco_etud.get_etud_info(etudid=etud.etudid, filled=True)[0]],
        #         moduleimpl_select=select,
        #     ),
        #     saisie_eval=saisie_eval,
        #     date_deb=date_deb,
        #     date_fin=date_fin,
        #     redirect_url=redirect_url,
        #     moduleimpl_id=moduleimpl_id,
        # ),
    ).build()


@bp.route("/ListeAssiduitesEtud")
@scodoc
@permission_required(Permission.ScoView)
def liste_assiduites_etud():
    """
    liste_assiduites_etud Affichage de toutes les assiduites et justificatifs d'un etudiant
    Args:
        etudid (int): l'identifiant de l'étudiant

    Returns:
        str: l'html généré
    """

    # Récupération de l'étudiant concerné
    etudid = request.args.get("etudid", -1)
    etud: Identite = Identite.query.get_or_404(etudid)
    if etud.dept_id != g.scodoc_dept_id:
        abort(404, "étudiant inexistant dans ce département")

    # Gestion d'une assiduité unique (redirigé depuis le calendrier)
    assiduite_id: int = request.args.get("assiduite_id", -1)

    # Préparation de la page
    header: str = html_sco_header.sco_header(
        page_title=f"Assiduité de {etud.nomprenom}",
        init_qtip=True,
        javascripts=[
            "js/assiduites.js",
            "js/date_utils.js",
        ],
        cssstyles=CSSSTYLES
        + [
            "css/assiduites.css",
        ],
    )
    # Peuplement du template jinja
    return HTMLBuilder(
        header,
        render_template(
            "assiduites/pages/liste_assiduites.j2",
            sco=ScoData(etud),
            date=datetime.date.today().isoformat(),
            assi_id=assiduite_id,
            assi_limit_annee=sco_preferences.get_preference(
                "assi_limit_annee",
                dept_id=g.scodoc_dept_id,
            ),
        ),
    ).build()


@bp.route("/BilanEtud")
@scodoc
@permission_required(Permission.ScoView)
def bilan_etud():
    """
    bilan_etud Affichage de toutes les assiduites et justificatifs d'un etudiant
    Args:
        etudid (int): l'identifiant de l'étudiant

    Returns:
        str: l'html généré
    """
    # Récupération de l'étudiant
    etudid = request.args.get("etudid", -1)
    etud: Identite = Identite.query.get_or_404(etudid)
    if etud.dept_id != g.scodoc_dept_id:
        abort(404, "étudiant inexistant dans ce département")

    # Préparation de la page (header)
    header: str = html_sco_header.sco_header(
        page_title=f"Bilan de l'assiduité de {etud.nomprenom}",
        init_qtip=True,
        javascripts=[
            "js/assiduites.js",
            "js/date_utils.js",
        ],
        cssstyles=CSSSTYLES
        + [
            "css/assiduites.css",
        ],
    )

    # Gestion des dates du bilan (par défaut l'année scolaire)
    date_debut: str = f"01/09/{scu.annee_scolaire()}"
    date_fin: str = f"30/06/{scu.annee_scolaire()+1}"

    # Récupération de la métrique d'assiduité
    assi_metric = scu.translate_assiduites_metric(
        sco_preferences.get_preference("assi_metrique", dept_id=g.scodoc_dept_id),
    )

    # Génération de la page
    return HTMLBuilder(
        header,
        render_template(
            "assiduites/pages/bilan_etud.j2",
            sco=ScoData(etud),
            date_debut=date_debut,
            date_fin=date_fin,
            assi_metric=assi_metric,
            assi_seuil=_get_seuil(),
            assi_limit_annee=sco_preferences.get_preference(
                "assi_limit_annee",
                dept_id=g.scodoc_dept_id,
            ),
        ),
    ).build()


@bp.route("/AjoutJustificatifEtud")
@scodoc
@permission_required(Permission.AbsChange)
def ajout_justificatif_etud():
    """
    ajout_justificatif_etud : Affichage et création/modification des justificatifs de l'étudiant
    Args:
        etudid (int): l'identifiant de l'étudiant

    Returns:
        str: l'html généré
    """

    # Récupération de l'étudiant concerné
    etudid = request.args.get("etudid", -1)
    etud: Identite = Identite.query.get_or_404(etudid)
    if etud.dept_id != g.scodoc_dept_id:
        abort(404, "étudiant inexistant dans ce département")

    # Préparation de la page (header)
    header: str = html_sco_header.sco_header(
        page_title="Justificatifs",
        init_qtip=True,
        javascripts=[
            "js/assiduites.js",
            "js/date_utils.js",
        ],
        cssstyles=CSSSTYLES
        + [
            "css/assiduites.css",
        ],
    )

    # Peuplement du template jinja
    return HTMLBuilder(
        header,
        render_template(
            "assiduites/pages/ajout_justificatif.j2",
            sco=ScoData(etud),
            assi_limit_annee=sco_preferences.get_preference(
                "assi_limit_annee",
                dept_id=g.scodoc_dept_id,
            ),
            assi_morning=ScoDocSiteConfig.get("assi_morning_time", "08:00"),
            assi_evening=ScoDocSiteConfig.get("assi_afternoon_time", "18:00"),
        ),
    ).build()


@bp.route("/CalendrierAssiduitesEtud")
@scodoc
@permission_required(Permission.ScoView)
def calendrier_etud():
    """
    calendrier_etud : Affichage d'un calendrier des assiduités de l'étudiant
    Args:
        etudid (int): l'identifiant de l'étudiant

    Returns:
        str: l'html généré
    """

    # Récupération de l'étudiant
    etudid = request.args.get("etudid", -1)
    etud: Identite = Identite.query.get_or_404(etudid)
    if etud.dept_id != g.scodoc_dept_id:
        abort(404, "étudiant inexistant dans ce département")

    # Préparation de la page
    header: str = html_sco_header.sco_header(
        page_title="Calendrier de l'assiduité",
        init_qtip=True,
        javascripts=[
            "js/assiduites.js",
            "js/date_utils.js",
        ],
        cssstyles=CSSSTYLES
        + [
            "css/assiduites.css",
        ],
    )

    # Récupération des années d'étude de l'étudiant
    annees: list[int] = []
    for ins in etud.formsemestre_inscriptions:
        annees.extend(
            (ins.formsemestre.date_debut.year, ins.formsemestre.date_fin.year)
        )
    annees = sorted(annees, reverse=True)

    # Transformation en une liste "json"
    # (sera utilisé pour générer le selecteur d'année)
    annees_str: str = "["
    for ann in annees:
        annees_str += f"{ann},"
    annees_str += "]"

    # Peuplement du template jinja
    return HTMLBuilder(
        header,
        render_template(
            "assiduites/pages/calendrier.j2",
            sco=ScoData(etud),
            annee=scu.annee_scolaire(),
            nonworkdays=_non_work_days(),
            minitimeline=_mini_timeline(),
            annees=annees_str,
        ),
    ).build()


@bp.route("/SignalAssiduiteGr")
@scodoc
@permission_required(Permission.AbsChange)
def signal_assiduites_group():
    """
    signal_assiduites_group Saisie des assiduités des groupes pour le jour donnée

    Returns:
        str: l'html généré
    """
    # Récupération des paramètres de l'url
    formsemestre_id: int = request.args.get("formsemestre_id", -1)
    moduleimpl_id: int = request.args.get("moduleimpl_id")
    date: str = request.args.get("jour", datetime.date.today().isoformat())
    heures: list[str] = [
        request.args.get("heure_deb", ""),
        request.args.get("heure_fin", ""),
    ]
    group_ids: list[int] = request.args.get("group_ids", None)
    if group_ids is None:
        group_ids = []
    else:
        group_ids = group_ids.split(",")
        map(str, group_ids)

    # Vérification du moduleimpl_id
    try:
        moduleimpl_id = int(moduleimpl_id)
    except (TypeError, ValueError):
        moduleimpl_id = None

    # Vérification du formsemestre_id
    try:
        formsemestre_id = int(formsemestre_id)
    except (TypeError, ValueError):
        formsemestre_id = None

    # Gestion des groupes
    groups_infos = sco_groups_view.DisplayedGroupsInfos(
        group_ids, moduleimpl_id=moduleimpl_id, formsemestre_id=formsemestre_id
    )
    if not groups_infos.members:
        return (
            html_sco_header.sco_header(page_title="Saisie journalière de l'assiduité")
            + "<h3>Aucun étudiant ! </h3>"
            + html_sco_header.sco_footer()
        )

    # --- Filtrage par formsemestre ---
    formsemestre_id = groups_infos.formsemestre_id

    formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
    if formsemestre.dept_id != g.scodoc_dept_id:
        abort(404, "groupes inexistants dans ce département")

    # Vérification du forçage du module
    require_module = sco_preferences.get_preference("forcer_module", formsemestre_id)

    # Récupération des étudiants des groupes
    etuds = [
        sco_etud.get_etud_info(etudid=m["etudid"], filled=True)[0]
        for m in groups_infos.members
    ]

    # --- Vérification de la date ---
    real_date = scu.is_iso_formated(date, True).date()

    if real_date < formsemestre.date_debut or real_date > formsemestre.date_fin:
        # Si le jour est hors semestre, indiquer une erreur

        # Formatage des dates pour le message d'erreur
        real_str = real_date.strftime("%d/%m/%Y")
        form_deb = formsemestre.date_debut.strftime("%d/%m/%Y")
        form_fin = formsemestre.date_fin.strftime("%d/%m/%Y")
        raise ScoValueError(
            f"Impossible de saisir l'assiduité pour le {real_str}"
            + f" : Jour en dehors du semestre ( {form_deb} → {form_fin}) "
        )

    # --- Restriction en fonction du moduleimpl_id ---
    if moduleimpl_id:
        mod_inscrits = {
            x["etudid"]
            for x in sco_moduleimpl.do_moduleimpl_inscription_list(
                moduleimpl_id=moduleimpl_id
            )
        }
        etuds_inscrits_module = [e for e in etuds if e["etudid"] in mod_inscrits]
        if etuds_inscrits_module:
            etuds = etuds_inscrits_module
        else:
            # Si aucun etudiant n'est inscrit au module choisi...
            moduleimpl_id = None

    # Récupération du nom des/du groupe(s)

    if groups_infos.tous_les_etuds_du_sem:
        gr_tit = "en"
    else:
        if len(groups_infos.group_ids) > 1:
            grp = "des groupes"
        else:
            grp = "du groupe"
        gr_tit = (
            grp + ' <span class="fontred">' + groups_infos.groups_titles + "</span>"
        )

    # --- Génération de l'HTML ---

    header: str = html_sco_header.sco_header(
        page_title="Saisie journalière des assiduités",
        init_qtip=True,
        javascripts=html_sco_header.BOOTSTRAP_MULTISELECT_JS
        + [
            # Voir fonctionnement JS
            "js/etud_info.js",
            "js/groups_view.js",
            "js/assiduites.js",
            "js/date_utils.js",
        ],
        cssstyles=CSSSTYLES
        + [
            "css/assiduites.css",
        ],
    )

    # Récupération du semestre en dictionnaire
    sem = formsemestre.to_dict()

    # Peuplement du template jinja
    return HTMLBuilder(
        header,
        _mini_timeline(),
        render_template(
            "assiduites/pages/signal_assiduites_group.j2",
            gr_tit=gr_tit,
            sem=sem["titre_num"],
            date=_dateiso_to_datefr(date),
            formsemestre_id=formsemestre_id,
            grp=sco_groups_view.menu_groups_choice(groups_infos),
            moduleimpl_select=_module_selector(formsemestre, moduleimpl_id),
            timeline=_timeline(heures=",".join([f"'{s}'" for s in heures])),
            nonworkdays=_non_work_days(),
            formsemestre_date_debut=str(formsemestre.date_debut),
            formsemestre_date_fin=str(formsemestre.date_fin),
            forcer_module=sco_preferences.get_preference(
                "forcer_module",
                formsemestre_id=formsemestre_id,
                dept_id=g.scodoc_dept_id,
            ),
            defdem=_get_etuds_dem_def(formsemestre),
            readonly="false",
        ),
        html_sco_header.sco_footer(),
    ).build()


@bp.route("/VisuAssiduiteGr")
@scodoc
@permission_required(Permission.ScoView)
def visu_assiduites_group():
    """
    Visualisation des assiduités des groupes pour le jour donné
    dans le formsemestre_id et le moduleimpl_id
    Returns:
        str: l'html généré
    """

    # Récupération des paramètres de la requête
    formsemestre_id: int = request.args.get("formsemestre_id", -1)
    moduleimpl_id: int = request.args.get("moduleimpl_id")
    date: str = request.args.get("jour", datetime.date.today().isoformat())
    group_ids: list[int] = request.args.get("group_ids", None)
    if group_ids is None:
        group_ids = []
    else:
        group_ids = group_ids.split(",")
        map(str, group_ids)

    # Vérification du moduleimpl_id
    if moduleimpl_id is not None:
        try:
            moduleimpl_id = int(moduleimpl_id)
        except (TypeError, ValueError) as exc:
            raise ScoValueError("identifiant de moduleimpl invalide") from exc
    # Vérification du formsemestre_id
    if formsemestre_id is not None:
        try:
            formsemestre_id = int(formsemestre_id)
        except (TypeError, ValueError) as exc:
            raise ScoValueError("identifiant de formsemestre invalide") from exc

    # Récupérations des/du groupe(s)
    groups_infos = sco_groups_view.DisplayedGroupsInfos(
        group_ids, moduleimpl_id=moduleimpl_id, formsemestre_id=formsemestre_id
    )
    if not groups_infos.members:
        return (
            html_sco_header.sco_header(page_title="Saisie journalière de l'assiduité")
            + "<h3>Aucun étudiant ! </h3>"
            + html_sco_header.sco_footer()
        )

    # --- Filtrage par formsemestre ---
    formsemestre_id = groups_infos.formsemestre_id

    formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
    if formsemestre.dept_id != g.scodoc_dept_id:
        abort(404, "groupes inexistants dans ce département")

    # Vérfication du forçage du module
    require_module = sco_preferences.get_preference("forcer_module", formsemestre_id)

    # Récupération des étudiants du/des groupe(s)
    etuds = [
        sco_etud.get_etud_info(etudid=m["etudid"], filled=True)[0]
        for m in groups_infos.members
    ]

    # --- Vérification de la date ---

    real_date = scu.is_iso_formated(date, True).date()

    if real_date < formsemestre.date_debut:
        date = formsemestre.date_debut.isoformat()
    elif real_date > formsemestre.date_fin:
        date = formsemestre.date_fin.isoformat()

    # --- Restriction en fonction du moduleimpl_id ---
    if moduleimpl_id:
        mod_inscrits = {
            x["etudid"]
            for x in sco_moduleimpl.do_moduleimpl_inscription_list(
                moduleimpl_id=moduleimpl_id
            )
        }
        etuds_inscrits_module = [e for e in etuds if e["etudid"] in mod_inscrits]
        if etuds_inscrits_module:
            etuds = etuds_inscrits_module
        else:
            # Si aucun etudiant n'est inscrit au module choisi...
            moduleimpl_id = None

    # --- Génération de l'HTML ---

    if groups_infos.tous_les_etuds_du_sem:
        gr_tit = "en"
    else:
        if len(groups_infos.group_ids) > 1:
            grp = "des groupes"
        else:
            grp = "du groupe"
        gr_tit = (
            grp + ' <span class="fontred">' + groups_infos.groups_titles + "</span>"
        )

    header: str = html_sco_header.sco_header(
        page_title="Saisie journalière de l'assiduité",
        init_qtip=True,
        javascripts=html_sco_header.BOOTSTRAP_MULTISELECT_JS
        + [
            # Voir fonctionnement JS
            "js/etud_info.js",
            "js/groups_view.js",
            "js/assiduites.js",
            "js/date_utils.js",
        ],
        cssstyles=CSSSTYLES
        + [
            "css/assiduites.css",
        ],
    )

    # Récupération du semestre en dictionnaire
    sem = formsemestre.to_dict()

    return HTMLBuilder(
        header,
        _mini_timeline(),
        render_template(
            "assiduites/pages/signal_assiduites_group.j2",
            gr_tit=gr_tit,
            sem=sem["titre_num"],
            date=_dateiso_to_datefr(date),
            formsemestre_id=formsemestre_id,
            grp=sco_groups_view.menu_groups_choice(groups_infos),
            moduleimpl_select=_module_selector(formsemestre, moduleimpl_id),
            timeline=_timeline(),
            nonworkdays=_non_work_days(),
            formsemestre_date_debut=str(formsemestre.date_debut),
            formsemestre_date_fin=str(formsemestre.date_fin),
            forcer_module=sco_preferences.get_preference(
                "forcer_module",
                formsemestre_id=formsemestre_id,
                dept_id=g.scodoc_dept_id,
            ),
            defdem=_get_etuds_dem_def(formsemestre),
            readonly="true",
        ),
        html_sco_header.sco_footer(),
    ).build()


@bp.route("/etat_abs_date")
@scodoc
@permission_required(Permission.ScoView)
def etat_abs_date():
    """date_debut, date_fin en ISO"""

    # Récupération des paramètre de la requête
    date_debut_str = request.args.get("date_debut")
    date_fin_str = request.args.get("date_fin")
    title = request.args.get("desc")
    group_ids: list[int] = request.args.get("group_ids", None)

    # Vérification des dates
    try:
        date_debut = datetime.datetime.fromisoformat(date_debut_str)
    except ValueError as exc:
        raise ScoValueError("date_debut invalide") from exc
    try:
        date_fin = datetime.datetime.fromisoformat(date_fin_str)
    except ValueError as exc:
        raise ScoValueError("date_fin invalide") from exc

    # Vérification des groupes
    if group_ids is None:
        group_ids = []
    else:
        group_ids = group_ids.split(",")
        map(str, group_ids)

    groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids)

    # Récupération des étudiants des groupes
    etuds = [
        sco_etud.get_etud_info(etudid=m["etudid"], filled=True)[0]
        for m in groups_infos.members
    ]
    # Récupération des assiduites des étudiants
    assiduites: Assiduite = Assiduite.query.filter(
        Assiduite.etudid.in_([e["etudid"] for e in etuds])
    )
    # Filtrage des assiduités en fonction des dates données
    assiduites = scass.filter_by_date(
        assiduites, Assiduite, date_debut, date_fin, False
    )

    # Génération d'objet étudiant simplifié (nom+lien cal, etat_assiduite)
    etudiants: list[dict] = []
    for etud in etuds:
        # On récupère l'état de la première assiduité sur la période
        assi = assiduites.filter_by(etudid=etud["etudid"]).first()
        etat = ""
        if assi is not None and assi.etat != 0:
            etat = scu.EtatAssiduite.inverse().get(assi.etat).name

        # On génère l'objet simplifié
        etudiant = {
            "nom": f"""<a href="{url_for(
                "assiduites.calendrier_etud",
                scodoc_dept=g.scodoc_dept,
                etudid=etud["etudid"])
            }"><font color="#A00000">{etud["nomprenom"]}</font></a>""",
            "etat": etat,
        }

        etudiants.append(etudiant)

    # On tri les étudiants
    etudiants = list(sorted(etudiants, key=lambda x: x["nom"]))

    # Génération de l'HTML
    header: str = html_sco_header.sco_header(
        page_title=safehtml.html_to_safe_html(title),
        init_qtip=True,
    )

    return HTMLBuilder(
        header,
        render_template(
            "assiduites/pages/etat_absence_date.j2",
            etudiants=etudiants,
            group_title=groups_infos.groups_titles,
            date_debut=date_debut,
            date_fin=date_fin,
        ),
        html_sco_header.sco_footer(),
    ).build()


@bp.route("/VisualisationAssiduitesGroupe")
@scodoc
@permission_required(Permission.ScoView)
def visu_assi_group():
    """Visualisation de l'assiduité d'un groupe entre deux dates"""

    # Récupération des paramètres de la requête
    dates = {
        "debut": request.args.get("date_debut"),
        "fin": request.args.get("date_fin"),
    }
    fmt = request.args.get("fmt", "html")

    group_ids: list[int] = request.args.get("group_ids", None)
    if group_ids is None:
        group_ids = []
    else:
        group_ids = group_ids.split(",")
        map(str, group_ids)

    # Récupération des groupes, du semestre et des étudiants
    groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids)
    formsemestre = db.session.get(FormSemestre, groups_infos.formsemestre_id)
    etuds = etuds_sorted_from_ids([m["etudid"] for m in groups_infos.members])

    # Génération du tableau des assiduités
    table: TableAssi = TableAssi(
        etuds=etuds, dates=list(dates.values()), formsemestre=formsemestre
    )

    # Export en XLS
    if fmt.startswith("xls"):
        return scu.send_file(
            table.excel(),
            filename=f"assiduite-{groups_infos.groups_filename}",
            mime=scu.XLSX_MIMETYPE,
            suffix=scu.XLSX_SUFFIX,
        )

    # récupération du/des noms du/des groupes
    if groups_infos.tous_les_etuds_du_sem:
        gr_tit = ""
        grp = ""
    else:
        if len(groups_infos.group_ids) > 1:
            grp = "des groupes"
        else:
            grp = "du groupe"
        gr_tit = (
            grp + ' <span class="fontred">' + groups_infos.groups_titles + "</span>"
        )

    # Génération de la page
    return render_template(
        "assiduites/pages/visu_assi.j2",
        assi_metric=scu.translate_assiduites_metric(
            scu.translate_assiduites_metric(
                sco_preferences.get_preference(
                    "assi_metrique", dept_id=g.scodoc_dept_id
                ),
            ),
            inverse=False,
            short=False,
        ),
        date_debut=_dateiso_to_datefr(dates["debut"]),
        date_fin=_dateiso_to_datefr(dates["fin"]),
        gr_tit=gr_tit,
        group_ids=request.args.get("group_ids", None),
        sco=ScoData(formsemestre=groups_infos.get_formsemestre()),
        tableau=table.html(),
        title=f"Assiduité {grp} {groups_infos.groups_titles}",
    )


@bp.route("/SignalAssiduiteDifferee")
@scodoc
@permission_required(Permission.AbsChange)
def signal_assiduites_diff():
    # Récupération des paramètres de la requête
    group_ids: list[int] = request.args.get("group_ids", None)
    formsemestre_id: int = request.args.get("formsemestre_id", -1)
    date: str = request.args.get("jour", datetime.date.today().isoformat())
    date_deb: str = request.args.get("date_deb")
    date_fin: str = request.args.get("date_fin")
    semaine: str = request.args.get("semaine")

    # Dans le cas où on donne une semaine plutot qu'un jour
    if semaine is not None:
        # On génère la semaine iso à partir de l'anne scolaire.
        semaine = (
            f"{scu.annee_scolaire()}-W{semaine}" if "W" not in semaine else semaine
        )
        # On met à jour les dates avec le date de debut et fin de semaine
        date_deb: datetime.date = datetime.datetime.strptime(
            semaine + "-1", "%Y-W%W-%w"
        )
        date_fin: datetime.date = date_deb + datetime.timedelta(days=6)

    etudiants: list[dict] = []
    titre = None

    # Vérification du formsemestre_id
    try:
        formsemestre_id = int(formsemestre_id)
    except (TypeError, ValueError):
        formsemestre_id = None

    formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)

    # --- Vérification de la date ---

    real_date = scu.is_iso_formated(date, True).date()

    if real_date < formsemestre.date_debut:
        date = formsemestre.date_debut.isoformat()
    elif real_date > formsemestre.date_fin:
        date = formsemestre.date_fin.isoformat()

    # Vérification des groupes
    if group_ids is None:
        group_ids = []
    else:
        group_ids = group_ids.split(",")
        map(str, group_ids)
    groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids)
    if not groups_infos.members:
        return (
            html_sco_header.sco_header(page_title="Assiduité: saisie différée")
            + "<h3>Aucun étudiant ! </h3>"
            + html_sco_header.sco_footer()
        )

    # Récupération des étudiants
    etudiants.extend(
        [
            sco_etud.get_etud_info(etudid=m["etudid"], filled=True)[0]
            for m in groups_infos.members
        ]
    )

    etudiants = list(sorted(etudiants, key=lambda x: x["nom"]))

    # Génération de l'HTML

    header: str = html_sco_header.sco_header(
        page_title="Assiduité: saisie différée",
        init_qtip=True,
        cssstyles=[
            "css/assiduites.css",
        ],
        javascripts=html_sco_header.BOOTSTRAP_MULTISELECT_JS
        + [
            "js/assiduites.js",
            "js/date_utils.js",
            "js/etud_info.js",
        ],
    )

    sem = formsemestre.to_dict()

    if groups_infos.tous_les_etuds_du_sem:
        gr_tit = "en"
    else:
        if len(groups_infos.group_ids) > 1:
            grp = "des groupes"
        else:
            grp = "du groupe"
        gr_tit = (
            grp + ' <span class="fontred">' + groups_infos.groups_titles + "</span>"
        )

    return HTMLBuilder(
        header,
        render_template(
            "assiduites/pages/signal_assiduites_diff.j2",
            diff=_differee(
                etudiants=etudiants,
                moduleimpl_select=_module_selector(
                    formsemestre, request.args.get("moduleimpl_id", None)
                ),
                date=date,
                periode={
                    "deb": formsemestre.date_debut.isoformat(),
                    "fin": formsemestre.date_fin.isoformat(),
                },
            ),
            gr=gr_tit,
            sem=sem["titre_num"],
            defdem=_get_etuds_dem_def(formsemestre),
            timeMorning=ScoDocSiteConfig.get("assi_morning_time", "08:00:00"),
            timeNoon=ScoDocSiteConfig.get("assi_lunch_time", "13:00:00"),
            timeEvening=ScoDocSiteConfig.get("assi_afternoon_time", "18:00:00"),
            defaultDates=_get_days_between_dates(date_deb, date_fin),
            nonworkdays=_non_work_days(),
        ),
        html_sco_header.sco_footer(),
    ).build()


@bp.route("/SignalEvaluationAbs/<int:evaluation_id>/<int:etudid>")
@scodoc
@permission_required(Permission.ScoView)
def signal_evaluation_abs(etudid: int = None, evaluation_id: int = None):
    """
    Signale l'absence d'un étudiant à une évaluation
    Si la durée de l'évaluation est inférieur à 1 jour
    Alors l'absence sera sur la période de l'évaluation
    Sinon L'utilisateur sera redirigé vers la page de saisie des absences de l'étudiant
    """

    # Récupération de l'étudiant concerné
    etud: Identite = Identite.query.get_or_404(etudid)
    if etud.dept_id != g.scodoc_dept_id:
        abort(404, "étudiant inexistant dans ce département")

    # Récupération de l'évaluation concernée
    evaluation: Evaluation = Evaluation.query.get_or_404(evaluation_id)

    delta: datetime.timedelta = evaluation.date_fin - evaluation.date_debut
    # Si l'évaluation dure plus qu'un jour alors on redirige vers la page de saisie etudiant
    if delta > datetime.timedelta(days=1):
        # rediriger vers page saisie
        return redirect(
            url_for(
                "assiduites.signal_assiduites_etud",
                etudid=etudid,
                evaluation_id=evaluation.id,
                date_deb=evaluation.date_debut.strftime("%Y-%m-%dT%H:%M:%S"),
                date_fin=evaluation.date_fin.strftime("%Y-%m-%dT%H:%M:%S"),
                moduleimpl_id=evaluation.moduleimpl.id,
                saisie_eval="true",
                scodoc_dept=g.scodoc_dept,
            )
        )

    # Sinon on créé l'assiduité

    try:
        assiduite_unique: Assiduite = Assiduite.create_assiduite(
            etud=etud,
            date_debut=scu.localize_datetime(evaluation.date_debut),
            date_fin=scu.localize_datetime(evaluation.date_fin),
            etat=scu.EtatAssiduite.ABSENT,
            moduleimpl=evaluation.moduleimpl,
        )
    except ScoValueError as see:
        # En cas d'erreur
        msg: str = see.args[0]
        if "Duplication" in msg:
            msg = "Une autre assiduité concerne déjà cette période. En cliquant sur continuer vous serez redirigé vers la page de saisie des assiduités de l'étudiant."
            dest: str = url_for(
                "assiduites.signal_assiduites_etud",
                etudid=etudid,
                evaluation_id=evaluation.id,
                date_deb=evaluation.date_debut.strftime("%Y-%m-%dT%H:%M:%S"),
                date_fin=evaluation.date_fin.strftime("%Y-%m-%dT%H:%M:%S"),
                moduleimpl_id=evaluation.moduleimpl.id,
                saisie_eval="true",
                scodoc_dept=g.scodoc_dept,
                duplication="oui",
            )
            raise ScoValueError(msg, dest) from see

    db.session.add(assiduite_unique)
    db.session.commit()

    # on flash pour indiquer que l'absence a bien été créée puis on revient sur la page de l'évaluation
    flash("L'absence a bien été créée")
    # rediriger vers la page d'évaluation
    return redirect(
        url_for(
            "notes.evaluation_check_absences_html",
            evaluation_id=evaluation.id,
            scodoc_dept=g.scodoc_dept,
        )
    )


def generate_bul_list(etud: Identite, semestre: FormSemestre) -> str:
    """Génère la liste des assiduités d'un étudiant pour le bulletin mail"""

    # On récupère la métrique d'assiduité
    metrique: str = scu.translate_assiduites_metric(
        sco_preferences.get_preference("assi_metrique", formsemestre_id=semestre.id),
    )

    # On récupère le nombre maximum de ligne d'assiduité
    max_nb: int = int(
        sco_preferences.get_preference(
            "bul_mail_list_abs_nb", formsemestre_id=semestre.id
        )
    )

    # On récupère les assiduités et les justificatifs de l'étudiant
    assiduites = scass.filter_by_formsemestre(
        etud.assiduites, Assiduite, semestre
    ).order_by(Assiduite.entry_date.desc())
    justificatifs = scass.filter_by_formsemestre(
        etud.justificatifs, Justificatif, semestre
    ).order_by(Justificatif.entry_date.desc())

    # On calcule les statistiques
    stats: dict = scass.get_assiduites_stats(
        assiduites, metric=metrique, filtered={"split": True}
    )

    # On sépare :
    # - abs_j = absences justifiées
    # - abs_nj = absences non justifiées
    # - retards = les retards
    # - justifs = les justificatifs

    abs_j: list[str] = [
        {"date": _get_date_str(assi.date_debut, assi.date_fin)}
        for assi in assiduites
        if assi.etat == scu.EtatAssiduite.ABSENT and assi.est_just is True
    ]
    abs_nj: list[str] = [
        {"date": _get_date_str(assi.date_debut, assi.date_fin)}
        for assi in assiduites
        if assi.etat == scu.EtatAssiduite.ABSENT and assi.est_just is False
    ]
    retards: list[str] = [
        {"date": _get_date_str(assi.date_debut, assi.date_fin)}
        for assi in assiduites
        if assi.etat == scu.EtatAssiduite.RETARD
    ]

    justifs: list[dict[str, str]] = [
        {
            "date": _get_date_str(justi.date_debut, justi.date_fin),
            "raison": "" if justi.raison is None else justi.raison,
            "etat": {
                scu.EtatJustificatif.VALIDE: "justificatif valide",
                scu.EtatJustificatif.NON_VALIDE: "justificatif invalide",
                scu.EtatJustificatif.ATTENTE: "justificatif en attente de validation",
                scu.EtatJustificatif.MODIFIE: "justificatif ayant été modifié",
            }.get(justi.etat),
        }
        for justi in justificatifs
    ]

    return render_template(
        "assiduites/widgets/liste_assiduites_mail.j2",
        abs_j=abs_j[:max_nb],
        abs_nj=abs_nj[:max_nb],
        retards=retards[:max_nb],
        justifs=justifs[:max_nb],
        stats=stats,
        metrique=scu.translate_assiduites_metric(metrique, short=True, inverse=False),
        metric=metrique,
    )


@bp.route("/test", methods=["GET", "POST"])
@scodoc
@permission_required(Permission.ScoView)
def test():
    """XXX fonction de test a retirer"""
    if request.method == "POST":
        print("test date_utils : ", request.form)
    return render_template("assiduites/pages/test.j2")


# --- Fonctions internes ---


def _dateiso_to_datefr(date_iso: str) -> str:
    """
    _dateiso_to_datefr Transforme une date iso en date format français

    Args:
        date_iso (str): date au format iso (YYYY-MM-DD)

    Raises:
        ValueError: Si l'argument `date_iso` n'est pas au bon format

    Returns:
        str: date au format français (DD/MM/YYYY)
    """

    regex_date_iso: str = r"^\d{4}-([0]\d|1[0-2])-([0-2]\d|3[01])$"

    # Vérification de la date_iso
    if not re.match(regex_date_iso, date_iso):
        raise ValueError(
            f"La dateiso passée en paramètre [{date_iso}] n'est pas valide."
        )

    return f"{date_iso[8:10]}/{date_iso[5:7]}/{date_iso[0:4]}"


def _get_date_str(deb: datetime.datetime, fin: datetime.datetime) -> str:
    """
    _get_date_str transforme une période en chaîne lisible

    Args:
        deb (datetime.datetime): date de début
        fin (datetime.datetime): date de fin

    Returns:
        str:
            "le dd/mm/yyyy de hh:MM à hh:MM" si les deux date sont sur le même jour
            "du dd/mm/yyyy hh:MM audd/mm/yyyy hh:MM" sinon
    """
    if deb.date() == fin.date():
        temps = deb.strftime("%d/%m/%Y %H:%M").split(" ") + [fin.strftime("%H:%M")]
        return f"le {temps[0]} de {temps[1]} à {temps[2]}"
    return f'du {deb.strftime("%d/%m/%Y %H:%M")} au {fin.strftime("%d/%m/%Y %H:%M")}'


def _get_days_between_dates(deb: str, fin: str) -> str:
    """
    _get_days_between_dates récupère tous les jours entre deux dates

    Args:
        deb (str): date de début
        fin (str): date de fin

    Returns:
        str: une chaine json représentant une liste des jours
            ['date_iso','date_iso2', ...]
    """
    if deb is None or fin is None:
        return "null"
    try:
        if isinstance(deb, str) and isinstance(fin, str):
            date_deb: datetime.date = datetime.date.fromisoformat(deb)
            date_fin: datetime.date = datetime.date.fromisoformat(fin)
        else:
            date_deb, date_fin = deb.date(), fin.date()
    except ValueError:
        return "null"
    dates: list[str] = []
    while date_deb <= date_fin:
        dates.append(f'"{date_deb.isoformat()}"')
        date_deb = date_deb + datetime.timedelta(days=1)

    return f"[{','.join(dates)}]"


def _differee(
    etudiants: list[dict],
    moduleimpl_select: str,
    date: str = None,
    periode: dict[str, str] = None,
    formsemestre_id: int = None,
) -> str:
    """
    _differee Génère un tableau de saisie différé

    Args:
        etudiants (list[dict]): la liste des étudiants (représentés par des dictionnaires)
        moduleimpl_select (str): l'html représentant le selecteur de module
        date (str, optional): la première date à afficher. Defaults to None.
        periode (dict[str, str], optional):La période par défaut de la première colonne. Defaults to None.
        formsemestre_id (int, optional): l'id du semestre pour le selecteur de module. Defaults to None.

    Returns:
        str: le widget (html/css/js)
    """
    if date is None:
        date = datetime.date.today().isoformat()

    forcer_module = sco_preferences.get_preference(
        "forcer_module",
        formsemestre_id=formsemestre_id,
        dept_id=g.scodoc_dept_id,
    )

    assi_etat_defaut = sco_preferences.get_preference(
        "assi_etat_defaut",
        formsemestre_id=formsemestre_id,
        dept_id=g.scodoc_dept_id,
    )

    return render_template(
        "assiduites/widgets/differee.j2",
        etudiants=etudiants,
        assi_etat_defaut=assi_etat_defaut,
        forcer_module=forcer_module,
        moduleimpl_select=moduleimpl_select,
        date=date,
        periode=periode,
    )


def _module_selector(formsemestre: FormSemestre, moduleimpl_id: int = None) -> str:
    """
    _module_selector Génère un HTMLSelectElement à partir des moduleimpl du formsemestre

    Args:
        formsemestre (FormSemestre): Le formsemestre d'où les moduleimpls seront pris.

    Returns:
        str: La représentation str d'un HTMLSelectElement
    """
    # récupération des ues du semestre
    ntc: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
    ues = ntc.get_ues_stat_dict()

    modimpls_list: list[dict] = []
    for ue in ues:
        # Ajout des moduleimpl de chaque ue dans la liste des moduleimpls
        modimpls_list += ntc.get_modimpls_dict(ue_id=ue["ue_id"])

    # prévoie la sélection par défaut d'un moduleimpl s'il a été passé en paramètre
    selected = "" if moduleimpl_id is not None else "selected"

    # Vérification que le moduleimpl_id passé en paramètre est bien un entier
    try:
        moduleimpl_id = int(moduleimpl_id)
    except (ValueError, TypeError):
        moduleimpl_id = None

    modules: list[dict[str, str | int]] = []
    # Récupération de l'id et d'un nom lisible pour chaque moduleimpl
    for modimpl in modimpls_list:
        modname: str = (
            (modimpl["module"]["code"] or "")
            + " "
            + (modimpl["module"]["abbrev"] or modimpl["module"]["titre"] or "")
        )
        modules.append({"moduleimpl_id": modimpl["moduleimpl_id"], "name": modname})

    return render_template(
        "assiduites/widgets/moduleimpl_selector.j2",
        selected=selected,
        modules=modules,
        moduleimpl_id=moduleimpl_id,
    )


def _dynamic_module_selector() -> str:
    """
    _dynamic_module_selector retourne l'html/css/javascript du selecteur de module dynamique

    Returns:
        str: l'html/css/javascript du selecteur de module dynamique
    """
    return render_template(
        "assiduites/widgets/moduleimpl_dynamic_selector.j2",
    )


def _timeline(formsemestre_id: int = None, heures=None) -> str:
    """
    _timeline retourne l'html de la timeline

    Args:
        formsemestre_id (int, optional): un formsemestre. Defaults to None.
            Le formsemestre sert à obtenir la période par défaut de la timeline
            sinon ce sera de 2 heure dès le début de la timeline

    Returns:
        str: l'html en chaîne de caractères
    """
    return render_template(
        "assiduites/widgets/timeline.j2",
        t_start=ScoDocSiteConfig.assi_get_rounded_time("assi_morning_time", "08:00:00"),
        t_end=ScoDocSiteConfig.assi_get_rounded_time("assi_afternoon_time", "18:00:00"),
        tick_time=ScoDocSiteConfig.get("assi_tick_time", 15),
        periode_defaut=sco_preferences.get_preference(
            "periode_defaut", formsemestre_id
        ),
        heures=heures,
    )


def _mini_timeline() -> str:
    """
    _mini_timeline Retourne l'html lié au mini timeline d'assiduités

    Returns:
        str: l'html en chaîne de caractères
    """
    return render_template(
        "assiduites/widgets/minitimeline.j2",
        t_start=ScoDocSiteConfig.assi_get_rounded_time("assi_morning_time", "08:00:00"),
        t_end=ScoDocSiteConfig.assi_get_rounded_time("assi_afternoon_time", "18:00:00"),
    )


def _non_work_days() -> str:
    """Abbréviation des jours non travaillés: "'sam','dim'".
    donnés par les préférences du département
    """
    non_travail = sco_preferences.get_preference("non_travail")
    non_travail = non_travail.replace(" ", "").split(",")
    return ",".join([f"'{i.lower()}'" for i in non_travail])


def _get_seuil() -> int:
    """Seuil d'alerte des absences (en unité de la métrique),
    tel que fixé dans les préférences du département."""
    return sco_preferences.get_preference("assi_seuil", dept_id=g.scodoc_dept_id)


def _get_etuds_dem_def(formsemestre) -> str:
    """Une chaine json donnant les étudiants démissionnaires ou défaillants
    du formsemestre, sous la forme
    '{"516" : "D", ... }'
    """
    return (
        "{"
        + ", ".join(
            [
                f'"{ins.etudid}" : "{ins.etat}"'
                for ins in formsemestre.inscriptions
                if ins.etat != scu.INSCRIT
            ]
        )
        + "}"
    )