From 7ed20a8f8c689af715b3df18ab2732ac3b958edd Mon Sep 17 00:00:00 2001
From: Emmanuel Viennet <emmanuel.viennet@gmail.com>
Date: Tue, 21 Feb 2023 21:34:38 +0100
Subject: [PATCH] Test unitaire superficiel de (presque) toutes les vues du
 tableau de bord semestre

---
 app/models/formsemestre.py               |   5 +
 app/scodoc/sco_bulletins.py              |   3 +-
 app/scodoc/sco_cost_formation.py         |  54 ++++----
 app/scodoc/sco_formations.py             |   2 +-
 app/scodoc/sco_formsemestre_edit.py      |  14 --
 app/scodoc/sco_formsemestre_status.py    |  70 ++--------
 app/scodoc/sco_recapcomplet.py           |   5 +-
 app/static/css/gt_table.css              |   5 +-
 app/static/css/scodoc.css                |   8 +-
 app/templates/formsemestre_header.j2     |   2 +-
 app/templates/formsemestre_page_title.j2 |   2 +-
 app/views/notes.py                       |  39 +++---
 tests/unit/__init__.py                   |  18 ++-
 tests/unit/test_but_jury.py              |   6 +-
 tests/unit/test_formsemestre.py          | 156 ++++++++++++++++++++++-
 15 files changed, 246 insertions(+), 143 deletions(-)

diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py
index 9815b9fd4..6b832a882 100644
--- a/app/models/formsemestre.py
+++ b/app/models/formsemestre.py
@@ -257,6 +257,11 @@ class FormSemestre(db.Model):
         d["etapes_apo_str"] = self.etapes_apo_str()
         return d
 
+    def flip_lock(self):
+        """Flip etat (lock)"""
+        self.etat = not self.etat
+        db.session.add(self)
+
     def get_parcours_apc(self) -> list[ApcParcours]:
         """Liste des parcours proposés par ce semestre.
         Si aucun n'est coché et qu'il y a un référentiel, tous ceux du référentiel.
diff --git a/app/scodoc/sco_bulletins.py b/app/scodoc/sco_bulletins.py
index 96c85d69a..7230bffa1 100644
--- a/app/scodoc/sco_bulletins.py
+++ b/app/scodoc/sco_bulletins.py
@@ -908,7 +908,8 @@ def formsemestre_bulletinetud(
     )[0]
     if format not in {"html", "pdfmail"}:
         filename = scu.bul_filename(formsemestre, etud, format)
-        return scu.send_file(bulletin, filename, mime=scu.get_mime_suffix(format)[0])
+        mime, suffix = scu.get_mime_suffix(format)
+        return scu.send_file(bulletin, filename, mime=mime, suffix=suffix)
     elif format == "pdfmail":
         return ""
     H = [
diff --git a/app/scodoc/sco_cost_formation.py b/app/scodoc/sco_cost_formation.py
index 09d1e1229..16429881b 100644
--- a/app/scodoc/sco_cost_formation.py
+++ b/app/scodoc/sco_cost_formation.py
@@ -32,12 +32,12 @@
 """
 from flask import request
 
-import app.scodoc.sco_utils as scu
+from app.models import FormSemestre
 from app.scodoc.gen_tables import GenTable
 from app.scodoc import sco_formsemestre
 from app.scodoc import sco_moduleimpl
-from app.scodoc import sco_formsemestre_status
 from app.scodoc import sco_preferences
+import app.scodoc.sco_utils as scu
 import sco_version
 
 
@@ -57,37 +57,35 @@ def formsemestre_table_estim_cost(
     peut conduire à une sur-estimation du coût s'il y a des modules optionnels
     (dans ce cas, retoucher le tableau excel exporté).
     """
-    sem = sco_formsemestre.get_formsemestre(formsemestre_id)
-    sco_formsemestre_status.fill_formsemestre(sem)
-    Mlist = sco_moduleimpl.moduleimpl_withmodule_list(formsemestre_id=formsemestre_id)
-    T = []
-    for M in Mlist:
-        Mod = M["module"]
-        T.append(
+    formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
+
+    rows = []
+    for modimpl in formsemestre.modimpls:
+        rows.append(
             {
-                "code": Mod["code"] or "",
-                "titre": Mod["titre"],
-                "heures_cours": Mod["heures_cours"],
-                "heures_td": Mod["heures_td"] * n_group_td,
-                "heures_tp": Mod["heures_tp"] * n_group_tp,
+                "code": modimpl.module.code or "",
+                "titre": modimpl.module.titre,
+                "heures_cours": modimpl.module.heures_cours,
+                "heures_td": modimpl.module.heures_td * n_group_td,
+                "heures_tp": modimpl.module.heures_tp * n_group_tp,
             }
         )
 
     # calcul des heures:
-    for t in T:
+    for t in rows:
         t["HeqTD"] = (
             t["heures_td"] + coef_cours * t["heures_cours"] + coef_tp * t["heures_tp"]
         )
-    sum_cours = sum([t["heures_cours"] for t in T])
-    sum_td = sum([t["heures_td"] for t in T])
-    sum_tp = sum([t["heures_tp"] for t in T])
+    sum_cours = sum([t["heures_cours"] for t in rows])
+    sum_td = sum([t["heures_td"] for t in rows])
+    sum_tp = sum([t["heures_tp"] for t in rows])
     sum_heqtd = sum_td + coef_cours * sum_cours + coef_tp * sum_tp
-    assert abs(sum([t["HeqTD"] for t in T]) - sum_heqtd) < 0.01, "%s != %s" % (
-        sum([t["HeqTD"] for t in T]),
+    assert abs(sum([t["HeqTD"] for t in rows]) - sum_heqtd) < 0.01, "%s != %s" % (
+        sum([t["HeqTD"] for t in rows]),
         sum_heqtd,
     )
 
-    T.append(
+    rows.append(
         {
             "code": "TOTAL SEMESTRE",
             "heures_cours": sum_cours,
@@ -117,13 +115,15 @@ def formsemestre_table_estim_cost(
             "heures_tp",
             "HeqTD",
         ),
-        rows=T,
+        rows=rows,
         html_sortable=True,
         preferences=sco_preferences.SemPreferences(formsemestre_id),
         html_class="table_leftalign table_listegroupe",
         xls_before_table=[
-            ["%(titre)s %(num_sem)s %(modalitestr)s" % sem],
-            ["Formation %(titre)s version %(version)s" % sem["formation"]],
+            [formsemestre.titre_annee()],
+            [
+                f"Formation {formsemestre.formation.titre} version {formsemestre.formation.version}"
+            ],
             [],
             ["", "TD", "TP"],
             ["Nombre de groupes", n_group_td, n_group_tp],
@@ -140,10 +140,8 @@ def formsemestre_table_estim_cost(
     (dans ce cas, retoucher le tableau excel exporté).
     </div>
                     """,
-        origin="Généré par %s le " % sco_version.SCONAME
-        + scu.timedate_human_repr()
-        + "",
-        filename="EstimCout-S%s" % sem["semestre_id"],
+        origin=f"""Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}""",
+        filename=f"EstimCout-S{formsemestre.semestre_id}",
     )
     return tab
 
diff --git a/app/scodoc/sco_formations.py b/app/scodoc/sco_formations.py
index 84a6039be..29103d94c 100644
--- a/app/scodoc/sco_formations.py
+++ b/app/scodoc/sco_formations.py
@@ -485,7 +485,7 @@ def formation_list_table() -> GenTable:
             )
             + f""", <a class="stdlink" id="add-semestre-{
                 formation.acronyme.lower().replace(" ", "-")}"
-                href={ url_for("notes.formsemestre_createwithmodules",
+                href="{ url_for("notes.formsemestre_createwithmodules",
                 scodoc_dept=g.scodoc_dept, formation_id=formation.id, semestre_id=1
                 )
                 }">ajouter</a>
diff --git a/app/scodoc/sco_formsemestre_edit.py b/app/scodoc/sco_formsemestre_edit.py
index 584f591ad..3280b1be5 100644
--- a/app/scodoc/sco_formsemestre_edit.py
+++ b/app/scodoc/sco_formsemestre_edit.py
@@ -1659,20 +1659,6 @@ def formsemestre_edit_options(formsemestre_id):
     return sco_preferences.SemPreferences(formsemestre_id).edit(categories=["bul"])
 
 
-def formsemestre_change_lock(formsemestre_id) -> None:
-    """Change etat (verrouille si ouvert, déverrouille si fermé)
-    nota: etat (1 ouvert, 0 fermé)
-    """
-    ok, err = sco_permissions_check.check_access_diretud(formsemestre_id)
-    if not ok:
-        return err
-    sem = sco_formsemestre.get_formsemestre(formsemestre_id)
-    etat = not sem["etat"]
-
-    args = {"formsemestre_id": formsemestre_id, "etat": etat}
-    sco_formsemestre.do_formsemestre_edit(args)
-
-
 def formsemestre_change_publication_bul(
     formsemestre_id, dialog_confirmed=False, redirect=True
 ):
diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py
index d3ac77400..32478d0b5 100644
--- a/app/scodoc/sco_formsemestre_status.py
+++ b/app/scodoc/sco_formsemestre_status.py
@@ -200,7 +200,7 @@ def formsemestre_status_menubar(formsemestre: FormSemestre) -> str:
         },
         {
             "title": change_lock_msg,
-            "endpoint": "notes.formsemestre_change_lock",
+            "endpoint": "notes.formsemestre_flip_lock",
             "args": {"formsemestre_id": formsemestre_id},
             "enabled": has_perm_change_sem,
             "helpmsg": "",
@@ -546,59 +546,6 @@ def formsemestre_page_title(formsemestre_id=None):
     )
 
 
-def fill_formsemestre(sem):
-    """Add some useful fields to help display formsemestres"""
-    sem["notes_url"] = scu.NotesURL()
-    formsemestre_id = sem["formsemestre_id"]
-    if not sem["etat"]:
-        sem[
-            "locklink"
-        ] = f"""<a href="{url_for('notes.formsemestre_change_lock',
-            scodoc_dept=g.scodoc_dept,formsemestre_id=formsemestre_id )
-            }">{scu.icontag("lock_img", border="0", title="Semestre verrouillé")}</a>"""
-    else:
-        sem["locklink"] = ""
-    if sco_preferences.get_preference("bul_display_publication", formsemestre_id):
-        if sem["bul_hide_xml"]:
-            eyeicon = scu.icontag("hide_img", border="0", title="Bulletins NON publiés")
-        else:
-            eyeicon = scu.icontag("eye_img", border="0", title="Bulletins publiés")
-        sem[
-            "eyelink"
-        ] = f"""<a href="{
-            url_for('notes.formsemestre_change_publication_bul', 
-                scodoc_dept=g.scodoc_dept,formsemestre_id=formsemestre_id)
-        }">{eyeicon}</a>"""
-    else:
-        sem["eyelink"] = ""
-    sem["formation"] = Formation.query.get_or_404(sem["formation_id"]).to_dict(
-        with_departement=False
-    )
-    parcours = codes_cursus.get_cursus_from_code(F["type_parcours"])
-    if sem["semestre_id"] != -1:
-        sem["num_sem"] = f""", {parcours.SESSION_NAME} {sem["semestre_id"]}"""
-    else:
-        sem["num_sem"] = ""  # formation sans semestres
-    if sem["modalite"]:
-        sem["modalitestr"] = f""" en {sem["modalite"]}"""
-    else:
-        sem["modalitestr"] = ""
-
-    sem["etape_apo_str"] = "Code étape Apogée: " + (
-        sco_formsemestre.formsemestre_etape_apo_str(sem) or "Pas de code étape"
-    )
-
-    inscrits = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(
-        args={"formsemestre_id": formsemestre_id}
-    )
-    sem["nbinscrits"] = len(inscrits)
-    uresps = [
-        sco_users.user_info(responsable_id) for responsable_id in sem["responsables"]
-    ]
-    sem["resp"] = ", ".join([u["prenomnom"] for u in uresps])
-    sem["nomcomplet"] = ", ".join([u["nomcomplet"] for u in uresps])
-
-
 # Description du semestre sous forme de table exportable
 def formsemestre_description_table(
     formsemestre_id: int, with_evals=False, with_parcours=False
@@ -645,7 +592,7 @@ def formsemestre_description_table(
     titles["publish_incomplete_str"] = "Toujours utilisée"
     title = f"{parcours.SESSION_NAME.capitalize()} {formsemestre.titre_mois()}"
 
-    R = []
+    rows = []
     sum_coef = 0
     sum_ects = 0
     last_ue_id = None
@@ -671,7 +618,7 @@ def formsemestre_description_table(
             if use_ue_coefs:
                 ue_info["Coef."] = ue.coefficient
                 ue_info["Coef._class"] = "ue_coef"
-            R.append(ue_info)
+            rows.append(ue_info)
 
         mod_inscrits = sco_moduleimpl.do_moduleimpl_inscription_list(
             moduleimpl_id=modimpl.id
@@ -713,14 +660,15 @@ def formsemestre_description_table(
                 sorted([pa.code for pa in modimpl.module.parcours])
             )
 
-        R.append(l)
+        rows.append(l)
 
         if with_evals:
             # Ajoute lignes pour evaluations
             evals = nt.get_mod_evaluation_etat_list(modimpl.id)
             evals.reverse()  # ordre chronologique
             # Ajoute etat:
-            for e in evals:
+            for eval_dict in evals:
+                e = eval_dict.copy()
                 e["_jour_order"] = e["jour"].isoformat()
                 e["jour"] = e["jour"].strftime("%d/%m/%Y") if e["jour"] else ""
                 e["UE"] = l["UE"]
@@ -749,14 +697,14 @@ def formsemestre_description_table(
                     e[f"_ue_{ue_id}_class"] = "poids"
                     e[f"_ue_{ue_id}_help"] = "poids vers l'UE"
 
-            R += evals
+            rows += evals
 
     sums = {"_css_row_class": "moyenne sortbottom", "ects": sum_ects, "Coef.": sum_coef}
-    R.append(sums)
+    rows.append(sums)
 
     return GenTable(
         columns_ids=columns_ids,
-        rows=R,
+        rows=rows,
         titles=titles,
         origin=f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}",
         caption=title,
diff --git a/app/scodoc/sco_recapcomplet.py b/app/scodoc/sco_recapcomplet.py
index 011a57085..f03bab04c 100644
--- a/app/scodoc/sco_recapcomplet.py
+++ b/app/scodoc/sco_recapcomplet.py
@@ -294,9 +294,8 @@ def _formsemestre_recapcomplet_to_file(
             include_evaluations=include_evaluations,
             filename=filename,
         )
-        return scu.send_file(
-            data, filename=filename, mime=scu.get_mime_suffix(tabformat)
-        )
+        mime, suffix = scu.get_mime_suffix(tabformat)
+        return scu.send_file(data, filename=filename, mime=mime, suffix=suffix)
     elif tabformat == "xml":
         data = gen_formsemestre_recapcomplet_xml(
             formsemestre.id,
diff --git a/app/static/css/gt_table.css b/app/static/css/gt_table.css
index 90c9bf08c..f4b644df4 100644
--- a/app/static/css/gt_table.css
+++ b/app/static/css/gt_table.css
@@ -10,6 +10,7 @@
 table.dataTable {
   width: 100%;
   margin: 0 auto;
+  margin-left: 0px;
   clear: both;
   border-collapse: separate;
   border-spacing: 0;
@@ -643,10 +644,12 @@ table.dataTable.order-column.stripe.hover tbody tr.even:hover td.sorting_1 {
 /* Reglage largeur de la table */
 table.dataTable.gt_table {
   width: auto;
+  margin-left: 32px;
+  margin-top: 8px;
   padding-right: 5px;
 }
 
-/* Tables non centrées */
+/* Tables non centrées (inutile) */
 table.dataTable.gt_table.gt_left {
   margin-left: 16px;
 }
\ No newline at end of file
diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css
index 1a88fa544..9676f89e0 100644
--- a/app/static/css/scodoc.css
+++ b/app/static/css/scodoc.css
@@ -2069,9 +2069,11 @@ table#formation_list_table tr.gt_hl {
 table.formation_list_table td.buttons {
   white-space: nowrap;
 }
+
 table.formation_list_table td.buttons a {
   margin-left: 8px;
 }
+
 table.formation_list_table td.buttons span.but_placeholder {
   display: inline-block;
   width: 15px;
@@ -4036,7 +4038,8 @@ div.table_recap {
 } 
 */
 
-div.table_recap table.table_recap {
+div.table_recap table.table_recap,
+div.evaluations_recap table.evaluations_recap {
   width: auto;
   margin-left: 0px;
   /* font-family: Consolas, monaco, monospace; */
@@ -4230,9 +4233,10 @@ table.table_recap th.col_res {
   border-right: 1px dashed green;
   border-left: 1px dashed green;
 }
+
 table.table_recap td.abs,
 table.table_recap th.abs {
-  color:rgb(80, 0, 0);
+  color: rgb(80, 0, 0);
   border-right: 1px dashed green;
   border-left: 1px dashed green;
 }
diff --git a/app/templates/formsemestre_header.j2 b/app/templates/formsemestre_header.j2
index fae42f00f..a76858232 100644
--- a/app/templates/formsemestre_header.j2
+++ b/app/templates/formsemestre_header.j2
@@ -21,7 +21,7 @@
                 title="{{sco.sem.responsables_str(abbrev_prenom=False)}}">{{sco.sem.responsables_str()}}</a></span>
         <span class="nbinscrits"><a class="discretelink" href="{{url_for('scolar.groups_view', scodoc_dept=g.scodoc_dept,
         formsemestre_id=sco.sem.id)}}">{{sco.sem.inscriptions|length}} inscrits</a></span><span class="lock">{% if
-            not sco.sem.etat %}<a href="{{url_for('notes.formsemestre_change_lock', scodoc_dept=g.scodoc_dept,
+            not sco.sem.etat %}<a href="{{url_for('notes.formsemestre_flip_lock', scodoc_dept=g.scodoc_dept,
                 formsemestre_id=sco.sem.id)}}">{{sco.scu.icontag("lock_img", border="0", title="Semestre
                 verrouillé")|safe}}</a>{% endif %}</span><span class="eye">
             {% if sco.prefs["bul_display_publication"] %}
diff --git a/app/templates/formsemestre_page_title.j2 b/app/templates/formsemestre_page_title.j2
index 14c7f6ea5..95774dae4 100644
--- a/app/templates/formsemestre_page_title.j2
+++ b/app/templates/formsemestre_page_title.j2
@@ -21,7 +21,7 @@
             scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id)
         }}">{{formsemestre.etuds_inscriptions|length}} inscrits</a></span><span class="lock">
             {%-if not formsemestre.etat -%}
-            <a href="{{ url_for( 'notes.formsemestre_change_lock', 
+            <a href="{{ url_for( 'notes.formsemestre_flip_lock', 
         scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id )}}">{{
                 scu.icontag("lock_img", border="0", title="Semestre verrouillé")|safe
                 }}</a>
diff --git a/app/views/notes.py b/app/views/notes.py
index 545ba9f5b..2bf9b1723 100644
--- a/app/views/notes.py
+++ b/app/views/notes.py
@@ -820,20 +820,22 @@ sco_publish(
 )
 
 
-@bp.route("/formsemestre_change_lock", methods=["GET", "POST"])
+@bp.route("/formsemestre_flip_lock", methods=["GET", "POST"])
 @scodoc
-@permission_required(Permission.ScoView)  # acces vérifié dans la fonction
+@permission_required(Permission.ScoView)  # acces vérifié dans la vue
 @scodoc7func
-def formsemestre_change_lock(formsemestre_id, dialog_confirmed=False):
+def formsemestre_flip_lock(formsemestre_id, dialog_confirmed=False):
     "Changement de l'état de verrouillage du semestre"
-
+    formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
+    dest_url = url_for(
+        "notes.formsemestre_status",
+        scodoc_dept=g.scodoc_dept,
+        formsemestre_id=formsemestre.id,
+    )
+    if not formsemestre.est_chef_or_diretud():
+        raise ScoPermissionDenied("opération non autorisée", dest_url=dest_url)
     if not dialog_confirmed:
-        sem = sco_formsemestre.get_formsemestre(formsemestre_id)
-        etat = not sem["etat"]
-        if etat:
-            msg = "déverrouillage"
-        else:
-            msg = "verrouillage"
+        msg = "verrouillage" if formsemestre.etat else "déverrouillage"
         return scu.confirm_dialog(
             f"<h2>Confirmer le {msg} du semestre ?</h2>",
             helpmsg="""Les notes d'un semestre verrouillé ne peuvent plus être modifiées.
@@ -843,23 +845,14 @@ def formsemestre_change_lock(formsemestre_id, dialog_confirmed=False):
             Le programme d'une formation qui a un semestre verrouillé ne peut plus être modifié.
             """,
             dest_url="",
-            cancel_url=url_for(
-                "notes.formsemestre_status",
-                scodoc_dept=g.scodoc_dept,
-                formsemestre_id=formsemestre_id,
-            ),
+            cancel_url=dest_url,
             parameters={"formsemestre_id": formsemestre_id},
         )
 
-    sco_formsemestre_edit.formsemestre_change_lock(formsemestre_id)
+    formsemestre.flip_lock()
+    db.session.commit()
 
-    return flask.redirect(
-        url_for(
-            "notes.formsemestre_status",
-            scodoc_dept=g.scodoc_dept,
-            formsemestre_id=formsemestre_id,
-        )
-    )
+    return flask.redirect(dest_url)
 
 
 sco_publish(
diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py
index a0291f069..dc5a532f1 100644
--- a/tests/unit/__init__.py
+++ b/tests/unit/__init__.py
@@ -1 +1,17 @@
-# Unit tests
+# ScoDoc Unit Tests
+
+"""Set of unit tests for ScoDoc
+"""
+
+
+def call_view(view_function, *args, **kwargs):
+    """Undecorate a view and call it directly."""
+    # On a 3 décorateurs: @scodoc, @permission_required, @scodoc7func
+    func = (
+        view_function.__closure__[0]
+        .cell_contents.__closure__[0]
+        .cell_contents.__closure__[0]
+        .cell_contents
+    )
+    assert func
+    return func(*args, **kwargs)
diff --git a/tests/unit/test_but_jury.py b/tests/unit/test_but_jury.py
index e4ea40e0e..14d034345 100644
--- a/tests/unit/test_but_jury.py
+++ b/tests/unit/test_but_jury.py
@@ -42,7 +42,7 @@ def test_but_jury_GB(test_client):
     app.set_sco_dept(DEPT)
     # Construit la base de test GB une seule fois
     # puis lance les tests de jury
-    doc = yaml_setup.setup_from_yaml("tests/unit/cursus_but_gb.yaml")
+    doc = yaml_setup.setup_from_yaml("tests/ressources/yaml/cursus_but_gb.yaml")
 
     # Vérifie les deca de tous les semestres:
     for formsemestre in FormSemestre.query:
@@ -69,7 +69,7 @@ def test_but_jury_GMP_lm(test_client):
     app.set_sco_dept(DEPT)
     # Construit la base de test GB une seule fois
     # puis lance les tests de jury
-    doc = yaml_setup.setup_from_yaml("tests/unit/cursus_but_gmp_iutlm.yaml")
+    doc = yaml_setup.setup_from_yaml("tests/ressources/yaml/cursus_but_gmp_iutlm.yaml")
 
     formsemestres = FormSemestre.query.order_by(
         FormSemestre.date_debut, FormSemestre.semestre_id
@@ -95,7 +95,7 @@ def test_but_jury_GEII_lyon(test_client):
     app.set_sco_dept(DEPT)
     # Construit la base de test GB une seule fois
     # puis lance les tests de jury
-    doc = yaml_setup.setup_from_yaml("tests/unit/cursus_but_geii_lyon.yaml")
+    doc = yaml_setup.setup_from_yaml("tests/ressources/yaml/cursus_but_geii_lyon.yaml")
     formsemestres = FormSemestre.query.order_by(
         FormSemestre.date_debut, FormSemestre.semestre_id
     ).all()
diff --git a/tests/unit/test_formsemestre.py b/tests/unit/test_formsemestre.py
index 3cb516afe..ee2566953 100644
--- a/tests/unit/test_formsemestre.py
+++ b/tests/unit/test_formsemestre.py
@@ -3,12 +3,37 @@
 
 """ Test création/accès/clonage formsemestre
 """
+from flask import Response
 import pytest
-from tests.unit import yaml_setup
+from tests.unit import yaml_setup, call_view
 
 import app
-from app.models import Formation
-from app.scodoc import sco_formsemestre_edit
+from app import db
+from app.models import Formation, FormSemestre
+from app.scodoc import (
+    sco_archives,
+    sco_cost_formation,
+    sco_debouche,
+    sco_edit_ue,
+    sco_evaluations,
+    sco_evaluation_check_abs,
+    sco_evaluation_recap,
+    sco_formsemestre_edit,
+    sco_formsemestre_inscriptions,
+    sco_formsemestre_status,
+    sco_groups,
+    sco_inscr_passage,
+    sco_lycee,
+    sco_moduleimpl_inscriptions,
+    sco_poursuite_dut,
+    sco_preferences,
+    sco_recapcomplet,
+    sco_report,
+    sco_undo_notes,
+)
+
+from app.scodoc import sco_utils as scu
+from app.views import notes, scolar
 from config import TestConfig
 
 DEPT = TestConfig.DEPT_TEST
@@ -34,3 +59,128 @@ def test_formsemestres_associate_new_version(test_client):
     assert new_formation
     assert formsemestres[0].formation_id == new_formation.id
     assert formsemestres[1].formation_id == new_formation.id
+
+
+def test_formsemestre_misc_views(test_client):
+    """Test de nombreuses vues: test juste que la page ne plante,
+    pas de vérif du contenu ni de soumission si c'est un formulaire.
+
+    Note: les anciennes vues renvoient souvent des str au lieu de Response.
+    """
+    app.set_sco_dept(DEPT)
+    yaml_setup.setup_from_yaml("tests/ressources/yaml/simple_formsemestres.yaml")
+    formsemestre: FormSemestre = FormSemestre.query.first()
+
+    # ----- MENU SEMESTRE
+    _ = sco_formsemestre_status.formsemestre_status(formsemestre_id=formsemestre.id)
+    _ = sco_edit_ue.ue_table(formsemestre.formation_id)
+    _ = sco_formsemestre_edit.formsemestre_editwithmodules(formsemestre.id)
+    _ = sco_preferences.SemPreferences(formsemestre_id=formsemestre.id).edit()
+    _ = sco_formsemestre_edit.formsemestre_edit_options(formsemestre.id)
+    assert formsemestre.etat
+    formsemestre.flip_lock()
+    assert not formsemestre.etat
+    formsemestre.flip_lock()
+    assert formsemestre.etat
+    ans = sco_formsemestre_status.formsemestre_description(
+        formsemestre.id, with_evals=True
+    )
+    assert isinstance(ans, (str, Response))  # ici c'est une str
+    ans = sco_formsemestre_status.formsemestre_description(
+        formsemestre.id, with_evals=True, format="xls"
+    )
+    assert isinstance(ans, Response)
+    assert ans.status == "200 OK"
+    assert ans.mimetype == scu.XLSX_MIMETYPE
+    ans = sco_formsemestre_status.formsemestre_description(
+        formsemestre.id, with_evals=True, format="pdf"
+    )
+    assert isinstance(ans, Response)
+    assert ans.status == "200 OK"
+    assert ans.mimetype == scu.PDF_MIMETYPE
+    ans = sco_evaluation_check_abs.formsemestre_check_absences_html(formsemestre.id)
+    assert isinstance(ans, (str, Response))
+    # Appel direct de la vue: enlève les décorateurs:
+    ans = call_view(notes.formsemestre_enseignants_list, formsemestre.id)
+    assert isinstance(ans, (str, Response))  # ici str
+    # Juste la page dialogue avant opération::
+    ans = sco_formsemestre_edit.formsemestre_clone(formsemestre.id)
+    ans = sco_formsemestre_edit.formsemestre_associate_new_version(formsemestre.id)
+    ans = sco_formsemestre_edit.formsemestre_delete(formsemestre.id)
+
+    # ----- MENU INSCRIPTIONS
+    ans = sco_moduleimpl_inscriptions.moduleimpl_inscriptions_stats(formsemestre.id)
+    ans = sco_inscr_passage.formsemestre_inscr_passage(formsemestre.id)
+    ans = call_view(scolar.form_students_import_excel, formsemestre.id)
+    assert isinstance(ans, str)
+    ans = call_view(scolar.form_students_import_infos_admissions, formsemestre.id)
+    ans = sco_formsemestre_inscriptions.formsemestre_inscrits_ailleurs(formsemestre.id)
+
+    # ----- MENU GROUPES
+    ans = call_view(scolar.groups_view, formsemestre.id)
+    ans = call_view(scolar.partition_editor, formsemestre.id)
+    ans = sco_groups.edit_partition_form(formsemestre.id)
+
+    # ----- MENU NOTES
+    ans = sco_recapcomplet.formsemestre_recapcomplet(formsemestre.id)
+    ans = sco_recapcomplet.formsemestre_recapcomplet(formsemestre.id, tabformat="evals")
+    ans = sco_recapcomplet.formsemestre_recapcomplet(formsemestre.id, tabformat="xlsx")
+    assert isinstance(ans, Response)
+    assert ans.status == "200 OK"
+    assert ans.mimetype == scu.XLSX_MIMETYPE
+    ans = sco_recapcomplet.formsemestre_recapcomplet(formsemestre.id, tabformat="json")
+    assert isinstance(ans, Response)
+    assert ans.status == "200 OK"
+    assert ans.mimetype == scu.JSON_MIMETYPE
+    ans = sco_evaluation_recap.evaluations_recap(formsemestre.id)
+
+    # Bulletins pdf:
+    ans = call_view(notes.formsemestre_bulletins_pdf_choice, formsemestre.id)
+    assert isinstance(ans, str)  # sans version, on a le formulaire de choix, une str
+    ans = call_view(
+        notes.formsemestre_bulletins_pdf_choice, formsemestre.id, version="long"
+    )
+    assert isinstance(ans, Response)
+    assert ans.status == "200 OK"
+    assert ans.mimetype == scu.PDF_MIMETYPE
+    # on ne teste pas les mails :)
+    ans = sco_evaluations.formsemestre_evaluations_cal(formsemestre.id)
+    assert isinstance(ans, str)
+    sco_undo_notes.formsemestre_list_saisies_notes(formsemestre.id)
+
+    # ----- MENU JURY
+    ans = sco_recapcomplet.formsemestre_recapcomplet(formsemestre.id, mode_jury=True)
+    ans = sco_recapcomplet.formsemestre_recapcomplet(
+        formsemestre.id, mode_jury=True, tabformat="xlsx"
+    )
+    assert isinstance(ans, Response)
+    assert ans.status == "200 OK"
+    assert ans.mimetype == scu.XLSX_MIMETYPE
+    ans = sco_recapcomplet.formsemestre_recapcomplet(
+        formsemestre.id, mode_jury=True, tabformat="json"
+    )
+    assert isinstance(ans, Response)
+    assert ans.status == "200 OK"
+    assert ans.mimetype == scu.JSON_MIMETYPE
+    ans = sco_archives.formsemestre_archive(formsemestre.id)
+    ans = sco_archives.formsemestre_list_archives(formsemestre.id)
+
+    # ----- MENU STATISTIQUES
+    ans = sco_report.formsemestre_report_counts(formsemestre.id)
+    ans = sco_report.formsemestre_report_counts(formsemestre.id, format="xls")
+    assert isinstance(ans, Response)
+    assert ans.status == "200 OK"
+    assert ans.mimetype == scu.XLSX_MIMETYPE
+    ans = sco_report.formsemestre_suivi_cohorte(formsemestre.id)
+    ans = sco_report.formsemestre_suivi_cohorte(formsemestre.id, format="pdf")
+    assert isinstance(ans, Response)
+    assert ans.status == "200 OK"
+    assert ans.mimetype == scu.PDF_MIMETYPE
+    ans = sco_report.formsemestre_graph_cursus(formsemestre.id)
+    ans = sco_report.formsemestre_suivi_cursus(formsemestre.id)
+    ans = sco_lycee.formsemestre_etuds_lycees(formsemestre.id)
+    ans = sco_poursuite_dut.formsemestre_poursuite_report(formsemestre.id)
+    # pas de test des avis de poursuite
+    ans = sco_debouche.report_debouche_date(start_year=2000)
+    ans = sco_cost_formation.formsemestre_estim_cost(formsemestre.id)
+    # pas de test des indicateurs de suivi BUT
-- 
GitLab