From 37c93c8524da904a9ecba1f816cc3f726a169f58 Mon Sep 17 00:00:00 2001
From: Emmanuel Viennet <emmanuel.viennet@gmail.com>
Date: Sat, 24 Aug 2024 08:06:46 +0200
Subject: [PATCH] Cosmetic / templatification

---
 app/scodoc/gen_tables.py                      |  3 +-
 app/scodoc/sco_archives_formsemestre.py       | 24 +++---
 app/scodoc/sco_debouche.py                    | 16 ++--
 app/scodoc/sco_edit_formation.py              |  9 ++-
 app/scodoc/sco_edit_module.py                 | 49 +++++------
 app/scodoc/sco_edit_ue.py                     | 12 +--
 app/scodoc/sco_evaluations.py                 |  4 +-
 app/scodoc/sco_excel.py                       |  2 +-
 app/scodoc/sco_formation_recap.py             |  4 +-
 app/scodoc/sco_formation_versions.py          |  1 +
 app/scodoc/sco_formsemestre_edit.py           | 25 +++---
 app/scodoc/sco_formsemestre_status.py         | 81 ++++++++++++-------
 app/scodoc/sco_formsemestre_validation.py     | 12 ++-
 app/scodoc/sco_groups_exports.py              |  2 +-
 app/scodoc/sco_groups_view.py                 | 25 +++---
 app/scodoc/sco_moduleimpl_status.py           |  2 +-
 app/scodoc/sco_utils.py                       |  7 +-
 app/static/css/scodoc.css                     | 20 ++++-
 .../pages/feuille_abs_formsemestre.j2         | 30 +++----
 app/templates/bul_head.j2                     |  2 +-
 app/templates/but/bulletin_court_page.j2      |  2 +-
 app/templates/but/parcour_formation.j2        |  2 +-
 app/templates/but/refcomp_load.j2             |  2 +-
 app/templates/but/refcomp_show.j2             |  2 +-
 app/templates/but/refcomp_table.j2            |  2 +-
 app/templates/but/validate_dut120.j2          |  2 +-
 .../formation/ue_assoc_parcours_ects.j2       | 20 ++---
 .../formsemestre/bulletins_choice.j2          |  2 +
 app/templates/pn/form_modules_ue_coefs.j2     | 28 ++++++-
 app/views/pn_modules.py                       | 74 +++++------------
 sco_version.py                                |  2 +-
 31 files changed, 260 insertions(+), 208 deletions(-)

diff --git a/app/scodoc/gen_tables.py b/app/scodoc/gen_tables.py
index f37a55f68..e9368f071 100644
--- a/app/scodoc/gen_tables.py
+++ b/app/scodoc/gen_tables.py
@@ -685,6 +685,7 @@ class GenTable:
         javascripts=(),
         with_html_headers=True,
         publish=True,
+        template="sco_page.j2",
     ):
         """
         Build page at given format
@@ -703,7 +704,7 @@ class GenTable:
             H.append(self.html())
             if with_html_headers:
                 return render_template(
-                    "sco_page.j2",
+                    template,
                     content="\n".join(H),
                     title=page_title,
                     javascripts=javascripts,
diff --git a/app/scodoc/sco_archives_formsemestre.py b/app/scodoc/sco_archives_formsemestre.py
index 449862531..2365e1b77 100644
--- a/app/scodoc/sco_archives_formsemestre.py
+++ b/app/scodoc/sco_archives_formsemestre.py
@@ -36,7 +36,6 @@ from app.comp.res_compat import NotesTableCompat
 from app.models import FormSemestre
 from app.scodoc.TrivialFormulator import TrivialFormulator
 from app.scodoc.sco_exceptions import ScoPermissionDenied
-from app.scodoc import html_sco_header
 from app.scodoc import sco_bulletins_pdf
 from app.scodoc import sco_groups
 from app.scodoc import sco_groups_view
@@ -123,18 +122,18 @@ def do_formsemestre_archive(
     )
     if table_html:
         flash(f"Moyennes archivées le {date}", category="info")
-        data = "\n".join(
-            [
-                html_sco_header.sco_header(
-                    page_title=f"Moyennes archivées le {date}",
-                    no_sidebar=True,
-                ),
-                f'<h2 class="fontorange">Valeurs archivées le {date}</h2>',
-                """<style type="text/css">table.notes_recapcomplet tr {  color: rgb(185,70,0); }
+        data = render_template(
+            "sco_page.j2",
+            no_sidebar=True,
+            title=f"Moyennes archivées le {date}",
+            content="\n".join(
+                [
+                    f'<h2 class="fontorange">Valeurs archivées le {date}</h2>',
+                    """<style type="text/css">table.notes_recapcomplet tr {  color: rgb(185,70,0); }
                 </style>""",
-                table_html,
-                html_sco_header.sco_footer(),
-            ]
+                    table_html,
+                ]
+            ),
         )
         PV_ARCHIVER.store(
             archive_id, "Tableau_moyennes.html", data, dept_id=formsemestre.dept_id
@@ -254,7 +253,6 @@ enregistrés et non modifiables, on peut les retrouver ultérieurement.
         }">Paramétrage</a>"
         (accessible à l'administrateur du département).</em>
         </p>""",
-        html_sco_header.sco_footer(),
     ]
 
     descr = [
diff --git a/app/scodoc/sco_debouche.py b/app/scodoc/sco_debouche.py
index 116c815dd..a5c97ab93 100644
--- a/app/scodoc/sco_debouche.py
+++ b/app/scodoc/sco_debouche.py
@@ -29,7 +29,7 @@
 Rapport (table) avec dernier semestre fréquenté et débouché de chaque étudiant
 """
 import http
-from flask import url_for, g, request
+from flask import g, render_template, request, url_for
 
 from app import log
 from app.comp import res_sem
@@ -40,7 +40,6 @@ import app.scodoc.notesdb as ndb
 from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
 from app.scodoc.gen_tables import GenTable
 from app.scodoc import safehtml
-from app.scodoc import html_sco_header
 from app.scodoc import sco_permissions_check
 from app.scodoc import sco_preferences
 from app.scodoc import sco_tag_module
@@ -78,6 +77,7 @@ def report_debouche_date(start_year=None, fmt="html"):
         title="""<h2 class="formsemestre">Débouchés étudiants </h2>""",
         fmt=fmt,
         with_html_headers=True,
+        template="sco_page_dept.j2",
     )
 
 
@@ -226,14 +226,16 @@ def table_debouche_etudids(etudids, keep_numeric=True):
 
 def report_debouche_ask_date(msg: str) -> str:
     """Formulaire demande date départ"""
-    return f"""{html_sco_header.sco_header()}
+    return render_template(
+        "sco_page_dept.j2",
+        content=f"""
     <h2>Table des débouchés des étudiants</h2>
     <form method="GET">
     {msg}
     <input type="text" name="start_year" value="" size=10/>
     </form>
-    {html_sco_header.sco_footer()}
-    """
+    """,
+    )
 
 
 # ----------------------------------------------------------------------------
@@ -323,11 +325,11 @@ def itemsuivi_set_date(itemsuivi_id, item_date):
     return ("", 204)
 
 
-def itemsuivi_set_situation(object, value):
+def itemsuivi_set_situation(obj, value):
     """set situation"""
     if not sco_permissions_check.can_edit_suivi():
         raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
-    itemsuivi_id = object
+    itemsuivi_id = obj
     situation = value.strip("-_ \t")
     # log('itemsuivi_set_situation %s : %s' % (itemsuivi_id, situation))
     cnx = ndb.GetDBConnexion()
diff --git a/app/scodoc/sco_edit_formation.py b/app/scodoc/sco_edit_formation.py
index 82d7d8d4e..151f83d5b 100644
--- a/app/scodoc/sco_edit_formation.py
+++ b/app/scodoc/sco_edit_formation.py
@@ -29,7 +29,7 @@
 (portage from DTML)
 """
 import flask
-from flask import flash, g, url_for, request
+from flask import flash, g, url_for, render_template, request
 import sqlalchemy
 
 from app import db
@@ -54,7 +54,6 @@ def formation_delete(formation_id=None, dialog_confirmed=False):
     formation: Formation = Formation.query.get_or_404(formation_id)
 
     H = [
-        html_sco_header.sco_header(page_title="Suppression d'une formation"),
         f"""<h2>Suppression de la formation {formation.titre} ({formation.acronyme})</h2>""",
     ]
     formsemestres = formation.formsemestres.all()
@@ -85,6 +84,7 @@ def formation_delete(formation_id=None, dialog_confirmed=False):
                 OK="Supprimer cette formation",
                 cancel_url=url_for("notes.index_html", scodoc_dept=g.scodoc_dept),
                 parameters={"formation_id": formation_id},
+                template="sco_page_dept.j2",
             )
         else:
             do_formation_delete(formation_id)
@@ -95,8 +95,9 @@ def formation_delete(formation_id=None, dialog_confirmed=False):
                     }">continuer</a></p>"""
             )
 
-    H.append(html_sco_header.sco_footer())
-    return "\n".join(H)
+    return render_template(
+        "sco_page-dept.j2", content="\n".join(H), title="Suppression d'une formation"
+    )
 
 
 def do_formation_delete(formation_id):
diff --git a/app/scodoc/sco_edit_module.py b/app/scodoc/sco_edit_module.py
index eef881a1d..4b2c0bd66 100644
--- a/app/scodoc/sco_edit_module.py
+++ b/app/scodoc/sco_edit_module.py
@@ -51,7 +51,6 @@ from app.scodoc.sco_exceptions import (
     ScoGenError,
     ScoNonEmptyFormationObject,
 )
-from app.scodoc import html_sco_header
 from app.scodoc import codes_cursus
 from app.scodoc import sco_edit_matiere
 from app.scodoc import sco_moduleimpl
@@ -208,8 +207,8 @@ def module_delete(module_id=None):
         )
 
     H = [
-        html_sco_header.sco_header(page_title="Suppression d'un module"),
-        f"""<h2>Suppression du module {module.titre or "<em>sans titre</em>"} ({module.code})</h2>""",
+        f"""<h2>Suppression du module {module.titre or "<em>sans titre</em>"}
+        ({module.code})</h2>""",
     ]
 
     dest_url = url_for(
@@ -227,7 +226,11 @@ def module_delete(module_id=None):
         cancelbutton="Annuler",
     )
     if tf[0] == 0:
-        return "\n".join(H) + tf[1] + html_sco_header.sco_footer()
+        return render_template(
+            "sco_page_dept.j2",
+            title="Suppression d'un module",
+            content="\n".join(H) + tf[1],
+        )
     elif tf[0] == -1:
         return flask.redirect(dest_url)
     else:
@@ -372,16 +375,6 @@ def module_edit(
         """
 
     H = [
-        html_sco_header.sco_header(
-            page_title=page_title,
-            cssstyles=["libjs/jQuery-tagEditor/jquery.tag-editor.css"],
-            javascripts=[
-                "libjs/jQuery-tagEditor/jquery.tag-editor.min.js",
-                "libjs/jQuery-tagEditor/jquery.caret.min.js",
-                "js/module_tag_editor.js",
-                "js/module_edit.js",
-            ],
-        ),
         f"""<h2>{title}</h2>""",
         render_template(
             "scodoc/help/modules.j2",
@@ -780,7 +773,7 @@ def module_edit(
         scu.get_request_args(),
         descr,
         html_foot_markup=(
-            f"""<div class="sco_tag_module_edit"><span
+            f"""<div class="scobox sco_tag_module_edit"><span
         class="sco_tag_edit"><textarea data-module_id="{module_id}" class="module_tag_editor"
         >{','.join(sco_tag_module.module_tag_list(module_id))}</textarea></span></div>
         """
@@ -793,8 +786,17 @@ def module_edit(
     )
     #
     if tf[0] == 0:
-        return (
-            "\n".join(H)
+        return render_template(
+            "sco_page_dept.j2",
+            title=page_title,
+            cssstyles=["libjs/jQuery-tagEditor/jquery.tag-editor.css"],
+            javascripts=[
+                "libjs/jQuery-tagEditor/jquery.tag-editor.min.js",
+                "libjs/jQuery-tagEditor/jquery.caret.min.js",
+                "js/module_tag_editor.js",
+                "js/module_edit.js",
+            ],
+            content="\n".join(H)
             + tf[1]
             + (
                 f"""
@@ -805,8 +807,7 @@ def module_edit(
         """
                 if not create
                 else ""
-            )
-            + html_sco_header.sco_footer()
+            ),
         )
     elif tf[0] == -1:
         return flask.redirect(
@@ -904,9 +905,6 @@ def module_table(formation_id):
         raise ScoValueError("invalid formation !")
     formation: Formation = Formation.query.get_or_404(formation_id)
     H = [
-        html_sco_header.sco_header(
-            page_title=f"Liste des modules de {formation.titre}"
-        ),
         f"""<h2>Listes des modules dans la formation {formation.titre} ({formation.acronyme}</h2>
         <ul class="notes_module_list">
         """,
@@ -926,8 +924,11 @@ def module_table(formation_id):
             )
         H.append("</li>")
     H.append("</ul>")
-    H.append(html_sco_header.sco_footer())
-    return "\n".join(H)
+    return render_template(
+        "sco_page_dept.j2",
+        title=f"Liste des modules de {formation.titre}",
+        content="\n".join(H),
+    )
 
 
 def module_is_locked(module_id):
diff --git a/app/scodoc/sco_edit_ue.py b/app/scodoc/sco_edit_ue.py
index 9414a26bf..e24affef9 100644
--- a/app/scodoc/sco_edit_ue.py
+++ b/app/scodoc/sco_edit_ue.py
@@ -505,8 +505,11 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
         else:
             clone_form = ""
 
-        return f"""
-        {html_sco_header.sco_header(page_title=title, javascripts=["js/edit_ue.js"])}
+        return render_template(
+            "sco_page_dept.j2",
+            title=title,
+            javascripts=["js/edit_ue.js"],
+            content=f"""
         <h2>{title}, (formation {formation.acronyme}, version {formation.version})</h2>
         <p class="help">Les UEs sont des groupes de modules dans une formation donnée,
         utilisés pour la validation (on calcule des moyennes par UE et applique des
@@ -529,9 +532,8 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
 
         <div id="bonus_description" class="scobox"></div>
         <div id="ue_list_code" class="sco_box sco_green_bg"></div>
-
-        {html_sco_header.sco_footer()}
-        """
+        """,
+        )
     elif tf[0] == 1:
         if create:
             if not tf[2]["ue_code"]:
diff --git a/app/scodoc/sco_evaluations.py b/app/scodoc/sco_evaluations.py
index a961ecb33..400729705 100644
--- a/app/scodoc/sco_evaluations.py
+++ b/app/scodoc/sco_evaluations.py
@@ -486,7 +486,8 @@ def formsemestre_evaluations_cal(formsemestre_id):
     <div class="cal_evaluations">
     { cal_html }
     </div>
-    <p>soit {nb_evals} évaluations planifiées;
+    <div class="scobox maxwidth">
+    <p>soit {nb_evals} évaluations planifiées&nbsp;:
     </p>
     <ul>
         <li>en <span style=
@@ -508,6 +509,7 @@ def formsemestre_evaluations_cal(formsemestre_id):
         )
         }" class="stdlink">voir les délais de correction</a>
     </p>
+    </div>
     """,
     )
 
diff --git a/app/scodoc/sco_excel.py b/app/scodoc/sco_excel.py
index 8a30e92e3..acd4293c2 100644
--- a/app/scodoc/sco_excel.py
+++ b/app/scodoc/sco_excel.py
@@ -825,7 +825,7 @@ def excel_feuille_listeappel(
     ws.append_blank_row()
 
     # bas de page (date, serveur)
-    dt = time.strftime("%d/%m/%Y à %Hh%M")
+    dt = time.strftime(scu.DATEATIME_FMT)
     if server_name:
         dt += " sur " + server_name
     cell_2 = ws.make_cell(("Liste éditée le " + dt), style1i)
diff --git a/app/scodoc/sco_formation_recap.py b/app/scodoc/sco_formation_recap.py
index 0cc10c306..71c08c9c1 100644
--- a/app/scodoc/sco_formation_recap.py
+++ b/app/scodoc/sco_formation_recap.py
@@ -174,7 +174,9 @@ def formation_table_recap(formation: Formation, fmt="html") -> Response:
         preferences=sco_preferences.SemPreferences(),
         table_id="formation_table_recap",
     )
-    return tab.make_page(fmt=fmt, javascripts=["js/formation_recap.js"])
+    return tab.make_page(
+        fmt=fmt, javascripts=["js/formation_recap.js"], template="sco_page_dept.j2"
+    )
 
 
 def export_recap_formations_annee_scolaire(annee_scolaire):
diff --git a/app/scodoc/sco_formation_versions.py b/app/scodoc/sco_formation_versions.py
index 8fbf3931f..b532146c4 100644
--- a/app/scodoc/sco_formation_versions.py
+++ b/app/scodoc/sco_formation_versions.py
@@ -149,6 +149,7 @@ def formsemestre_associate_new_version(
                 "formation_id": formation_id,
                 "formsemestre_id": formsemestre_id,
             },
+            template="sco_page_dept.j2",
         )
     elif request.method == "POST":
         if formsemestre_id is not None:  # pas dans le form car checkbox disabled
diff --git a/app/scodoc/sco_formsemestre_edit.py b/app/scodoc/sco_formsemestre_edit.py
index 1058cf7af..e607b2cd0 100644
--- a/app/scodoc/sco_formsemestre_edit.py
+++ b/app/scodoc/sco_formsemestre_edit.py
@@ -60,7 +60,6 @@ from app.scodoc.TrivialFormulator import TrivialFormulator
 from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
 from app.scodoc.sco_permissions import Permission
 from app.scodoc.sco_vdi import ApoEtapeVDI
-from app.scodoc import html_sco_header
 from app.scodoc import codes_cursus
 from app.scodoc import sco_edit_module
 from app.scodoc import sco_formsemestre
@@ -80,20 +79,20 @@ def _default_sem_title(formation):
 
 def formsemestre_createwithmodules():
     """Page création d'un semestre"""
-    H = [
-        html_sco_header.sco_header(
-            page_title="Création d'un semestre",
-            javascripts=["libjs/AutoSuggest.js", "js/formsemestre_edit.js"],
-            cssstyles=["css/autosuggest_inquisitor.css"],
-        ),
-        """<h2>Mise en place d'un semestre de formation</h2>""",
-    ]
+    H = ["""<h2>Mise en place d'un semestre de formation</h2>"""]
     r = do_formsemestre_createwithmodules()
-    if isinstance(r, str):
-        H.append(r)
-    else:
+    if not isinstance(r, str):
         return r  # response redirect
-    return "\n".join(H) + html_sco_header.sco_footer()
+
+    H.append(r)
+
+    return render_template(
+        "sco_page_dept.j2",
+        title="Création d'un semestre",
+        content="\n".join(H),
+        javascripts=["libjs/AutoSuggest.js", "js/formsemestre_edit.js"],
+        cssstyles=["css/autosuggest_inquisitor.css"],
+    )
 
 
 def formsemestre_editwithmodules(formsemestre_id: int):
diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py
index a6a0d7f32..154a55978 100755
--- a/app/scodoc/sco_formsemestre_status.py
+++ b/app/scodoc/sco_formsemestre_status.py
@@ -802,7 +802,7 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
         })</span></h3>"""
     )
     #
-    H.append('<div class="sem-groups-abs">')
+    H.append('<div class="sem-groups-abs space-before-18">')
 
     disable_abs: str | bool = scass.has_assiduites_disable_pref(formsemestre)
     show_abs: str = "hidden" if disable_abs else ""
@@ -812,6 +812,8 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
         groups = partition.groups.all()
         effectifs = {g.id: g.get_nb_inscrits() for g in groups}
         partition_is_empty = sum(effectifs.values()) == 0
+        if partition_is_empty and (partition.is_default() or partition.is_parcours()):
+            continue  # inutile de montrer des partitions vides non éditables
         H.append(
             f"""
             <div class="sem-groups-partition">
@@ -909,7 +911,9 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
                 )
 
                 H.append("</div>")  # /sem-groups-assi
-        if partition_is_empty and not partition.is_default():
+        if partition_is_empty and not (
+            partition.is_default() or partition.is_parcours()
+        ):
             H.append(
                 '<div class="help sem-groups-none">Aucun groupe peuplé dans cette partition'
             )
@@ -924,41 +928,72 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
             H.append("</div>")
         H.append("</div>")  # /sem-groups-partition
 
+    # Boite avec liens divers
+    autres_liens = []
     if formsemestre.can_change_groups():
-        H.append(
-            f"""<h4><a class="stdlink"
+        autres_liens.append(
+            f"""<a class="stdlink"
+            title="une partition est un ensemble de groupes: TD, TP, ..."
             href="{url_for("scolar.partition_editor",
                     scodoc_dept=g.scodoc_dept,
                     formsemestre_id=formsemestre.id,
                     edit_partition=1)
-            }">Ajouter une partition</a></h4>"""
+            }">Ajouter une partition</a>"""
         )
 
     # --- Formulaire importation Assiduité excel (si autorisé)
     if current_user.has_permission(Permission.AbsChange) and not disable_abs:
-        H.append(
-            f"""<p>
+        autres_liens.append(
+            f"""
             <a class="stdlink" href="{url_for('assiduites.feuille_abs_formsemestre',
                 scodoc_dept=g.scodoc_dept,
                 formsemestre_id=formsemestre.id)}">
                 Importation de l'assiduité depuis un fichier excel</a>
-            </p>"""
+            """
         )
 
     # --- Lien Traitement Justificatifs:
-
     if (
         current_user.has_permission(Permission.AbsJustifView)
         and current_user.has_permission(Permission.JustifValidate)
         and not disable_abs
     ):
-        H.append(
-            f"""<p>
+        autres_liens.append(
+            f"""
             <a class="stdlink" href="{url_for('assiduites.traitement_justificatifs',
                 scodoc_dept=g.scodoc_dept,
                 formsemestre_id=formsemestre.id)}">
                 Traitement des justificatifs d'absence</a>
-            </p>"""
+            """
+        )
+
+    # --- Lien pour mail aux enseignants
+    # Construit la liste de tous les enseignants de ce semestre:
+    mails_enseignants = set(u.email for u in formsemestre.responsables)
+    for modimpl in formsemestre.modimpls_sorted:
+        mails_enseignants.add(sco_users.user_info(modimpl.responsable_id)["email"])
+        mails_enseignants |= {u.email for u in modimpl.enseignants if u.email}
+    adrlist = list(mails_enseignants - {None, ""})
+    if adrlist:
+        autres_liens.append(
+            f"""
+            <a class="stdlink" href="mailto:?cc={','.join(adrlist)}">Courrier aux {
+                len(adrlist)} enseignants du semestre</a>
+            """
+        )
+
+    # Met le tout en boite
+    if autres_liens:
+        H.append(
+            f"""
+            <div class="sem-groups-partition sem-groups-autres-liens">
+            <div class="sem-groups-none">
+                <ul>
+                    <li>{'</li><li>'.join(autres_liens)}</li>
+                </ul>
+            </div>
+            </div>
+            """
         )
 
     H.append("</div>")
@@ -1033,8 +1068,9 @@ def formsemestre_status_head(formsemestre_id: int = None, page_title: str = None
     )
     if evals["last_modif"]:
         H.append(
-            " <em>(dernière note saisie le %s)</em>"
-            % evals["last_modif"].strftime("%d/%m/%Y à %Hh%M")
+            f""" <em>(dernière note saisie le {
+                evals["last_modif"].strftime(scu.DATEATIME_FMT)
+            })</em>"""
         )
     H.append("</td></tr>")
     H.append("</table>")
@@ -1073,12 +1109,6 @@ def formsemestre_status(formsemestre_id=None, check_parcours=True):
     modimpls = formsemestre.modimpls_sorted
     nt = res_sem.load_formsemestre_results(formsemestre)
 
-    # Construit la liste de tous les enseignants de ce semestre:
-    mails_enseignants = set(u.email for u in formsemestre.responsables)
-    for modimpl in modimpls:
-        mails_enseignants.add(sco_users.user_info(modimpl.responsable_id)["email"])
-        mails_enseignants |= {u.email for u in modimpl.enseignants if u.email}
-
     can_edit = formsemestre.can_be_edited_by(current_user)
     can_change_all_notes = current_user.has_permission(Permission.EditAllNotes) or (
         current_user.id in [resp.id for resp in formsemestre.responsables]
@@ -1206,20 +1236,11 @@ def formsemestre_status(formsemestre_id=None, check_parcours=True):
         )
     # --- LISTE DES ETUDIANTS
     H += [
-        '<div class="formsemestre-groupes">',
+        '<div class="formsemestre-groupes space-before-24">',
         _make_listes_sem(formsemestre),
         "</div>",
     ]
 
-    # --- Lien mail enseignants:
-    adrlist = list(mails_enseignants - {None, ""})
-    if adrlist:
-        H.append(
-            f"""<p>
-            <a class="stdlink" href="mailto:?cc={','.join(adrlist)}">Courrier aux {
-                len(adrlist)} enseignants du semestre</a>
-            </p>"""
-        )
     return render_template(
         "sco_page.j2",
         content="".join(H),
diff --git a/app/scodoc/sco_formsemestre_validation.py b/app/scodoc/sco_formsemestre_validation.py
index dfac0461b..f211a9da6 100644
--- a/app/scodoc/sco_formsemestre_validation.py
+++ b/app/scodoc/sco_formsemestre_validation.py
@@ -583,6 +583,11 @@ def formsemestre_recap_parcours_table(
         )
         is_cur = situation_etud_cursus.formsemestre_id == formsemestre.id
         num_sem += 1
+        url_status = url_for(
+            "notes.formsemestre_status",
+            scodoc_dept=g.scodoc_dept,
+            formsemestre_id=formsemestre.id,
+        )
 
         dpv = sco_pv_dict.dict_pvjury(formsemestre.id, etudids=[etudid])
         pv = dpv["decisions"][0]
@@ -642,7 +647,7 @@ def formsemestre_recap_parcours_table(
         H.append(
             f"""
         <td class="rcp_type_sem" style="background-color:{bgcolor};">{num_sem}{pm}</td>
-        <td class="datedebut">{formsemestre.mois_debut()}</td>
+        <td class="datedebut"><a href="{url_status}">{formsemestre.mois_debut()}</a></td>
         <td class="rcp_titre_sem"><a class="formsemestre_status_link"
         href="{
             url_for("notes.formsemestre_bulletinetud", scodoc_dept=g.scodoc_dept,
@@ -724,8 +729,9 @@ def formsemestre_recap_parcours_table(
                 f"""Autre formation: {formsemestre.formation.formation_code}"""
             )
         H.append(
-            '<td class="datefin">%s</td><td class="sem_info">%s</td>'
-            % (formsemestre.mois_fin(), sem_info.get(formsemestre.id, default_sem_info))
+            f"""<td class="datefin"><a href="{url_status}">{formsemestre.mois_fin()}</a></td>
+            <td class="sem_info">{sem_info.get(formsemestre.id, default_sem_info)}</td>
+            """
         )
         # Moy Gen (sous le code decision)
         H.append(
diff --git a/app/scodoc/sco_groups_exports.py b/app/scodoc/sco_groups_exports.py
index ddf02d1a8..4bce85126 100644
--- a/app/scodoc/sco_groups_exports.py
+++ b/app/scodoc/sco_groups_exports.py
@@ -65,7 +65,7 @@ def groups_export_annotations(group_ids, formsemestre_id=None, fmt="html"):
     )
     annotations = groups_list_annotation(groups_infos.group_ids)
     for annotation in annotations:
-        annotation["date_str"] = annotation["date"].strftime("%d/%m/%Y à %Hh%M")
+        annotation["date_str"] = annotation["date"].strftime(scu.DATEATIME_FMT)
     if fmt == "xls":
         columns_ids = ("etudid", "nom", "prenom", "date", "comment")
     else:
diff --git a/app/scodoc/sco_groups_view.py b/app/scodoc/sco_groups_view.py
index 598ede762..26b30918f 100644
--- a/app/scodoc/sco_groups_view.py
+++ b/app/scodoc/sco_groups_view.py
@@ -782,16 +782,21 @@ def groups_table(
                 [
                     tab.html(),
                     f"""
-                    <ul>
-                    <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>""",
+            <ul>
+            <li><a class="stdlink" href="{tab.base_url}&fmt=xls">Table Excel
+            groupe(s) {groups_infos.groups_titles}</a>
+            </li>
+            <li><a class="stdlink" href="{tab.base_url}&fmt=moodlecsv">Fichier CSV pour
+            Moodle groupe(s) {groups_infos.groups_titles}</a>
+            </li>
+            <li>
+            <a class="stdlink" href="{{
+                url_for('notes.export_groups_as_moodle_csv',
+                scodoc_dept=g.scodoc_dept, 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:
diff --git a/app/scodoc/sco_moduleimpl_status.py b/app/scodoc/sco_moduleimpl_status.py
index 2fa11e28a..139081f19 100644
--- a/app/scodoc/sco_moduleimpl_status.py
+++ b/app/scodoc/sco_moduleimpl_status.py
@@ -656,7 +656,7 @@ def _ligne_evaluation(
     if etat["last_modif"]:
         H.append(
             f"""<span class="mievr_lastmodif">(dernière modif le {
-                etat["last_modif"].strftime("%d/%m/%Y à %Hh%M")})</span>"""
+                etat["last_modif"].strftime(scu.DATEATIME_FMT)})</span>"""
         )
     #
     H.append(
diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py
index d189a0ffd..f75090cd6 100644
--- a/app/scodoc/sco_utils.py
+++ b/app/scodoc/sco_utils.py
@@ -556,7 +556,7 @@ MONTH_NAMES = (
 )
 DAY_NAMES = ("lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi", "dimanche")
 
-TIME_FMT = "%H:%M"  # affichage des heures
+TIME_FMT = "%Hh%M"  # affichage des heures
 DATE_FMT = "%d/%m/%Y"  # affichage des dates
 DATEATIME_FMT = DATE_FMT + " à " + TIME_FMT
 DATETIME_FMT = DATE_FMT + " " + TIME_FMT
@@ -1339,7 +1339,7 @@ def format_telephone(n: str | None) -> str:
 #
 def timedate_human_repr():
     "representation du temps courant pour utilisateur"
-    return time.strftime("%d/%m/%Y à %Hh%M")
+    return time.strftime(DATEATIME_FMT)
 
 
 def annee_scolaire_repr(year, month):
@@ -1525,6 +1525,7 @@ def confirm_dialog(
     help_msg=None,
     parameters: dict = None,
     target_variable="dialog_confirmed",
+    template="sco_page.j2",
 ):
     """HTML confirmation dialog: submit (POST) to same page or dest_url if given."""
     parameters = parameters or {}
@@ -1565,7 +1566,7 @@ def confirm_dialog(
     if help_msg:
         H.append('<p class="help">' + help_msg + "</p>")
     if add_headers:
-        return render_template("sco_page.j2", content="\n".join(H))
+        return render_template(template, content="\n".join(H))
     else:
         return "\n".join(H)
 
diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css
index 927eebd2d..cbfba50e9 100644
--- a/app/static/css/scodoc.css
+++ b/app/static/css/scodoc.css
@@ -55,13 +55,21 @@ div.container {
 div.sco-app-content {
   display: flex;
   flex-direction: column;
-  margin-right: 8px;
+  margin-right: 12px;
 }
 
 .space-before-18 {
   margin-top: 18px;
 }
 
+.space-before-24 {
+  margin-top: 24px;
+}
+
+div.scobox.maxwidth {
+  max-width: none;
+}
+
 div.scobox {
   flex: 1 0 0;
   /* Equal width for all boxes */
@@ -748,14 +756,14 @@ div.fiche_etud {
   /* rgb(255,240,128); */
   border: 1px solid gray;
   width: 910px;
-  padding: 10px;
+  padding: 24px 10px 10px 10px;
   margin-top: 10px;
 }
 
 div.menus_etud {
   position: absolute;
-  margin-left: 1px;
-  margin-top: 1px;
+  margin-left: 10px;
+  margin-top: 16px;
 }
 
 div.fiche_etud h2 {
@@ -2059,6 +2067,10 @@ ul.ue_inscr_list li.etud {
   text-underline-offset: 3px;
 }
 
+.sem-groups-autres-liens {
+  background-color: var(--sco-color-box-bg);
+}
+
 .sem-groups-list .stdlink,
 .sem-groups-list .stdlink:visited {
   color: rgb(0, 0, 192);
diff --git a/app/templates/assiduites/pages/feuille_abs_formsemestre.j2 b/app/templates/assiduites/pages/feuille_abs_formsemestre.j2
index 236154200..c3424d54f 100644
--- a/app/templates/assiduites/pages/feuille_abs_formsemestre.j2
+++ b/app/templates/assiduites/pages/feuille_abs_formsemestre.j2
@@ -45,7 +45,7 @@
 <h2 style="color: crimson;">{{titre_form}}</h2>
 <div class="scobox">
     <div id="excel-content">
-        <p class="hint">Avertissement : le fichier doit respecter le format suivant</p>
+        <p class="hint">Le fichier importé doit respecter le format suivant</p>
 
         <ul>
             <li>
@@ -53,12 +53,12 @@
             </li>
             <li class="star">colonne B : Date de début</li>
             <li class="star">colonne C : Date de fin</li>
-            <li class="opt">colonne D : État (ABS,RET,PRE, ABS si vide)</li>
+            <li class="opt">colonne D : État (ABS, RET, PRE), considéré ABSent si vide</li>
             <li class="opt">colonne E : code du module</li>
         </ul>
 
         <p class="hint"><span class="opt"></span> : Colonne optionnelle, les cases peuvent être vides</p>
-        <p class="hint"><span class="star"></span> : Formats autorisés :
+        <p class="hint"><span class="star"></span> : Formats de dates autorisés :
         <ul>
             <li>
                 <pre>aaaa-mm-jjThh:mm:ss</pre>
@@ -73,21 +73,23 @@
 
 
         <form action="" method="post" enctype="multipart/form-data">
-            <label for="type_identifiant">Type d'identifiant d'étudiant (etudid, ine, nip)</label>
+            <label for="type_identifiant">Type d'identifiant d'étudiant (etudid, INE, NIP)</label>
             <select name="type_identifiant" id="type_identifiant">
-                <option value="etudid">ETUDID</option>
+                <option value="etudid">etudid</option>
                 <option value="ine">INE</option>
                 <option value="nip">NIP</option>
             </select>
-            <br>
-            <label for="file">
-                Sélectionnez un fichier:
-                
-            </label>
-            <input id="file" type="file" name="file"
-                    accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel" />
-            <br>
-            <button type="submit">Importer</button>
+            <div class="space-before-18">
+                <label for="file">
+                    Sélectionnez un fichier:
+
+                </label>
+                <input id="file" type="file" name="file"
+                        accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel" />
+            </div>
+            <div class="space-before-18">
+                <button type="submit">Importer</button>
+            </div>
         </form>
     </div>
 </div>
diff --git a/app/templates/bul_head.j2 b/app/templates/bul_head.j2
index 639a72ceb..3d338fabe 100644
--- a/app/templates/bul_head.j2
+++ b/app/templates/bul_head.j2
@@ -27,7 +27,7 @@
             </span>
         </div>
         <div>
-        <em>établi le {{time.strftime("%d/%m/%Y à %Hh%M")}} (notes sur 20)</em>
+        <em>établi le {{time.strftime(scu.DATEATIME_FMT)}} (notes sur 20)</em>
         <span class="rightjust">
             <select name="version" onchange="self.location.href='{{
                 url_for('notes.formsemestre_bulletinetud',
diff --git a/app/templates/but/bulletin_court_page.j2 b/app/templates/but/bulletin_court_page.j2
index 95180ff83..222efc499 100644
--- a/app/templates/but/bulletin_court_page.j2
+++ b/app/templates/but/bulletin_court_page.j2
@@ -158,7 +158,7 @@
     </div>
 
     <div id="footer">
-    Bulletin généré par ScoDoc le {{time.strftime("%d/%m/%Y à %Hh%M")}}. Explication des codes sur
+    Bulletin généré par ScoDoc le {{time.strftime(scu.DATEATIME_FMT)}}. Explication des codes sur
         <a href="https://scodoc.org/BUTCodesJury">https://scodoc.org/BUTCodesJury</a>
     </div>
 </div>
diff --git a/app/templates/but/parcour_formation.j2 b/app/templates/but/parcour_formation.j2
index 91f57b4e0..ce8614e41 100644
--- a/app/templates/but/parcour_formation.j2
+++ b/app/templates/but/parcour_formation.j2
@@ -1,4 +1,4 @@
-{% extends "sco_page.j2" %}
+{% extends "sco_page_dept.j2" %}
 
 {% block styles %}
     {{super()}}
diff --git a/app/templates/but/refcomp_load.j2 b/app/templates/but/refcomp_load.j2
index b6c88cefa..b1e2ea567 100644
--- a/app/templates/but/refcomp_load.j2
+++ b/app/templates/but/refcomp_load.j2
@@ -1,5 +1,5 @@
 {# -*- mode: jinja-html -*- #}
-{% extends "base.j2" %}
+{% extends "sco_page_dept.j2" %}
 {% import 'wtf.j2' as wtf %}
 
 {% block app_content %}
diff --git a/app/templates/but/refcomp_show.j2 b/app/templates/but/refcomp_show.j2
index f2a460f2c..4ddfa2ce0 100644
--- a/app/templates/but/refcomp_show.j2
+++ b/app/templates/but/refcomp_show.j2
@@ -1,5 +1,5 @@
 {# -*- mode: jinja-html -*- #}
-{% extends "sco_page.j2" %}
+{% extends "sco_page_dept.j2" %}
 {% block styles %}
     {{super()}}
     <link href="{{scu.STATIC_DIR}}/css/refcomp_parcours_niveaux.css" rel="stylesheet" type="text/css" />
diff --git a/app/templates/but/refcomp_table.j2 b/app/templates/but/refcomp_table.j2
index 0c7585dc0..ee956c394 100644
--- a/app/templates/but/refcomp_table.j2
+++ b/app/templates/but/refcomp_table.j2
@@ -1,5 +1,5 @@
 {# -*- mode: jinja-html -*- #}
-{% extends "sco_page.j2" %}
+{% extends "sco_page_dept.j2" %}
 {% import 'wtf.j2' as wtf %}
 
 {% block app_content %}
diff --git a/app/templates/but/validate_dut120.j2 b/app/templates/but/validate_dut120.j2
index 3cfe91899..fd13de8ad 100644
--- a/app/templates/but/validate_dut120.j2
+++ b/app/templates/but/validate_dut120.j2
@@ -51,7 +51,7 @@ une formation utilisant une autre version de référentiel, pensez à revalider
         pour l'étudiant{{etud.ne}} {{etud.html_link_fiche()|safe}}
         <ul>
             <li>DUT 120 spécialité {{formsemestre.formation.referentiel_competence.specialite_long}}
-                enregistré le {{time.strftime("%d/%m/%Y à %Hh%M")}}
+                enregistré le {{time.strftime(scu.DATEATIME_FMT)}}
             </li>
         </ul>
 
diff --git a/app/templates/formation/ue_assoc_parcours_ects.j2 b/app/templates/formation/ue_assoc_parcours_ects.j2
index 290d0d169..c4ee778fa 100644
--- a/app/templates/formation/ue_assoc_parcours_ects.j2
+++ b/app/templates/formation/ue_assoc_parcours_ects.j2
@@ -1,5 +1,5 @@
 {# Association d'ECTS à une UE par parcours #}
-{% extends "sco_page.j2" %}
+{% extends "sco_page_dept.j2" %}
 {% import 'wtf.j2' as wtf %}
 
 {% block styles %}
@@ -23,18 +23,20 @@
 <form method="POST">
   {% for field in form %}
     {% if field.name != 'csrf_token' %}
-        <div>
-        <label for="{{ field.id }}">{{ field.label }}</label>
-        {{ field }}
-        {% for error in field.errors %}
-          <div class="error-message">{{ error }}</div>
-        {% endfor %}
+        <div class="space-before-18">
+          <label for="{{ field.id }}">{{ field.label }}</label>
+          {{ field }}
+          {% for error in field.errors %}
+            <div class="error-message">{{ error }}</div>
+          {% endfor %}
         </div>
     {% endif %}
   {% endfor %}
   {{ form.csrf_token }}
-  <input type="submit" name="submit" value="Enregistrer">
-  <input type="submit" name="cancel" value="Annuler">
+  <div class="space-before-24">
+    <input type="submit" name="submit" value="Enregistrer">
+    <input type="submit" name="cancel" value="Annuler">
+  </div>
 </form>
 
 {% endblock %}
diff --git a/app/templates/formsemestre/bulletins_choice.j2 b/app/templates/formsemestre/bulletins_choice.j2
index db38985b1..20d5f81cf 100644
--- a/app/templates/formsemestre/bulletins_choice.j2
+++ b/app/templates/formsemestre/bulletins_choice.j2
@@ -20,6 +20,7 @@
 <div class="tab-content">
     <h2>{{ title }}</h2>
 
+    <div class="scobox">
     <p class="help">
         {{ explanation|safe }}
     </p>
@@ -46,5 +47,6 @@
             <input type="submit" value="Générer">
         </div>
     </form>
+    </div>
 </div>
 {% endblock %}
diff --git a/app/templates/pn/form_modules_ue_coefs.j2 b/app/templates/pn/form_modules_ue_coefs.j2
index 66ba286fc..92d316b1f 100644
--- a/app/templates/pn/form_modules_ue_coefs.j2
+++ b/app/templates/pn/form_modules_ue_coefs.j2
@@ -1,4 +1,26 @@
 {# -*- mode: jinja-html -*- #}
+{% extends "sco_page_dept.j2" %}
+
+
+{% block styles %}
+{{super()}}
+<link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/table_editor.css">
+{% endblock %}
+
+{% block app_content %}
+<h2>Formation {{formation.titre}} ({{formation.acronyme}})
+        [version {{formation.version}}] code {{formation.formation_code}}
+        {% if read_only %}
+            {{scu.icontag("lock32_img", title="verrouillé")|safe}}
+        {% endif %}
+</h2>
+
+{% if read_only %}
+<span class="warning">
+Formation verrouilée car un ou plusieurs semestres verrouillés l'utilisent.
+</span>
+{% endif %}
+
 <h2>{% if not read_only %}Édition des c{% else %}C{%endif%}oefficients des modules vers les UEs</h2>
 <div class="help">
     {% if not read_only %}
@@ -59,8 +81,11 @@
     <div class="champs_coef_hors_parcours champs_legende"></div>
     <div class="help">module non associé au parcours de cette UE, le coef devrait être nul.</div>
 </div>
+{% endblock %}
 
-
+{% block scripts %}
+{{super()}}
+<script src="{{scu.STATIC_DIR}}/js/table_editor.js"></script>
 <script>
     var read_only = {{ "true" if read_only else "false"}};
     $(function () {
@@ -113,3 +138,4 @@
         return true;
     }
 </script>
+{% endblock %}
\ No newline at end of file
diff --git a/app/views/pn_modules.py b/app/views/pn_modules.py
index cd9ec4db8..01f06ac87 100644
--- a/app/views/pn_modules.py
+++ b/app/views/pn_modules.py
@@ -31,24 +31,18 @@ PN / Edition des coefs
 Emmanuel Viennet, 2021
 """
 
-from flask import url_for
-from flask import g, request
+from flask import g, render_template, request, url_for
 from flask_json import as_json
 from flask_login import current_user
-from flask.templating import render_template
-from app.scodoc.codes_cursus import UE_SPORT
-
 
 from app import db, models
-
 from app.comp import moy_ue
 from app.decorators import scodoc, permission_required
 from app.models import ApcParcours, Formation, Module
-from app.views import notes_bp as bp
-
-from app.scodoc import html_sco_header
+from app.scodoc.codes_cursus import UE_SPORT
 from app.scodoc import sco_utils as scu
 from app.scodoc.sco_permissions import Permission
+from app.views import notes_bp as bp
 
 
 @bp.route("/table_modules_ue_coefs/<int:formation_id>")
@@ -197,51 +191,23 @@ def edit_modules_ue_coefs():
         formation_id=formation_id
     ).first_or_404()
     locked = formation.has_locked_sems(semestre_idx)
-    if locked:
-        lockicon = scu.icontag("lock32_img", title="verrouillé")
-    else:
-        lockicon = ""
-    H = [
-        html_sco_header.sco_header(
-            cssstyles=["css/table_editor.css"],
-            javascripts=[
-                "js/table_editor.js",
-            ],
-            page_title=f"Coefs programme {formation.acronyme}",
-        ),
-        f"""<h2>Formation {formation.titre} ({formation.acronyme})
-        [version {formation.version}] code {formation.formation_code}
-        {lockicon}
-        </h2>
-        """,
-        (
-            """<span class="warning">Formation verrouilée car un ou plusieurs
-        semestres verrouillés l'utilisent.
-        </span>"""
-            if locked
-            else ""
-        ),
-        render_template(
-            "pn/form_modules_ue_coefs.j2",
-            formation=formation,
-            data_source=url_for(
-                "notes.table_modules_ue_coefs",
-                scodoc_dept=g.scodoc_dept,
-                formation_id=formation_id,
-                semestre_idx=semestre_idx,
-                parcours_id=parcours_id,
-            ),
-            data_save=url_for(
-                "notes.set_module_ue_coef",
-                scodoc_dept=g.scodoc_dept,
-            ),
-            read_only=locked
-            or not current_user.has_permission(Permission.EditFormation),
+    return render_template(
+        "pn/form_modules_ue_coefs.j2",
+        formation=formation,
+        data_source=url_for(
+            "notes.table_modules_ue_coefs",
+            scodoc_dept=g.scodoc_dept,
+            formation_id=formation_id,
             semestre_idx=semestre_idx,
-            semestre_ids=range(1, formation.get_cursus().NB_SEM + 1),
             parcours_id=parcours_id,
         ),
-        html_sco_header.sco_footer(),
-    ]
-
-    return "\n".join(H)
+        data_save=url_for(
+            "notes.set_module_ue_coef",
+            scodoc_dept=g.scodoc_dept,
+        ),
+        read_only=locked or not current_user.has_permission(Permission.EditFormation),
+        semestre_idx=semestre_idx,
+        semestre_ids=range(1, formation.get_cursus().NB_SEM + 1),
+        parcours_id=parcours_id,
+        title=f"Coefs programme {formation.acronyme}",
+    )
diff --git a/sco_version.py b/sco_version.py
index b5372319f..42dab1159 100644
--- a/sco_version.py
+++ b/sco_version.py
@@ -1,7 +1,7 @@
 # -*- mode: python -*-
 # -*- coding: utf-8 -*-
 
-SCOVERSION = "9.7.6"
+SCOVERSION = "9.7.7"
 
 SCONAME = "ScoDoc"
 
-- 
GitLab