From 6c044dd4dd313e1b1b8bc9c347192c4be8ab968e Mon Sep 17 00:00:00 2001
From: Emmanuel Viennet <emmanuel.viennet@gmail.com>
Date: Sun, 25 Aug 2024 07:23:36 +0200
Subject: [PATCH] Adaptation multi-select groupes

---
 app/scodoc/TrivialFormulator.py         |   5 +
 app/scodoc/sco_archives_formsemestre.py |   2 +-
 app/scodoc/sco_evaluations.py           |   4 +-
 app/scodoc/sco_moduleimpl_status.py     |  12 +--
 app/scodoc/sco_pv_forms.py              | 119 ++++++++++++++----------
 app/scodoc/sco_report.py                |  36 +++++--
 app/scodoc/sco_saisie_excel.py          |  54 +++++++----
 app/scodoc/sco_saisie_notes.py          |  49 ++++++----
 app/scodoc/sco_trombino_doc.py          |   7 +-
 app/scodoc/sco_utils.py                 |   9 +-
 app/static/css/scodoc.css               |   4 +
 app/static/css/scodoc97.css             |   9 +-
 app/views/notes.py                      |  19 +++-
 13 files changed, 216 insertions(+), 113 deletions(-)

diff --git a/app/scodoc/TrivialFormulator.py b/app/scodoc/TrivialFormulator.py
index d9bd37d24..b15255105 100644
--- a/app/scodoc/TrivialFormulator.py
+++ b/app/scodoc/TrivialFormulator.py
@@ -40,6 +40,7 @@ def TrivialFormulator(
     submitbuttonattributes=None,
     top_buttons=False,  # place buttons at top of form
     bottom_buttons=True,  # buttons after form
+    html_head_markup="",
     html_foot_markup="",
     readonly=False,
     is_submitted=False,
@@ -116,6 +117,7 @@ def TrivialFormulator(
         submitbuttonattributes=submitbuttonattributes or [],
         top_buttons=top_buttons,
         bottom_buttons=bottom_buttons,
+        html_head_markup=html_head_markup,
         html_foot_markup=html_foot_markup,
         readonly=readonly,
         is_submitted=is_submitted,
@@ -152,6 +154,7 @@ class TF(object):
         submitbuttonattributes=None,
         top_buttons=False,  # place buttons at top of form
         bottom_buttons=True,  # buttons after form
+        html_head_markup="",  # html snippet put at the beginning, just before the table
         html_foot_markup="",  # html snippet put at the end, just after the table
         readonly=False,
         is_submitted=False,
@@ -178,6 +181,7 @@ class TF(object):
         self.submitbuttonattributes = submitbuttonattributes or []
         self.top_buttons = top_buttons
         self.bottom_buttons = bottom_buttons
+        self.html_head_markup = html_head_markup
         self.html_foot_markup = html_foot_markup
         self.title = title
         self.after_table = after_table
@@ -469,6 +473,7 @@ class TF(object):
         if self.top_buttons:
             R.append(buttons_markup + "<p></p>")
         R.append(self.before_table.format(title=self.title))
+        R.append(self.html_head_markup)
         R.append('<table class="tf">')
         for field, descr in self.formdescription:
             if descr.get("readonly", False):
diff --git a/app/scodoc/sco_archives_formsemestre.py b/app/scodoc/sco_archives_formsemestre.py
index 561d0b6cf..31548bcd6 100644
--- a/app/scodoc/sco_archives_formsemestre.py
+++ b/app/scodoc/sco_archives_formsemestre.py
@@ -289,7 +289,7 @@ enregistrés et non modifiables, on peut les retrouver ultérieurement.
     ]
     menu_choix_groupe = (
         """<div class="group_ids_sel_menu">Groupes d'étudiants à lister: """
-        + sco_groups_view.menu_groups_choice(groups_infos)
+        + sco_groups_view.menu_groups_choice(groups_infos, submit_on_change=True)
         + """(pour les PV et lettres)</div>"""
     )
 
diff --git a/app/scodoc/sco_evaluations.py b/app/scodoc/sco_evaluations.py
index 400729705..9c5fad6d7 100644
--- a/app/scodoc/sco_evaluations.py
+++ b/app/scodoc/sco_evaluations.py
@@ -732,7 +732,9 @@ def evaluation_describe(evaluation_id="", edit_in_place=True, link_saisie=True)
             H.append(
                 f"""
             <a style="margin-left: 12px;" class="stdlink" href="{url_for(
-                "notes.saisie_notes", scodoc_dept=g.scodoc_dept, evaluation_id=evaluation_id)
+                "notes.form_saisie_notes",
+                scodoc_dept=g.scodoc_dept,
+                evaluation_id=evaluation_id)
             }">saisie des notes</a>
             """
             )
diff --git a/app/scodoc/sco_moduleimpl_status.py b/app/scodoc/sco_moduleimpl_status.py
index 139081f19..ed1e256d2 100644
--- a/app/scodoc/sco_moduleimpl_status.py
+++ b/app/scodoc/sco_moduleimpl_status.py
@@ -72,7 +72,7 @@ def moduleimpl_evaluation_menu(evaluation: Evaluation, nbnotes: int = 0) -> str:
     menu_eval = [
         {
             "title": "Saisir les notes",
-            "endpoint": "notes.saisie_notes",
+            "endpoint": "notes.form_saisie_notes",
             "args": {
                 "evaluation_id": evaluation_id,
             },
@@ -745,7 +745,7 @@ def _ligne_evaluation(
         )
     if can_edit_notes:
         H.append(
-            f"""<a class="smallbutton" href="{url_for('notes.saisie_notes',
+            f"""<a class="smallbutton" href="{url_for('notes.form_saisie_notes',
                 scodoc_dept=g.scodoc_dept, evaluation_id=evaluation.id)
             }">{scu.icontag("notes_img", alt="saisie notes", title="Saisie des notes")}</a>"""
         )
@@ -824,7 +824,7 @@ def _ligne_evaluation(
         )
     else:
         H.append(
-            f"""<a class="redlink" href="{url_for('notes.saisie_notes',
+            f"""<a class="redlink" href="{url_for('notes.form_saisie_notes',
                 scodoc_dept=g.scodoc_dept, evaluation_id=evaluation.id)
                 }">saisir notes</a>
                 """
@@ -880,7 +880,7 @@ def _ligne_evaluation(
                     H.append("""[<font color="red">""")
                     if can_edit_notes:
                         H.append(
-                            f"""<a class="redlink" href="{url_for('notes.saisie_notes',
+                            f"""<a class="redlink" href="{url_for('notes.form_saisie_notes',
                             scodoc_dept=g.scodoc_dept, evaluation_id=evaluation.id,
                             **{'group_ids:list': gr_moyenne["group_id"]})
                             }">incomplet&nbsp;: terminer saisie</a></font>]"""
@@ -891,9 +891,9 @@ def _ligne_evaluation(
                 H.append("""<span class="redboldtext">&nbsp; """)
                 if can_edit_notes:
                     H.append(
-                        f"""<a class="redlink" href="{url_for('notes.saisie_notes',
+                        f"""<a class="redlink" href="{url_for('notes.form_saisie_notes',
                         scodoc_dept=g.scodoc_dept, evaluation_id=evaluation.id,
-                        **{'group_ids:list': gr_moyenne["group_id"]})
+                        **{'group_ids': gr_moyenne["group_id"]})
                         }">"""
                     )
                 H.append("pas de notes")
diff --git a/app/scodoc/sco_pv_forms.py b/app/scodoc/sco_pv_forms.py
index 02521cacf..ce8e97f99 100644
--- a/app/scodoc/sco_pv_forms.py
+++ b/app/scodoc/sco_pv_forms.py
@@ -50,6 +50,7 @@ from app.scodoc import sco_pdf
 from app.scodoc import sco_preferences
 from app.scodoc import sco_pv_pdf
 from app.scodoc import sco_pv_lettres_inviduelles
+from app.scodoc.sco_exceptions import ScoValueError
 from app.scodoc.gen_tables import GenTable
 from app.scodoc.codes_cursus import NO_SEMESTRE_ID
 from app.scodoc.sco_pdf import PDFLOCK
@@ -336,12 +337,20 @@ def formsemestre_pvjury(formsemestre_id, fmt="html", publish=True):
 # ---------------------------------------------------------------------------
 
 
-def formsemestre_pvjury_pdf(formsemestre_id, group_ids: list[int] = None, etudid=None):
-    """Generation PV jury en PDF: saisie des paramètres
-    Si etudid, PV pour un seul etudiant. Sinon, tout les inscrits au groupe indiqué.
+def formsemestre_pvjury_pdf(formsemestre_id, etudid=None):
+    """Génération PV jury en PDF: saisie des paramètres
+    Si etudid, PV pour un seul etudiant.
+    Sinon, tout les inscrits au(x) groupe(s) indiqué(s).
     """
-    group_ids = group_ids or []
-    formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
+    if request.method == "POST":
+        group_ids = request.form.getlist("group_ids")
+    else:
+        group_ids = request.args.getlist("group_ids")
+    try:
+        group_ids = [int(gid) for gid in group_ids]
+    except ValueError as exc:
+        raise ScoValueError("group_ids invalide") from exc
+    formsemestre: FormSemestre = FormSemestre.get_formsemestre(formsemestre_id)
     # Mise à jour des groupes d'étapes:
     sco_groups.create_etapes_partition(formsemestre_id)
     groups_infos = None
@@ -361,7 +370,8 @@ def formsemestre_pvjury_pdf(formsemestre_id, group_ids: list[int] = None, etudid
         etudids = [m["etudid"] for m in groups_infos.members]
 
     H = [
-        f"""<div class="help">Utiliser cette page pour éditer des versions provisoires des PV.
+        f"""<div class="help space-after-24">
+        Utiliser cette page pour éditer des versions provisoires des PV.
           <span class="fontred">Il est recommandé d'archiver les versions définitives:
            <a class="stdlink" href="{url_for(
             'notes.formsemestre_archive',
@@ -381,7 +391,7 @@ def formsemestre_pvjury_pdf(formsemestre_id, group_ids: list[int] = None, etudid
     if groups_infos:
         menu_choix_groupe = (
             """<div class="group_ids_sel_menu">Groupes d'étudiants à lister sur le PV: """
-            + sco_groups_view.menu_groups_choice(groups_infos)
+            + sco_groups_view.menu_groups_choice(groups_infos, submit_on_change=True)
             + """</div>"""
         )
     else:
@@ -394,24 +404,28 @@ def formsemestre_pvjury_pdf(formsemestre_id, group_ids: list[int] = None, etudid
         submitlabel="Générer document",
         name="tf",
         formid="group_selector",
-        html_foot_markup=menu_choix_groupe,
+        html_head_markup=menu_choix_groupe,
     )
     if tf[0] == 0:
+        info_etud = (
+            f"""de <a class="discretelink" href="{
+            url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
+            }">{etud.nomprenom}</a>"""
+            if etud
+            else ""
+        )
         return render_template(
             "sco_page.j2",
             title=f"Édition du PV de jury {('de ' + etud.nom_prenom()) if etud else ''}",
             content=f"""<h2 class="formsemestre">Édition du PV de jury
-            de <a class="discretelink" href="{
-            url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
-            }">{etud.nomprenom}</a></h2>"""
+            {info_etud}</h2>"""
             + "\n".join(H)
             + "\n"
             + tf[1]
             + "\n".join(F),
-            javascripts=sco_groups_view.JAVASCRIPTS,
-            cssstyles=sco_groups_view.CSSSTYLES,
+            javascripts=["js/groups_view.js"],
         )
-    elif tf[0] == -1:
+    if tf[0] == -1:
         return flask.redirect(
             url_for(
                 "notes.formsemestre_pvjury",
@@ -419,34 +433,34 @@ def formsemestre_pvjury_pdf(formsemestre_id, group_ids: list[int] = None, etudid
                 formsemestre_id=formsemestre_id,
             )
         )
+
+    # submit
+    tf[2]["show_title"] = bool(tf[2]["show_title"])
+    tf[2]["anonymous"] = bool(tf[2]["anonymous"])
+    try:
+        PDFLOCK.acquire()
+        pdfdoc = sco_pv_pdf.pvjury_pdf(
+            formsemestre,
+            etudids,
+            numero_arrete=tf[2]["numero_arrete"],
+            code_vdi=tf[2]["code_vdi"],
+            date_commission=tf[2]["date_commission"],
+            date_jury=tf[2]["date_jury"],
+            show_title=tf[2]["show_title"],
+            pv_title_session=tf[2]["pv_title_session"],
+            pv_title=tf[2]["pv_title"],
+            with_paragraph_nom=tf[2]["with_paragraph_nom"],
+            anonymous=tf[2]["anonymous"],
+        )
+    finally:
+        PDFLOCK.release()
+    date_iso = time.strftime("%Y-%m-%d")
+    if groups_infos:
+        groups_filename = "-" + groups_infos.groups_filename
     else:
-        # submit
-        tf[2]["show_title"] = bool(tf[2]["show_title"])
-        tf[2]["anonymous"] = bool(tf[2]["anonymous"])
-        try:
-            PDFLOCK.acquire()
-            pdfdoc = sco_pv_pdf.pvjury_pdf(
-                formsemestre,
-                etudids,
-                numero_arrete=tf[2]["numero_arrete"],
-                code_vdi=tf[2]["code_vdi"],
-                date_commission=tf[2]["date_commission"],
-                date_jury=tf[2]["date_jury"],
-                show_title=tf[2]["show_title"],
-                pv_title_session=tf[2]["pv_title_session"],
-                pv_title=tf[2]["pv_title"],
-                with_paragraph_nom=tf[2]["with_paragraph_nom"],
-                anonymous=tf[2]["anonymous"],
-            )
-        finally:
-            PDFLOCK.release()
-        date_iso = time.strftime("%Y-%m-%d")
-        if groups_infos:
-            groups_filename = "-" + groups_infos.groups_filename
-        else:
-            groups_filename = ""
-        filename = f"""PV-{formsemestre.titre_num()}{groups_filename}-{date_iso}.pdf"""
-        return scu.sendPDFFile(pdfdoc, filename)
+        groups_filename = ""
+    filename = f"""PV-{formsemestre.titre_num()}{groups_filename}-{date_iso}.pdf"""
+    return scu.sendPDFFile(pdfdoc, filename)
 
 
 def descrform_pvjury(formsemestre: FormSemestre):
@@ -542,9 +556,17 @@ def descrform_pvjury(formsemestre: FormSemestre):
     ]
 
 
-def formsemestre_lettres_individuelles(formsemestre_id, group_ids=()):
+def formsemestre_lettres_individuelles(formsemestre_id):
     "Lettres avis jury en PDF"
     formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
+    if request.method == "POST":
+        group_ids = request.form.getlist("group_ids")
+    else:
+        group_ids = request.args.getlist("group_ids")
+    try:
+        group_ids = [int(gid) for gid in group_ids]
+    except ValueError as exc:
+        raise ScoValueError("group_ids invalide") from exc
     if not group_ids:
         # tous les inscrits du semestre
         group_ids = [sco_groups.get_default_group(formsemestre_id)]
@@ -556,20 +578,22 @@ def formsemestre_lettres_individuelles(formsemestre_id, group_ids=()):
     H = [
         f"""
         <h2 class="formsemestre">Édition des lettres individuelles</h2>
-        <p class="help">Utiliser cette page pour éditer des versions provisoires des PV.
+        <div class="help space-after-24">
+        Utiliser cette page pour éditer des versions provisoires des PV.
           <span class="fontred">Il est recommandé d'archiver les versions définitives: <a
           href="{url_for(
                 "notes.formsemestre_archive",
                 scodoc_dept=g.scodoc_dept,
                 formsemestre_id=formsemestre_id,
             )}"
-          >voir cette page</a></span></p>
+          >voir cette page</a></span>
+        </div>
          """,
     ]
     descr = descrform_lettres_individuelles()
     menu_choix_groupe = (
         """<div class="group_ids_sel_menu">Groupes d'étudiants à lister: """
-        + sco_groups_view.menu_groups_choice(groups_infos)
+        + sco_groups_view.menu_groups_choice(groups_infos, submit_on_change=True)
         + """</div>"""
     )
 
@@ -581,15 +605,14 @@ def formsemestre_lettres_individuelles(formsemestre_id, group_ids=()):
         submitlabel="Générer document",
         name="tf",
         formid="group_selector",
-        html_foot_markup=menu_choix_groupe,
+        html_head_markup=menu_choix_groupe,
     )
     if tf[0] == 0:
         return render_template(
             "sco_page.j2",
             title="Édition des lettres individuelles",
             content="\n".join(H) + "\n" + tf[1],
-            javascripts=sco_groups_view.JAVASCRIPTS,
-            cssstyles=sco_groups_view.CSSSTYLES,
+            javascripts=["js/groups_view.js"],
         )
     elif tf[0] == -1:
         return flask.redirect(
diff --git a/app/scodoc/sco_report.py b/app/scodoc/sco_report.py
index 0984c8c3a..f234786ff 100644
--- a/app/scodoc/sco_report.py
+++ b/app/scodoc/sco_report.py
@@ -296,6 +296,14 @@ def formsemestre_report_counts(
         sinon liste prédéfinie (voir ci-dessous)
     """
     formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
+    if request.method == "POST":
+        group_ids = request.form.getlist("group_ids")
+    else:
+        group_ids = request.args.getlist("group_ids")
+    try:
+        group_ids = [int(gid) for gid in group_ids]
+    except ValueError as exc:
+        raise ScoValueError("group_ids invalide") from exc
     groups_infos = sco_groups_view.DisplayedGroupsInfos(
         group_ids,
         formsemestre_id=formsemestre.id,
@@ -420,8 +428,7 @@ def formsemestre_report_counts(
     ]
     return render_template(
         "sco_page.j2",
-        cssstyles=sco_groups_view.CSSSTYLES,
-        javascripts=sco_groups_view.JAVASCRIPTS,
+        javascripts=["js/groups_view.js"],
         title=title,
         content="\n".join(H),
     )
@@ -740,7 +747,6 @@ def table_suivi_cohorte(
 def formsemestre_suivi_cohorte(
     formsemestre_id,
     fmt="html",
-    group_ids: list[int] = None,  # si indiqué, ne prend que ces groupes
     percent=1,
     bac="",
     bacspecialite="",
@@ -759,6 +765,14 @@ def formsemestre_suivi_cohorte(
         raise ScoValueError("formsemestre_suivi_cohorte: argument invalide") from exc
 
     formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
+    if request.method == "POST":
+        group_ids = request.form.getlist("group_ids")
+    else:
+        group_ids = request.args.getlist("group_ids")
+    try:
+        group_ids = [int(gid) for gid in group_ids]
+    except ValueError as exc:
+        raise ScoValueError("group_ids invalide") from exc
     groups_infos = sco_groups_view.DisplayedGroupsInfos(
         group_ids,
         formsemestre_id=formsemestre.id,
@@ -850,8 +864,7 @@ def formsemestre_suivi_cohorte(
     ]
     return render_template(
         "sco_page.j2",
-        cssstyles=sco_groups_view.CSSSTYLES,
-        javascripts=sco_groups_view.JAVASCRIPTS,
+        javascripts=["js/groups_view.js"],
         title=tab.page_title,
         content="\n".join(H),
     )
@@ -1629,7 +1642,6 @@ def graph_cursus(
 
 def formsemestre_graph_cursus(
     formsemestre_id,
-    group_ids: list[int] = None,  # si indiqué, ne prend que ces groupes
     fmt="html",
     only_primo=False,
     bac="",  # selection sur type de bac
@@ -1644,6 +1656,15 @@ def formsemestre_graph_cursus(
     annee_bac = str(annee_bac or "")
     annee_admission = str(annee_admission or "")
     formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
+    if request.method == "POST":
+        group_ids = request.form.getlist("group_ids")
+    else:
+        group_ids = request.args.getlist("group_ids")
+    try:
+        group_ids = [int(gid) for gid in group_ids]
+    except ValueError as exc:
+        raise ScoValueError("group_ids invalide") from exc
+
     groups_infos = sco_groups_view.DisplayedGroupsInfos(
         group_ids,
         formsemestre_id=formsemestre.id,
@@ -1781,8 +1802,7 @@ def formsemestre_graph_cursus(
         ]
         return render_template(
             "sco_page.j2",
-            cssstyles=sco_groups_view.CSSSTYLES,
-            javascripts=sco_groups_view.JAVASCRIPTS,
+            javascripts=["js/groups_view.js"],
             page_title=f"Graphe cursus de {sem['titreannee']}",
             no_sidebar=True,
             content="\n".join(H),
diff --git a/app/scodoc/sco_saisie_excel.py b/app/scodoc/sco_saisie_excel.py
index 6aa6a86f9..f46d4a6dc 100644
--- a/app/scodoc/sco_saisie_excel.py
+++ b/app/scodoc/sco_saisie_excel.py
@@ -68,7 +68,9 @@ from app.views import ScoData
 FONT_NAME = "Arial"
 
 
-def excel_feuille_saisie(evaluation: "Evaluation", rows: list[dict]) -> AnyStr:
+def excel_feuille_saisie(
+    evaluation: "Evaluation", rows: list[dict], groups_titles: str = ""
+) -> AnyStr:
     """Génère feuille excel pour saisie des notes dans l'evaluation
     - evaluation
     - rows: liste de dict
@@ -77,7 +79,9 @@ def excel_feuille_saisie(evaluation: "Evaluation", rows: list[dict]) -> AnyStr:
     """
     ws = ScoExcelSheet("Saisie notes")
     styles = _build_styles()
-    nb_lines_titles = _insert_top_title(ws, styles, evaluation=evaluation)
+    nb_lines_titles = _insert_top_title(
+        ws, styles, evaluation=evaluation, groups_titles=groups_titles
+    )
 
     _insert_line_titles(
         ws,
@@ -263,6 +267,7 @@ def _insert_top_title(
     evaluation: Evaluation | None = None,
     formsemestre: FormSemestre | None = None,
     description="",
+    groups_titles: str = "",
 ) -> int:
     """Insère les lignes de titre de la feuille (suivies d'une ligne blanche).
     Si evaluation, indique son titre.
@@ -298,7 +303,7 @@ def _insert_top_title(
         evaluation.moduleimpl.formsemestre.titre_annee()
         if evaluation
         else (formsemestre.titre_annee() if formsemestre else "")
-    )
+    ) + ((" - " + groups_titles) if groups_titles else "")
     ws.append_single_cell_row(
         scu.unescape_html(titre_annee), styles["titres"], prefix=[""]
     )
@@ -372,15 +377,16 @@ def _insert_bottom_help(ws, styles: dict):
     )
 
 
-def feuille_saisie_notes(
-    evaluation_id, group_ids: list[int] = None
-):  # TODO ré-écrire et passer dans notes.py
+def feuille_saisie_notes(evaluation_id: int):  # TODO ré-écrire et passer dans notes.py
     """Vue: document Excel pour saisie notes dans l'évaluation et les groupes indiqués"""
     evaluation = Evaluation.get_evaluation(evaluation_id)
-    group_ids = group_ids or []
+    group_ids = request.args.getlist("group_ids") or []
+    try:
+        group_ids = [int(gid) for gid in group_ids]
+    except ValueError as exc:
+        raise ScoValueError("group_ids invalide") from exc
     modimpl = evaluation.moduleimpl
     formsemestre = modimpl.formsemestre
-
     if evaluation.date_debut:
         indication_date = evaluation.date_debut.date().isoformat()
     else:
@@ -430,7 +436,9 @@ def feuille_saisie_notes(
 
     eval_name = f"{evaluation.moduleimpl.module.code}-{indication_date}"
     filename = f"notes_{eval_name}_{gr_title_filename}"
-    xls = excel_feuille_saisie(evaluation, rows=rows)
+    xls = excel_feuille_saisie(
+        evaluation, rows=rows, groups_titles=groups_infos.groups_titles
+    )
     return scu.send_file(xls, filename, scu.XLSX_SUFFIX, mime=scu.XLSX_MIMETYPE)
 
 
@@ -962,9 +970,14 @@ def _get_sheet_evaluations(
     raise ValueError("_get_sheet_evaluations")
 
 
-def saisie_notes_tableur(evaluation_id: int, group_ids=()):
+def saisie_notes_tableur(evaluation_id: int):
     """Saisie des notes via un fichier Excel"""
-    evaluation = Evaluation.query.get_or_404(evaluation_id)
+    group_ids = request.args.getlist("group_ids")
+    try:
+        group_ids = [int(gid) for gid in group_ids]
+    except ValueError as exc:
+        raise ScoValueError("group_ids invalide") from exc
+    evaluation = Evaluation.get_evaluation(evaluation_id)
     moduleimpl_id = evaluation.moduleimpl.id
     formsemestre_id = evaluation.moduleimpl.formsemestre_id
     if not evaluation.moduleimpl.can_edit_notes(current_user):
@@ -1004,18 +1017,20 @@ def saisie_notes_tableur(evaluation_id: int, group_ids=()):
 
     # Menu choix groupe:
     H.append("""<div id="group-tabs"><table><tr><td>""")
-    H.append(sco_groups_view.form_groups_choice(groups_infos))
+    H.append(sco_groups_view.form_groups_choice(groups_infos, submit_on_change=True))
     H.append("</td></tr></table></div>")
 
     H.append(
         f"""<div class="saisienote_etape1">
         <span class="titredivsaisienote">Étape 1 : </span>
         <ul>
-        <li><a class="stdlink" href="feuille_saisie_notes?evaluation_id={evaluation_id}&{
+        <li><a class="stdlink" href="{
+            url_for('notes.feuille_saisie_notes',
+                scodoc_dept=g.scodoc_dept, evaluation_id=evaluation_id)}&{
             groups_infos.groups_query_args}"
             id="lnk_feuille_saisie">obtenir le fichier tableur à remplir</a>
         </li>
-        <li>ou <a class="stdlink" href="{url_for("notes.saisie_notes",
+        <li>ou <a class="stdlink" href="{url_for("notes.form_saisie_notes",
             scodoc_dept=g.scodoc_dept, evaluation_id=evaluation_id)
         }">aller au formulaire de saisie</a></li>
         </ul>
@@ -1085,7 +1100,7 @@ def saisie_notes_tableur(evaluation_id: int, group_ids=()):
                 scodoc_dept=g.scodoc_dept, evaluation_id=evaluation.id)
             }">Charger un autre fichier de notes</a>
             &nbsp;&nbsp;&nbsp;
-            <a class="stdlink" href="{url_for("notes.saisie_notes",
+            <a class="stdlink" href="{url_for("notes.form_saisie_notes",
                 scodoc_dept=g.scodoc_dept, evaluation_id=evaluation.id)
             }">Formulaire de saisie des notes</a>
             </div>"""
@@ -1113,7 +1128,9 @@ def saisie_notes_tableur(evaluation_id: int, group_ids=()):
         <div>
         <ul>
         <li>
-        <form action="do_evaluation_set_missing" method="POST">
+        <form action="{
+            url_for("notes.do_evaluation_set_missing", scodoc_dept=g.scodoc_dept)
+        }" method="POST">
         Mettre toutes les notes manquantes à <input type="text" size="5" name="value"/>
         <input type="submit" value="OK"/>
         <input type="hidden" name="evaluation_id" value="{evaluation_id}"/>
@@ -1129,7 +1146,7 @@ def saisie_notes_tableur(evaluation_id: int, group_ids=()):
             scodoc_dept=g.scodoc_dept, moduleimpl_id=moduleimpl_id)
         }">Revenir au module</a>
         </li>
-        <li><a class="stdlink" href="{url_for("notes.saisie_notes",
+        <li><a class="stdlink" href="{url_for("notes.form_saisie_notes",
             scodoc_dept=g.scodoc_dept, evaluation_id=evaluation.id)
         }">Revenir au formulaire de saisie</a>
         </li>
@@ -1176,8 +1193,7 @@ def saisie_notes_tableur(evaluation_id: int, group_ids=()):
         "sco_page.j2",
         content="\n".join(H),
         page_title=page_title,
-        javascripts=sco_groups_view.JAVASCRIPTS,
-        cssstyles=sco_groups_view.CSSSTYLES,
+        javascripts=["js/groups_view.js"],
     )
 
 
diff --git a/app/scodoc/sco_saisie_notes.py b/app/scodoc/sco_saisie_notes.py
index 3eb0b346f..10dfed91e 100644
--- a/app/scodoc/sco_saisie_notes.py
+++ b/app/scodoc/sco_saisie_notes.py
@@ -54,7 +54,6 @@ from app.scodoc.sco_exceptions import (
     AccessDenied,
     NoteProcessError,
     ScoException,
-    ScoInvalidParamError,
     ScoValueError,
 )
 from app.scodoc import htmlutils
@@ -71,6 +70,7 @@ from app.scodoc.TrivialFormulator import TF
 import app.scodoc.sco_utils as scu
 from app.scodoc.sco_utils import json_error
 from app.scodoc.sco_utils import ModuleType
+from app.views import ScoData
 
 
 def convert_note_from_string(
@@ -212,7 +212,7 @@ def do_evaluation_set_missing(
     evaluation_id, value, dialog_confirmed=False, group_ids_str: str = ""
 ):
     """Initialisation des notes manquantes"""
-    evaluation = Evaluation.query.get_or_404(evaluation_id)
+    evaluation = Evaluation.get_evaluation(evaluation_id)
     modimpl = evaluation.moduleimpl
     # Check access
     # (admin, respformation, and responsable_id)
@@ -222,8 +222,12 @@ def do_evaluation_set_missing(
     notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id)
     if not group_ids_str:
         groups = None
+        groups_infos = None
     else:
         group_ids = [int(x) for x in str(group_ids_str).split(",")]
+        groups_infos = sco_groups_view.DisplayedGroupsInfos(
+            group_ids, formsemestre_id=modimpl.formsemestre.id
+        )
         groups = sco_groups.listgroups(group_ids)
 
     etudid_etats = sco_groups.do_evaluation_listeetuds_groups(
@@ -240,7 +244,9 @@ def do_evaluation_set_missing(
     # Convert and check values
     valid_notes, invalids, _, _, _ = check_notes(notes, evaluation)
     dest_url = url_for(
-        "notes.saisie_notes", scodoc_dept=g.scodoc_dept, evaluation_id=evaluation_id
+        "notes.form_saisie_notes",
+        scodoc_dept=g.scodoc_dept,
+        evaluation_id=evaluation_id,
     )
     diag = ""
     if len(invalids) > 0:
@@ -260,12 +266,17 @@ def do_evaluation_set_missing(
         plural = len(valid_notes) > 1
         return scu.confirm_dialog(
             f"""<h2>Mettre toutes les notes manquantes de l'évaluation
-            à la valeur {value} ?</h2>
+            à la valeur <span class="fontred">{value} / {evaluation.note_max:g}</span> ?</h2>
             <p>Seuls les étudiants pour lesquels aucune note (ni valeur, ni ABS, ni EXC)
             n'a été rentrée seront affectés.</p>
-            <p><b>{len(valid_notes)} étudiant{"s" if plural else ""} concerné{"s" if plural else ""}
+            <div>
+            <b>Groupes: {groups_infos.groups_titles if groups_infos else "tous"},
+            dont
+            <span class="fontred">
+            {len(valid_notes)} étudiant{"s" if plural else ""} concerné{"s" if plural else ""}
+            </span>
             par ce changement de note.</b>
-            </p>
+            </div>
             """,
             dest_url="",
             cancel_url=dest_url,
@@ -624,14 +635,8 @@ def _record_note(
 
 
 # Nouveau formulaire saisie notes (2016)
-def saisie_notes(evaluation_id: int, group_ids: list = None):
+def saisie_notes(evaluation: Evaluation, group_ids: list[int] | tuple[int] = ()):
     """Formulaire saisie notes d'une évaluation pour un groupe"""
-    if not isinstance(evaluation_id, int):
-        raise ScoInvalidParamError()
-    group_ids = [int(group_id) for group_id in (group_ids or [])]
-    evaluation: Evaluation = db.session.get(Evaluation, evaluation_id)
-    if evaluation is None:
-        raise ScoValueError("évaluation inexistante")
     modimpl = evaluation.moduleimpl
     moduleimpl_status_url = url_for(
         "notes.moduleimpl_status",
@@ -660,7 +665,6 @@ def saisie_notes(evaluation_id: int, group_ids: list = None):
         select_all_when_unspecified=True,
         etat=None,
     )
-
     page_title = (
         f'Saisie "{evaluation.description}"'
         if evaluation.description
@@ -669,12 +673,12 @@ def saisie_notes(evaluation_id: int, group_ids: list = None):
     # HTML page:
     H = [
         sco_evaluations.evaluation_describe(
-            evaluation_id=evaluation_id, link_saisie=False
+            evaluation_id=evaluation.id, link_saisie=False
         ),
         '<div id="saisie_notes"><span class="eval_title">Saisie des notes</span>',
     ]
     H.append("""<div id="group-tabs"><table><tr><td>""")
-    H.append(sco_groups_view.form_groups_choice(groups_infos))
+    H.append(sco_groups_view.form_groups_choice(groups_infos, submit_on_change=True))
     H.append('</td><td style="padding-left: 35px;">')
     H.append(
         htmlutils.make_menu(
@@ -755,7 +759,8 @@ def saisie_notes(evaluation_id: int, group_ids: list = None):
         "sco_page.j2",
         content="\n".join(H),
         title=page_title,
-        javascripts=["js/saisie_notes.js"],
+        javascripts=["js/groups_view.js", "js/saisie_notes.js"],
+        sco=ScoData(formsemestre=modimpl.formsemestre),
     )
 
 
@@ -839,11 +844,15 @@ def _form_saisie_notes(
     """
     formsemestre_id = modimpl.formsemestre_id
     formsemestre: FormSemestre = evaluation.moduleimpl.formsemestre
+    groups = sco_groups.listgroups(groups_infos.group_ids)
     res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
     etudids = [
         x[0]
         for x in sco_groups.do_evaluation_listeetuds_groups(
-            evaluation.id, getallstudents=True, include_demdef=True
+            evaluation.id,
+            groups=groups,
+            getallstudents=groups is None,
+            include_demdef=True,
         )
     ]
     if not etudids:
@@ -1001,7 +1010,9 @@ def _form_saisie_notes(
         H.append(
             f"""
         <div>
-        <form id="do_evaluation_set_missing" action="do_evaluation_set_missing" method="POST">
+        <form id="do_evaluation_set_missing" action="{
+            url_for("notes.do_evaluation_set_missing", scodoc_dept=g.scodoc_dept)
+        }" method="POST">
         Mettre les notes manquantes à
         <input type="text" size="5" name="value"/>
         <input type="submit" value="OK"/>
diff --git a/app/scodoc/sco_trombino_doc.py b/app/scodoc/sco_trombino_doc.py
index 91b573e5a..b6517ca99 100644
--- a/app/scodoc/sco_trombino_doc.py
+++ b/app/scodoc/sco_trombino_doc.py
@@ -12,7 +12,6 @@ from docx.shared import Mm
 from docx.enum.text import WD_ALIGN_PARAGRAPH
 from docx.enum.table import WD_ALIGN_VERTICAL
 
-from app.scodoc import sco_etud
 from app.scodoc import sco_photos
 import app.scodoc.sco_utils as scu
 import sco_version
@@ -31,9 +30,9 @@ def trombino_doc(groups_infos):
     )
     section = document.sections[0]
     footer = section.footer
-    footer.paragraphs[
-        0
-    ].text = f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}"
+    footer.paragraphs[0].text = (
+        f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}"
+    )
 
     nb_images = len(groups_infos.members)
     table = document.add_table(rows=2 * (nb_images // N_PER_ROW + 1), cols=N_PER_ROW)
diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py
index f75090cd6..245b7d5b6 100644
--- a/app/scodoc/sco_utils.py
+++ b/app/scodoc/sco_utils.py
@@ -1543,15 +1543,18 @@ def confirm_dialog(
     H = [
         f"""<form {action} method="POST">
         {message}
+
+        <div class="form-group space-before-24">
         """,
     ]
     if OK or not cancel_url:
-        H.append(f'<input type="submit" value="{OK}"/>')
+        H.append(f'<input class="btn btn-default" type="submit" value="{OK}"/>')
     if cancel_url:
         H.append(
-            f"""<input type ="button" value="{cancel_label}"
-            onClick="document.location='{cancel_url}';"/>"""
+            f"""<input class="btn btn-default" type="submit" name="cancel" type ="button" value="{cancel_label}"
+            onClick="event.preventDefault(); document.location='{cancel_url}';"/>"""
         )
+    H.append("</div>")
     for param in parameters.keys():
         if parameters[param] is None:
             parameters[param] = ""
diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css
index bbf47a87f..835140365 100644
--- a/app/static/css/scodoc.css
+++ b/app/static/css/scodoc.css
@@ -66,6 +66,10 @@ div.sco-app-content {
   margin-top: 24px !important;
 }
 
+.space-after-24 {
+  margin-bottom: 24px !important;
+}
+
 div.scobox.maxwidth {
   max-width: none;
 }
diff --git a/app/static/css/scodoc97.css b/app/static/css/scodoc97.css
index 6e7917807..43dd369f2 100644
--- a/app/static/css/scodoc97.css
+++ b/app/static/css/scodoc97.css
@@ -441,8 +441,13 @@ textarea {
     transition: border-color 0.2s ease-in-out;
 }
 
-.form-group input[type="submit"] {
-    max-width: var(--sco-content-max-width);
+/* Media query for desktop devices
+ évite que les boutons submit ne deviennent trop larges
+ */
+@media (min-width: 769px) {
+    .form-group input[type="submit"] {
+        width: 192px;
+    }
 }
 
 .form-group input:focus,
diff --git a/app/views/notes.py b/app/views/notes.py
index 3ed64a434..3ba9ef0ac 100644
--- a/app/views/notes.py
+++ b/app/views/notes.py
@@ -1836,7 +1836,6 @@ sco_publish(
     sco_saisie_excel.feuille_saisie_notes,
     Permission.EnsView,
 )
-sco_publish("/saisie_notes", sco_saisie_notes.saisie_notes, Permission.EnsView)
 sco_publish(
     "/do_evaluation_set_missing",
     sco_saisie_notes.do_evaluation_set_missing,
@@ -1851,6 +1850,20 @@ sco_publish(
 )
 
 
+@bp.route("/form_saisie_notes/<int:evaluation_id>")
+@scodoc
+@permission_required(Permission.EnsView)  # + controle contextuel
+def form_saisie_notes(evaluation_id: int):
+    "Formulaire de saisie des notes d'une évaluation"
+    evaluation = Evaluation.get_evaluation(evaluation_id)
+    group_ids = request.args.getlist("group_ids")
+    try:
+        group_ids = [int(gid) for gid in group_ids]
+    except ValueError as exc:
+        raise ScoValueError("group_ids invalide") from exc
+    return sco_saisie_notes.saisie_notes(evaluation, group_ids)
+
+
 @bp.route("/formsemestre_import_notes/<int:formsemestre_id>", methods=["GET", "POST"])
 @scodoc
 @permission_required(Permission.ScoView)  # controle contextuel
@@ -2117,7 +2130,9 @@ def _formsemestre_bulletins_choice(
         explanation=explanation,
         choose_mail=choose_mail,
         formsemestre=formsemestre,
-        menu_groups_choice=sco_groups_view.menu_groups_choice(groups_infos),
+        menu_groups_choice=sco_groups_view.menu_groups_choice(
+            groups_infos, submit_on_change=True
+        ),
         sco=ScoData(formsemestre=formsemestre),
         sco_groups_view=sco_groups_view,
         title=title,
-- 
GitLab