Skip to content
Snippets Groups Projects
Select Git revision
  • fa59cc522f581dc1eaa1a12e017f0b2fb44325d3
  • master default protected
2 results

sco_page_etud.py

Blame
  • Forked from Jean-Marie Place / SCODOC_R6A06
    This fork has diverged from the upstream repository.
    sco_page_etud.py 33.59 KiB
    # -*- 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
    #
    ##############################################################################
    
    """ScoDoc fiche_etud
    
       Fiche description d'un étudiant et de son parcours
    
    """
    from flask import url_for, g, render_template, request
    from flask_login import current_user
    import sqlalchemy as sa
    
    from app import log
    from app.auth.models import User
    from app.but import cursus_but, validations_view
    from app.models import (
        Adresse,
        EtudAnnotation,
        FormSemestre,
        Identite,
        ScoDocSiteConfig,
        ValidationDUT120,
        ModuleImpl
    )
    from app.scodoc import (
        codes_cursus,
        htmlutils,
        sco_archives_etud,
        sco_bac,
        sco_cursus,
        sco_etud,
        sco_groups,
        sco_permissions_check,
        sco_report,
    )
    from app.scodoc.html_sidebar import retreive_formsemestre_from_request
    from app.scodoc.sco_bulletins import etud_descr_situation_semestre
    from app.scodoc.sco_exceptions import ScoValueError
    from app.scodoc.sco_formsemestre_validation import formsemestre_recap_parcours_table
    from app.scodoc.sco_permissions import Permission
    
    import app.scodoc.sco_utils as scu
    
    
    def _menu_scolarite(
        authuser, formsemestre: FormSemestre, etudid: int, etat_inscription: str
    ):
        """HTML pour menu "scolarite" pour un etudiant dans un semestre.
        Le contenu du menu depend des droits de l'utilisateur et de l'état de l'étudiant.
        """
        locked = not formsemestre.etat
        if locked:
            lockicon = scu.icontag(
                "lock_25",
                file_format="svg",
                border="0",
                height="20px",
                title="Semestre verrouillé",
            )
            return lockicon  # no menu
        if not authuser.has_permission(
            Permission.EtudInscrit
        ) and not authuser.has_permission(Permission.EtudChangeGroups):
            return ""  # no menu
    
        args = {"etudid": etudid, "formsemestre_id": formsemestre.id}
    
        if etat_inscription != scu.DEMISSION:
            dem_title = "Démission"
            dem_url = "scolar.form_dem"
        else:
            dem_title = "Annuler la démission"
            dem_url = "scolar.do_cancel_dem"
    
        # Note: seul un etudiant inscrit (I) peut devenir défaillant.
        if etat_inscription != codes_cursus.DEF:
            def_title = "Déclarer défaillance"
            def_url = "scolar.form_def"
        elif etat_inscription == codes_cursus.DEF:
            def_title = "Annuler la défaillance"
            def_url = "scolar.do_cancel_def"
        def_enabled = (
            (etat_inscription != scu.DEMISSION)
            and authuser.has_permission(Permission.EtudInscrit)
            and not locked
        )
        items = [
            {
                "title": dem_title,
                "endpoint": dem_url,
                "args": args,
                "enabled": authuser.has_permission(Permission.EtudInscrit) and not locked,
            },
            {
                "title": "Validation du semestre (jury)",
                "endpoint": "notes.formsemestre_validation_etud_form",
                "args": args,
                "enabled": authuser.has_permission(Permission.EtudInscrit) and not locked,
            },
            {
                "title": def_title,
                "endpoint": def_url,
                "args": args,
                "enabled": def_enabled,
            },
            {
                "title": "Désinscrire (en cas d'erreur)",
                "endpoint": "notes.formsemestre_desinscription",
                "args": args,
                "enabled": authuser.has_permission(Permission.EtudInscrit) and not locked,
            },
            {
                "title": "Inscrire à un module optionnel (ou au sport)",
                "endpoint": "notes.formsemestre_inscription_option",
                "args": args,
                "enabled": authuser.has_permission(Permission.EtudInscrit) and not locked,
            },
            {
                "title": "Gérer les validations d'UEs antérieures",
                "endpoint": "notes.formsemestre_validate_previous_ue",
                "args": args,
                "enabled": formsemestre.can_edit_jury(),
            },
            {
                "title": "Enregistrer un semestre effectué ailleurs",
                "endpoint": "notes.formsemestre_ext_create_form",
                "args": args,
                "enabled": authuser.has_permission(Permission.EditFormSemestre),
            },
            {
                "title": "Affecter les notes manquantes",
                "endpoint": "notes.formsemestre_note_etuds_sans_notes",
                "args": args,
                "enabled": authuser.has_permission(Permission.EditAllNotes),
            },
            {
                "title": "Inscrire à un autre semestre",
                "endpoint": "notes.formsemestre_inscription_with_modules_form",
                "args": {"etudid": etudid},
                "enabled": authuser.has_permission(Permission.EtudInscrit),
            },
        ]
    
        return htmlutils.make_menu(
            "Scolarité", items, css_class="direction_etud", alone=True
        )
    
    
    def fiche_etud(etudid=None):
        "fiche d'informations sur un etudiant"
        restrict_etud_data = not current_user.has_permission(Permission.ViewEtudData)
        try:
            etud = Identite.get_etud(etudid)
        except Exception as exc:
            log(f"fiche_etud: etudid={etudid!r} request.args={request.args!r}")
            raise ScoValueError("Étudiant inexistant !") from exc
        # la sidebar est differente s'il y a ou pas un etudid
        g.etudid = etudid
        info = etud.to_dict_scodoc7(restrict=restrict_etud_data)
        if etud.prenom_etat_civil:
            info["etat_civil"] = (
                "<h3>Etat-civil: "
                + etud.civilite_etat_civil_str
                + " "
                + etud.prenom_etat_civil
                + " "
                + etud.nom
                + "</h3>"
            )
        else:
            info["etat_civil"] = ""
        info["ScoURL"] = url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)
        info["authuser"] = current_user
        if restrict_etud_data:
            info["info_naissance"] = ""
            adresse = None
        else:
            info["info_naissance"] = info["date_naissance"]
            if info["lieu_naissance"]:
                info["info_naissance"] += " à " + info["lieu_naissance"]
            if info["dept_naissance"]:
                info["info_naissance"] += f" ({info['dept_naissance']})"
            adresse = etud.adresses.first()
        info.update(_format_adresse(adresse))
    
        info.update(etud.inscription_descr())
        info["etudfoto"] = etud.photo_html()
    
        # Champ dépendant des permissions:
        if current_user.has_permission(
            Permission.EtudChangeAdr
        ) and current_user.has_permission(Permission.ViewEtudData):
            info[
                "modifadresse"
            ] = f"""<a class="stdlink" href="{
                    url_for("scolar.form_change_coordonnees",
                        scodoc_dept=g.scodoc_dept, etudid=etudid)
                }">modifier adresse</a>"""
        else:
            info["modifadresse"] = ""
    
        # Groupes:
        inscription_courante = etud.inscription_courante()
        sco_groups.etud_add_group_infos(
            info,
            inscription_courante.formsemestre.id if inscription_courante else None,
            only_to_show=True,
        )
        # Parcours de l'étudiant
        last_formsemestre = None
        inscriptions = etud.inscriptions()
        info["last_formsemestre_id"] = (
            inscriptions[0].formsemestre.id if inscriptions else ""
        )
    
        sem_info = {}
        for inscription in inscriptions:
            formsemestre = inscription.formsemestre
            if inscription.etat != scu.INSCRIT:
                descr, _ = etud_descr_situation_semestre(
                    etudid,
                    formsemestre,
                    etud.e,
                    show_date_inscr=False,
                )
                grlink = f"""<span class="fontred">{descr["situation"]}</span>"""
            else:
                e = {"etudid": etudid}
                sco_groups.etud_add_group_infos(e, formsemestre.id, only_to_show=True)
    
                grlinks = []
                for partition in e["partitions"].values():
                    if partition["partition_name"]:
                        gr_name = partition["group_name"]
                    else:
                        gr_name = "tous"
    
                    grlinks.append(
                        f"""<a class="discretelink" href="{
                        url_for('scolar.groups_lists',
                        scodoc_dept=g.scodoc_dept, group_ids=partition['group_id'])
                    }" title="Liste du groupe {gr_name}">{gr_name}</a>
                    """
                    )
                grlink = ", ".join(grlinks)
            # infos ajoutées au semestre dans le parcours (groupe, menu)
            menu = _menu_scolarite(current_user, formsemestre, etudid, inscription.etat)
            if menu:
                sem_info[formsemestre.id] = (
                    "<table><tr><td>" + grlink + "</td><td>" + menu + "</td></tr></table>"
                )
            else:
                sem_info[formsemestre.id] = grlink
    
        if inscriptions:
            Se = sco_cursus.get_situation_etud_cursus(etud, info["last_formsemestre_id"])
            info["liste_inscriptions"] = formsemestre_recap_parcours_table(
                Se,
                etudid,
                with_links=False,
                sem_info=sem_info,
                with_all_columns=False,
            )
            info["link_bul_pdf"] = (
                """<span class="link_bul_pdf fontred">PDF interdits par l'admin.</span>"""
                if ScoDocSiteConfig.is_bul_pdf_disabled()
                else f"""
                <span class="link_bul_pdf">
                <a class="stdlink" href="{
                    url_for("notes.etud_bulletins_pdf", scodoc_dept=g.scodoc_dept, etudid=etudid)
                    }">Tous les bulletins</a>
                </span>
                """
            )
            last_formsemestre: FormSemestre = inscriptions[0].formsemestre
            if last_formsemestre.formation.is_apc() and last_formsemestre.semestre_id > 2:
                info[
                    "link_bul_pdf"
                ] += f"""
                <span class="link_bul_pdf">
                <a class="stdlink" href="{
                    url_for("notes.validation_rcues",
                        scodoc_dept=g.scodoc_dept, etudid=etudid, formsemestre_id=last_formsemestre.id)
                    }">Visualiser les compétences BUT</a>
                </span>
                """
            info["link_inscrire_ailleurs"] = (
                f"""<span class="link_bul_pdf"><a class="stdlink" href="{
                    url_for("notes.formsemestre_inscription_with_modules_form",
                    scodoc_dept=g.scodoc_dept, etudid=etudid)
                    }">Inscrire à un autre semestre</a></span>
                    """
                if current_user.has_permission(Permission.EtudInscrit)
                else ""
            )
            can_edit_jury = current_user.has_permission(Permission.EtudInscrit)
            info[
                "link_inscrire_ailleurs"
            ] += f"""
                    <span class="link_bul_pdf"><a class="stdlink" href="{
                    url_for("notes.jury_delete_manual",
                    scodoc_dept=g.scodoc_dept, etudid=etudid,
                    read_only=not can_edit_jury)
                    }">{'Éditer' if can_edit_jury else 'Détail de'} toutes décisions de jury</a></span>
                    """
    
            info[
                "link_bilan_ects"
            ] = f"""<span class="link_bul_pdf"><a class="stdlink" href="{
                    url_for("notes.etud_bilan_ects",
                    scodoc_dept=g.scodoc_dept, etudid=etudid)
                    }">ECTS</a></span>"""
        else:
            # non inscrit
            l = [f"""<p><b>Étudiant{etud.e} non inscrit{etud.e}"""]
            if current_user.has_permission(Permission.EtudInscrit):
                l.append(
                    f"""<a href="{
                        url_for("notes.formsemestre_inscription_with_modules_form",
                        scodoc_dept=g.scodoc_dept, etudid=etudid)
                    }">inscrire</a></li>"""
                )
            l.append("</b></b>")
            info["liste_inscriptions"] = "\n".join(l)
            info["link_bul_pdf"] = ""
            info["link_inscrire_ailleurs"] = ""
            info["link_bilan_ects"] = ""
    
        # Liste des annotations
        html_annotations_list = "\n".join(
            [] if restrict_etud_data else get_html_annotations_list(etud)
        )
    
        # fiche admission
        if etud.admission:
            infos_admission = _infos_admission(etud, restrict_etud_data)
            has_adm_notes = any(
                infos_admission[k] for k in ("math", "physique", "anglais", "francais")
            )
            has_bac_info = any(
                infos_admission[k]
                for k in (
                    "bac_specialite",
                    "annee_bac",
                    "rapporteur",
                    "commentaire",
                    "classement",
                    "type_admission",
                    "rap",
                )
            )
            if has_bac_info or has_adm_notes:
                adm_tmpl = """<!-- Donnees admission -->
        <div class="fichetitre">Informations admission</div>
        """
                if has_adm_notes:
                    adm_tmpl += """
    <table>
    <tr><th>Bac</th><th>Année</th><th>Rg</th>
    <th>Math</th><th>Physique</th><th>Anglais</th><th>Français</th></tr>
    <tr>
    <td>%(bac_specialite)s</td>
    <td>%(annee_bac)s </td>
    <td>%(classement)s</td>
    <td>%(math)s</td><td>%(physique)s</td><td>%(anglais)s</td><td>%(francais)s</td>
    </tr>
    </table>
        """
                adm_tmpl += """
        <div>Bac %(bac_specialite)s obtenu en %(annee_bac)s </div>
        <div class="info_lycee">%(info_lycee)s</div>"""
                if infos_admission["type_admission"] or infos_admission["classement"]:
                    adm_tmpl += """<div class="vadmission">"""
                if infos_admission["type_admission"]:
                    adm_tmpl += """<span>Voie d'admission: <span class="etud_type_admission">%(type_admission)s</span></span> """
                if infos_admission["classement"]:
                    adm_tmpl += """<span>Rang admission: <span class="etud_type_admission">%(classement)s</span></span>"""
                if infos_admission["type_admission"] or infos_admission["classement"]:
                    adm_tmpl += "</div>"
                if infos_admission["rap"]:
                    adm_tmpl += """<div class="note_rapporteur">%(rap)s</div>"""
                adm_tmpl += """</div>"""
            else:
                adm_tmpl = ""  # pas de boite "info admission"
            info["adm_data"] = adm_tmpl % infos_admission
        else:
            info["adm_data"] = ""
    
        # Fichiers archivés:
        info["fichiers_archive_htm"] = (
            ""
            if restrict_etud_data
            else (
                '<div class="fichetitre">Fichiers associés</div>'
                + sco_archives_etud.etud_list_archives_html(etud)
            )
        )
    
    
        # Modules & Notes
    
        #added brut ui classes, to remove later
        info["module_html"] = f"""
        <div id=ressources  class="ui-accordion ui-widget ui-helper-reset"
        data-etudid="{info['etudid']}">
        <span class="modules ui-accordion-header ui-helper-reset ui-state-default ui-corner-all ui-accordion-icons">  Modules: </span>
        <div> <form>
        <ul class="listressources">
        {get_ressources(etud)}
        </ul>
    
        <ul class="listressources">
        </ul>
        </form> </div>
        </div>
        """
    
        # Devenir de l'étudiant:
        has_debouche = True
        if sco_permissions_check.can_edit_suivi():
            suivi_readonly = "0"
            link_add_suivi = """<li class="adddebouche">
                <a id="adddebouchelink" class="stdlink" href="#">ajouter une ligne</a>
                </li>"""
        else:
            suivi_readonly = "1"
            link_add_suivi = ""
        if has_debouche:
            info[
                "debouche_html"
            ] = f"""<div id="fichedebouche"
                data-readonly="{suivi_readonly}"
                data-etudid="{info['etudid']}">
            <span class="debouche_tit">Devenir:</span>
            <div><form>
            <ul class="listdebouches">
            {link_add_suivi}
            </ul>
            </form></div>
            </div>"""
        else:
            info["debouche_html"] = ""  # pas de boite "devenir"
    
        # Inscriptions
        info[
            "inscriptions_mkup"
        ] = f"""<div class="ficheinscriptions" id="ficheinscriptions">
            <div class="fichetitre">Cursus</div>{info["liste_inscriptions"]}
                {info["link_bul_pdf"]}
                {info["link_inscrire_ailleurs"]}
                {info["link_bilan_ects"]}
            </div>"""
    
        #
        if info["groupes"].strip():
            info[
                "groupes_row"
            ] = f"""<tr>
            <td class="fichetitre2">Groupes :</td><td>{info['groupes']}</td>
            </tr>"""
        else:
            info["groupes_row"] = ""
        info["menus_etud"] = menus_etud(etudid)
        if info["boursier"] and not restrict_etud_data:
            info["bourse_span"] = """<span class="boursier">boursier</span>"""
        else:
            info["bourse_span"] = ""
    
        # Liens vers compétences BUT
        if last_formsemestre and last_formsemestre.formation.is_apc():
            try:
                but_cursus = cursus_but.EtudCursusBUT(etud, last_formsemestre.formation)
            except ScoValueError:
                but_cursus = None
            refcomp = last_formsemestre.formation.referentiel_competence
            if refcomp:
                ue_validation_by_niveau = validations_view.get_ue_validation_by_niveau(
                    refcomp, etud
                )
                ects_total = sum((v.ects() for v in ue_validation_by_niveau.values()))
            else:
                ects_total = ""
    
            validation_dut120 = ValidationDUT120.query.filter_by(etudid=etudid).first()
            validation_dut120_html = (
                f"""Diplôme DUT décerné
                    en&nbsp; <a class="stdlink" href="{
                        url_for("notes.formsemestre_status",
                            scodoc_dept=g.scodoc_dept,
                            formsemestre_id=validation_dut120.formsemestre.id)
                        }">S{validation_dut120.formsemestre.semestre_id}</a>
                    """
                if validation_dut120
                else ""
            )
    
            info[
                "but_cursus_mkup"
            ] = f"""
                <div class="section_but">
                    {render_template(
                        "but/cursus_etud.j2",
                        cursus=but_cursus,
                        scu=scu,
                        validation_dut120_html=validation_dut120_html,
                    ) if but_cursus else '<span class="pb-config">problème configuration formation BUT</span>'}
                    <div class="fiche_but_col2">
                        <div class="link_validation_rcues">
                            <a class="stdlink" href="{url_for("notes.validation_rcues",
                                                scodoc_dept=g.scodoc_dept, etudid=etudid,
                                                formsemestre_id=last_formsemestre.id)}"
                                title="Visualiser les compétences BUT"
                                >
                                <img src="/ScoDoc/static/icons/parcours-but.png" alt="validation_rcues" height="132px"/>
                                <div style="text-align: center;">Compétences BUT</div>
                            </a>
                        </div>
                        <div class="fiche_total_etcs">
                            Total ECTS BUT&nbsp;: {(float(ects_total) if ects_total else 0):g}
                        </div>
                    </div>
                </div>
            """
        else:
            info["but_cursus_mkup"] = ""
    
        adresse_template = (
            ""
            if restrict_etud_data
            else """
        <!-- Adresse -->
        <div class="ficheadresse" id="ficheadresse">
            <table>
            <tr>
                <td class="fichetitre2">Adresse :</td>
                <td> %(domicile)s %(codepostaldomicile)s %(villedomicile)s %(paysdomicile)s
                %(modifadresse)s
                %(telephones)s
                </td>
            </tr>
            </table>
        </div>
        """
        )
    
        info_naissance = (
            f"""<tr><td class="fichetitre2">Né{etud.e} le :</td>
                    <td>{info["info_naissance"]}</td></tr>
            """
            if info["info_naissance"]
            else ""
        )
        situation_template = (
            f"""
        <div class="fichesituation">
            <div class="fichetablesitu">
                <table>
                <tr><td class="fichetitre2">Situation :</td><td>%(situation)s %(bourse_span)s</td></tr>
                %(groupes_row)s
                {info_naissance}
                </table>
        """
            + adresse_template
            + """
            </div>
        </div>
        """
        )
    
        info["annotations_mkup"] = (
            f"""
    <div class="ficheannotations">
        <div class="fichetitre">Annotations</div>
        <table id="etudannotations">{html_annotations_list}</table>
    
            <form action="doAddAnnotation" method="GET" class="noprint">
                <input type="hidden" name="etudid" value="{etudid}">
                <b>Ajouter une annotation sur {etud.nomprenom}: </b>
                <div>
                    <textarea name="comment" rows="4" cols="50" value=""></textarea>
                    <div style="font-size: small; font-style: italic;">
                        <div>Ces annotations sont lisibles par tous les utilisateurs ayant la permission
                        <tt>ViewEtudData</tt> dans ce département (souvent les enseignants et le
                        secrétariat).
                        </div>
                        <div>L'annotation commençant par "PE:" est un avis de poursuite d'études.</div>
                    </div>
                </div>
    
                <input type="hidden" name="author" width=12 value="{current_user}">
                <input type="submit" value="Ajouter annotation">
            </form>
    </div>
        """
            if not restrict_etud_data
            else ""
        )
    
        tmpl = (
            """<div class="menus_etud">%(menus_etud)s</div>
    <div class="fiche_etud" id="fiche_etud"><table>
    <tr><td>
    <h2>%(nomprenom)s (%(inscription)s)</h2>
    %(etat_civil)s
    <span>%(email_link)s</span>
    </td><td class="photocell">
    <a href="etud_photo_orig_page/%(etudid)s">%(etudfoto)s</a>
    </td></tr></table>
    """
            + situation_template
            + """
    
    %(inscriptions_mkup)s
    
    %(but_cursus_mkup)s
    
    <div class="ficheadmission">
    %(adm_data)s
    
    %(fichiers_archive_htm)s
    </div>
    
    %(module_html)s
    
    %(debouche_html)s
    
    %(annotations_mkup)s
    
    <div class="code_nip">code NIP: %(code_nip)s</div>
    
    </div>
            """
        )
        return render_template(
            "sco_page_dept.j2",
            content=tmpl % info,
            title=f"Fiche étudiant {etud.nomprenom}",
            cssstyles=[
                "libjs/jQuery-tagEditor/jquery.tag-editor.css",
                "css/jury_but.css",
                "css/cursus_but.css",
            ],
            javascripts=[
                "libjs/jinplace-1.2.1.min.js",
                "js/ue_list.js",
                "libjs/jQuery-tagEditor/jquery.tag-editor.min.js",
                "libjs/jQuery-tagEditor/jquery.caret.min.js",
                "js/recap_parcours.js",
                "js/etud_debouche.js",
            ],
        )
    
    def get_ressources(etud: Identite) -> str:
        """Liste des ressources associées à un étudiant"""
        res = ""
        etudFormsemstres = list[FormSemestre](etud.get_formsemestres())
        if not etudFormsemstres:
            return res
        for formsemestre  in etudFormsemstres:
            """Ajouter un div pour chaque semestre"""
            res += f"""
            <div class="semestre">
                S{formsemestre.semestre_id}</a>
                <div>
            """
            """Ajouter un div pour chaque ressource"""
    
            ues = formsemestre.get_ues()
            """Ranger les UE selon leurs types (code venant de formsemestres.py)"""
            m_list = {
            scu.ModuleType.RESSOURCE: [],
            scu.ModuleType.SAE: [],
            scu.ModuleType.STANDARD: [],
            scu.ModuleType.MALUS: [],
            }
            for modimpl in formsemestre.modimpls_sorted:
                d = modimpl.to_dict(convert_objects=True)
                m_list[modimpl.module.module_type].append(d)
    
    
            if m_list[scu.ModuleType.RESSOURCE]:
    
                res += f"""<section>
                    <div>
                        <h2> Ressources </h2>
                        """
                for ressource in m_list[scu.ModuleType.RESSOURCE]:
                    res += f"""
                        <div class="module">
                                <h3> <a href="" target="_blank"> {ressource["module"]["code"]}</a> {ressource["module"]["titre"]} </h3>
                        </div>
                        <div class="ressource_desc">
                        </div>
                    """
    
                res += f"""</div> </section>"""
    
            
            if m_list[scu.ModuleType.SAE]:
                res += f"""<section>
                    <div>
                        <h2> SAE </h2>
                        """
                for sae in m_list[scu.ModuleType.SAE]:
                    res += f"""
                    <div class="module">
                                <h3> <a href="" target="_blank"> {sae["module"]["code"]}</a> {sae["module"]["titre"]} </h3>
                        </div>
                        <div class="ressource_desc">
                        </div>
                    """
    
                res += f"""</div> </section>"""
    
            res += f"""
                </div>
            </div>
            """
        return res
    
    def _format_adresse(adresse: Adresse | None) -> dict:
        """{ "telephonestr" : ..., "telephonemobilestr" : ... } (formats html)"""
        d = {
            "telephonestr": (
                ("<b>Tél.:</b> " + scu.format_telephone(adresse.telephone))
                if (adresse and adresse.telephone)
                else ""
            ),
            "telephonemobilestr": (
                ("<b>Mobile:</b> " + scu.format_telephone(adresse.telephonemobile))
                if (adresse and adresse.telephonemobile)
                else ""
            ),
            # e-mail:
            "email_link": (
                ", ".join(
                    [
                        f"""<a class="stdlink" href="mailto:{m}">{m}</a>"""
                        for m in [adresse.email, adresse.emailperso]
                        if m
                    ]
                )
                if adresse and (adresse.email or adresse.emailperso)
                else ""
            ),
            "domicile": (
                (adresse.domicile or "")
                if adresse
                and (
                    adresse.domicile or adresse.codepostaldomicile or adresse.villedomicile
                )
                else "<em>inconnue</em>"
            ),
            "paysdomicile": (
                f"{sco_etud.format_pays(adresse.paysdomicile)}"
                if adresse and adresse.paysdomicile
                else ""
            ),
        }
        d["telephones"] = (
            f"<br>{d['telephonestr']} &nbsp;&nbsp; {d['telephonemobilestr']}"
            if adresse and (adresse.telephone or adresse.telephonemobile)
            else ""
        )
        return d
    
    
    def _infos_admission(etud: Identite, restrict_etud_data: bool) -> dict:
        """dict with admission data, restricted or not"""
        # info sur rapporteur et son commentaire
        rap = ""
        if not restrict_etud_data:
            if etud.admission.rapporteur or etud.admission.commentaire:
                rap = "Note du rapporteur"
                if etud.admission.rapporteur:
                    rap += f" ({etud.admission.rapporteur})"
                rap += ": "
                if etud.admission.commentaire:
                    rap += f"<em>{etud.admission.commentaire}</em>"
        # nom du lycée
        if restrict_etud_data:
            info_lycee = ""
        elif etud.admission.nomlycee:
            info_lycee = "Lycée " + sco_etud.format_lycee(etud.admission.nomlycee)
            if etud.admission.villelycee:
                info_lycee += f" ({etud.admission.villelycee})"
            info_lycee += "<br>"
        elif etud.admission.codelycee:
            info_lycee = sco_etud.format_lycee_from_code(etud.admission.codelycee)
        else:
            info_lycee = ""
    
        return {
            # infos accessibles à tous:
            "bac_specialite": f"{etud.admission.bac or ''}{(' '+(etud.admission.specialite or '')) if etud.admission.specialite else ''}",
            "annee_bac": etud.admission.annee_bac or "",
            # infos protégées par ViewEtudData:
            "info_lycee": info_lycee,
            "rapporteur": etud.admission.rapporteur if not restrict_etud_data else "",
            "rap": rap,
            "commentaire": (
                (etud.admission.commentaire or "") if not restrict_etud_data else ""
            ),
            "classement": (
                (etud.admission.classement or "") if not restrict_etud_data else ""
            ),
            "type_admission": (
                (etud.admission.type_admission or "") if not restrict_etud_data else ""
            ),
            "math": (etud.admission.math or "") if not restrict_etud_data else "",
            "physique": (etud.admission.physique or "") if not restrict_etud_data else "",
            "anglais": (etud.admission.anglais or "") if not restrict_etud_data else "",
            "francais": (etud.admission.francais or "") if not restrict_etud_data else "",
        }
    
    
    def get_html_annotations_list(etud: Identite) -> list[str]:
        """Liste de chaînes html décrivant les annotations."""
        html_annotations_list = []
        annotations = EtudAnnotation.query.filter_by(etudid=etud.id).order_by(
            sa.desc(EtudAnnotation.date)
        )
        for annot in annotations:
            del_link = (
                f"""<td class="annodel"><a href="{
                    url_for("scolar.doSuppressAnnotation",
                        scodoc_dept=g.scodoc_dept, etudid=etud.id, annotation_id=annot.id)}">{
                            scu.icontag(
                            "delete_img",
                            border="0",
                            alt="suppress",
                            title="Supprimer cette annotation",
                    )
                    }</a></td>"""
                if sco_permissions_check.can_suppress_annotation(annot.id)
                else ""
            )
    
            author = User.query.filter_by(user_name=annot.author).first()
            html_annotations_list.append(
                f"""<tr><td><span class="annodate">Le {
                    annot.date.strftime(scu.DATE_FMT) if annot.date else "?"}
                par {author.get_prenomnom() if author else "?"} :
                </span><span class="annoc">{annot.comment or ""}</span></td>{del_link}</tr>
                """
            )
        return html_annotations_list
    
    
    def menus_etud(etudid):
        """Menu etudiant (operations sur l'etudiant)"""
        authuser = current_user
    
        etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
    
        menuEtud = [
            {
                "title": etud["nomprenom"],
                "endpoint": "scolar.fiche_etud",
                "args": {"etudid": etud["etudid"]},
                "enabled": True,
                "helpmsg": "Fiche étudiant",
            },
            {
                "title": "Changer la photo",
                "endpoint": "scolar.form_change_photo",
                "args": {"etudid": etud["etudid"]},
                "enabled": authuser.has_permission(Permission.EtudChangeAdr),
            },
            {
                "title": "Changer les données identité/admission",
                "endpoint": "scolar.etudident_edit_form",
                "args": {"etudid": etud["etudid"]},
                "enabled": authuser.has_permission(Permission.EtudInscrit)
                and authuser.has_permission(Permission.ViewEtudData),
            },
            {
                "title": "Copier dans un autre département...",
                "endpoint": "scolar.etud_copy_in_other_dept",
                "args": {"etudid": etud["etudid"]},
                "enabled": authuser.has_permission(Permission.EtudInscrit),
            },
            {
                "title": "Supprimer cet étudiant...",
                "endpoint": "scolar.etudident_delete",
                "args": {"etudid": etud["etudid"]},
                "enabled": authuser.has_permission(Permission.EtudInscrit),
            },
            {
                "title": "Voir le journal...",
                "endpoint": "scolar.show_etud_log",
                "args": {"etudid": etud["etudid"]},
                "enabled": True,
            },
        ]
    
        return htmlutils.make_menu(
            "Étudiant", menuEtud, alone=True, css_class="menu-etudiant"
        )
    
    
    def etud_info_html(etudid, with_photo="1", debug=False):
        """An HTML div with basic information and links about this etud.
        Used for popups information windows.
        """
        formsemestre_id = retreive_formsemestre_from_request()
        with_photo = int(with_photo)
        etud = Identite.get_etud(etudid)
    
        photo_html = etud.photo_html(title="fiche de " + etud.nomprenom)
        code_cursus, _ = sco_report.get_code_cursus_etud(
            etud, formsemestres=etud.get_formsemestres(), prefix="S", separator=", "
        )
        if etud.admission:
            bac = sco_bac.Baccalaureat(etud.admission.bac, etud.admission.specialite)
            bac_abbrev = bac.abbrev()
        else:
            bac_abbrev = "-"
        H = f"""<div class="etud_info_div">
        <div class="eid_left">
         <div class="eid_nom"><div><a class="stdlink" target="_blank" href="{
             url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
         }">{etud.nomprenom}</a></div></div>
         <div class="eid_info eid_bac">Bac: <span class="eid_bac">{bac_abbrev}</span></div>
         <div class="eid_info eid_parcours">{code_cursus}</div>
        """
    
        # Informations sur l'etudiant dans le semestre courant:
        if formsemestre_id:  # un semestre est spécifié par la page
            formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
        else:
            # le semestre "en cours" pour l'étudiant
            inscription_courante = etud.inscription_courante()
            formsemestre = (
                inscription_courante.formsemestre if inscription_courante else None
            )
    
        if formsemestre:
            groups = sco_groups.get_etud_groups(etudid, formsemestre.id)
            grc = sco_groups.listgroups_abbrev(groups)
            H += f"""<div class="eid_info">En <b>S{formsemestre.semestre_id}</b>: {grc}</div>"""
        H += "</div>"  # fin partie gauche (eid_left)
        if with_photo:
            H += '<span class="eid_right">' + photo_html + "</span>"
    
        H += "</div>"
        if debug:
            return render_template("sco_page.j2", title="debug", content=H)
    
        return H