From 8c3e7b4ff61e98ea89dc60349a8c9a02c9011e0b Mon Sep 17 00:00:00 2001
From: Emmanuel Viennet <emmanuel.viennet@gmail.com>
Date: Thu, 12 Sep 2024 14:34:34 +0200
Subject: [PATCH] =?UTF-8?q?Am=C3=A9lioration=20page=20bilan=20ECTS:=20plus?=
 =?UTF-8?q?=20de=20d=C3=A9tails,=20messages=20d'avertissement.=20Closes=20?=
 =?UTF-8?q?#992?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 app/api/jury.py                               |  6 ++
 app/but/jury_but.py                           |  2 +-
 app/models/formations.py                      |  2 +-
 app/models/validations.py                     |  9 +--
 app/scodoc/sco_edit_ue.py                     |  2 +-
 app/static/css/jury_delete_manual.css         | 10 ++-
 app/templates/but/parcour_formation.j2        |  4 +-
 app/templates/jury/etud_bilan_ects.j2         | 36 ++++++++++-
 .../jury/ue_list_etud_validations.j2          | 53 +++++++++++-----
 app/views/jury_validations.py                 | 61 +++++++++++++++++++
 10 files changed, 155 insertions(+), 30 deletions(-)

diff --git a/app/api/jury.py b/app/api/jury.py
index 66324ab3..4aa31ce0 100644
--- a/app/api/jury.py
+++ b/app/api/jury.py
@@ -86,6 +86,12 @@ def _news_delete_jury_etud(etud: Identite, detail: str = ""):
         text=f"""Suppression décision jury {detail} pour <a href="{url}">{etud.nomprenom}</a>""",
         url=url,
     )
+    Scolog.logdb(
+        "jury_delete_manual",
+        etudid=etud.id,
+        msg=f"Validation {detail} effacée",
+        commit=True,
+    )
 
 
 @bp.route(
diff --git a/app/but/jury_but.py b/app/but/jury_but.py
index a12877d9..a7a37ab6 100644
--- a/app/but/jury_but.py
+++ b/app/but/jury_but.py
@@ -501,7 +501,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
                         scodoc_dept=g.scodoc_dept,
                         semestre_idx=formsemestre.semestre_id,
                         formation_id=formsemestre.formation.id)}">
-                        {formsemestre.formation.html()} ({
+                        {formsemestre.formation.html()|safe} ({
                             formsemestre.formation.id})</a>
                     </li>
                     </ul>
diff --git a/app/models/formations.py b/app/models/formations.py
index 65538fda..1384873f 100644
--- a/app/models/formations.py
+++ b/app/models/formations.py
@@ -64,7 +64,7 @@ class Formation(ScoDocModel):
 
     def html(self) -> str:
         "titre complet pour affichage"
-        return f"""Formation {self.titre} ({self.acronyme}) [version {self.version}] code {self.formation_code}"""
+        return f"""Formation {self.titre} ({self.acronyme}) version {self.version} code <tt>{self.formation_code}</tt>"""
 
     @classmethod
     def get_formation(cls, formation_id: int | str, dept_id: int = None) -> "Formation":
diff --git a/app/models/validations.py b/app/models/validations.py
index a9a3ad2b..150bb94a 100644
--- a/app/models/validations.py
+++ b/app/models/validations.py
@@ -91,7 +91,7 @@ class ScolarFormSemestreValidation(db.Model):
         d.pop("_sa_instance_state", None)
         return d
 
-    def html(self, detail=False) -> str:
+    def html(self, detail=True) -> str:
         "Affichage html"
         if self.ue_id is not None:
             moyenne = (
@@ -114,11 +114,12 @@ class ScolarFormSemestreValidation(db.Model):
                   + ", ".join([p.code for p in self.ue.parcours]))
                   + "</span>"
                   if self.ue.parcours else ""}
-                {("émise par " + link)}
                 : <b>{self.code}</b>{moyenne}
                 <b>{(self.ue.ects or 0):g} ECTS</b>
-                le {self.event_date.strftime(scu.DATEATIME_FMT)}
-                """
+                {("émise par " + link)}
+                """ + (
+                f"le {self.event_date.strftime(scu.DATEATIME_FMT)}" if detail else ""
+            )
         else:
             return f"""Validation du semestre S{
                 self.formsemestre.semestre_id if self.formsemestre else "?"}
diff --git a/app/scodoc/sco_edit_ue.py b/app/scodoc/sco_edit_ue.py
index 40989539..89af78ec 100644
--- a/app/scodoc/sco_edit_ue.py
+++ b/app/scodoc/sco_edit_ue.py
@@ -765,7 +765,7 @@ def ue_table(formation_id=None, semestre_idx=1, msg=""):  # was ue_list
         "delete_small_dis_img", title="Suppression impossible (module utilisé)"
     )
     H = [
-        f"""<h2>{formation.html()} {lockicon}
+        f"""<h2>{formation.html()|safe} {lockicon}
         </h2>
         """,
     ]
diff --git a/app/static/css/jury_delete_manual.css b/app/static/css/jury_delete_manual.css
index 478167eb..59bf1291 100644
--- a/app/static/css/jury_delete_manual.css
+++ b/app/static/css/jury_delete_manual.css
@@ -8,7 +8,12 @@ span.parcours {
     color: blueviolet;
 }
 
-div.ue_list_etud_validations ul.liste_validations li {
+div.liste_validations {
+    margin-top: 16px;
+    margin-bottom: 16px;
+}
+
+div.ue_list_etud_validations div.liste_validations details {
     margin-bottom: 8px;
 }
 
@@ -28,6 +33,7 @@ details {
 }
 
 div.validation-details {
-    margin-left: 32px;
+    margin-top: 12px;
     margin-bottom: 16px;
+    margin-left: 32px;
 }
\ No newline at end of file
diff --git a/app/templates/but/parcour_formation.j2 b/app/templates/but/parcour_formation.j2
index ce8614e4..1b0f2396 100644
--- a/app/templates/but/parcour_formation.j2
+++ b/app/templates/but/parcour_formation.j2
@@ -44,7 +44,7 @@
 {%- endmacro %}
 
 {% block app_content %}
-<h2>{{formation.html()}}</h2>
+<h2>{{formation.html()|safe}}</h2>
 
 {# Liens vers les différents parcours #}
 <div class="les_parcours">
@@ -133,7 +133,7 @@ Choisissez un parcours...
     d'associer à chaque semestre d'un niveau de compétence une UE de la formation
     <a class="stdlink"
     href="{{url_for('notes.ue_table', scodoc_dept=g.scodoc_dept, formation_id=formation.id )
-    }}">{{formation.html()}}
+    }}">{{formation.html()|safe}}
     </a>.</p>
 
     <p>Le symbole <span class="parc">TC</span> désigne un niveau du tronc commun
diff --git a/app/templates/jury/etud_bilan_ects.j2 b/app/templates/jury/etud_bilan_ects.j2
index 0908dc7e..553864b6 100644
--- a/app/templates/jury/etud_bilan_ects.j2
+++ b/app/templates/jury/etud_bilan_ects.j2
@@ -1,4 +1,4 @@
-{% extends "sco_page.j2" %}
+{% extends "sco_page_dept.j2" %}
 
 {% block styles %}
     {{super()}}
@@ -10,8 +10,13 @@
     <h1>Bilan des ECTS de {{etud.html_link_fiche()|safe}}</h1>
 
     <div class="help">
-    Cette page donne toutes les UEs acquises par l'étudiant (codes <tt>ADM, ADJ, ADJR, ADSUP, CMP...</tt>)
-    dans chaque formation qu'il a suivi.
+    <p>
+    Cette page donne toutes les UEs acquises par l'étudiant (codes <tt>ADM, ADJ, ADJR, ADSUP, CMP...</tt>).
+    </p>
+    <p>
+    La somme des crédits ECTS en bas de page peut compter des UEs
+    suivies plusieurs fois (redoublements) n'a pas de signification pour l'octroit des diplômes.
+    </p>
     </div>
 
     {% for diplome in formsemestre_by_diplome %}
@@ -21,4 +26,29 @@
         {% include "jury/ue_list_etud_validations.j2" %}
     {% endfor %}
 
+    {% if ue_warnings %}
+    <div class="scobox">
+        <div class="scobox-title warning">Attention</div>
+        <ul>
+            {% for warning in ue_warnings %}
+                <li>{{warning|safe}}</li>
+            {% endfor %}
+        </ul>
+        <div class="help fontred">Ces problème peuvent dans certains cas affecter
+        le comptage des crédits ECTS et la délivrance des diplômes.</div>
+    </div>
+    {% endif %}
+
 {% endblock app_content %}
+
+{% block scripts %}
+{{super()}}
+<script>
+function open_all_details() {
+    var details = document.querySelectorAll('.liste_validations details');
+    details.forEach(function(detail) {
+        detail.open = true;
+    });
+}
+</script>
+{% endblock %}
\ No newline at end of file
diff --git a/app/templates/jury/ue_list_etud_validations.j2 b/app/templates/jury/ue_list_etud_validations.j2
index aed5b100..b8652a15 100644
--- a/app/templates/jury/ue_list_etud_validations.j2
+++ b/app/templates/jury/ue_list_etud_validations.j2
@@ -5,31 +5,52 @@
     <div class="help">Liste de toutes les UEs validées par {{etud.html_link_fiche()|safe}},
         sur des semestres ou déclarées comme "antérieures" (externes).
     </div>
-    <ul class="liste_validations">
+    <div class="liste_validations">
     {% for validation in validations %}
 
-        <li
+        <div
         {% if loop.index0 > 0 and validation.formsemestre and loop.previtem.formsemestre.semestre_id != validation.formsemestre.semestre_id %}
         class="new_semestre"
+        data-ue_id="{{validation.ue.id}}"
         {% endif %}
-        >{{ validation.html() | safe }}
-        {% if edit_mode %}
-            {% if validation.formsemestre and validation.formsemestre.can_edit_jury() %}
-                <form class="inline-form">
-                    <button data-v_id="{{validation.id}}" data-type="validation_ue" data-etudid="{{etud.id}}">
-                        effacer
-                    </button>
-                </form>
-            {% else %}
-                {{ scu.icontag("lock_img", border="0", title="Semestre verrouillé")|safe }}
+        >
+        <details>
+        <summary>
+            {{ validation.html(detail=False) | safe }}
+            {% if edit_mode %}
+                {% if validation.formsemestre and validation.formsemestre.can_edit_jury() %}
+                    <form class="inline-form">
+                        <button data-v_id="{{validation.id}}" data-type="validation_ue" data-etudid="{{etud.id}}">
+                            effacer
+                        </button>
+                    </form>
+                {% else %}
+                    {{ scu.icontag("lock_img", border="0", title="Semestre verrouillé")|safe }}
+                {% endif %}
             {% endif %}
-        {% endif %}
-        </li>
+            </summary>
+            <div class="validation-details">
+                <b>UE {{validation.ue.acronyme}}</b> <tt>[{{validation.ue.ue_code}}]</tt> en
+                    <a class="discretelink" href="{{
+                    url_for('notes.ue_table', scodoc_dept=g.scodoc_dept,
+                    formation_id=validation.ue.formation.id, semestre_idx=validation.ue.semestre_idx)
+                    }}">{{validation.ue.formation.html()|safe}}</a>
+                {% if validation.ue.formation.is_apc() %}
+                    <div>Compétence: {{validation.ue.niveau_competence}}</div>
+                    <div>Référentiel :
+                    {{ validation.ue.formation.referentiel_competence.get_title()
+                        if validation.ue.formation.referentiel_competence else '<em>pas de référentiel</em>' }}
+                    </div>
+                {% endif %}
+            </div>
+        </details>
+        </div>
     {% endfor %}
-    </ul>
+    </div>
+    <div><a class="stdlink" href="#" onclick="open_all_details()">ouvrir tous les détails</a></div>
     {% if total_ects %}
     <div class="total_ects">
-        Total ECTS: {{ "%g" % total_ects }}
+        Total ECTS (toutes UEs, y compris redoublées): {{ "%g" % total_ects }}
     </div>
     {% endif %}
 </div>
\ No newline at end of file
diff --git a/app/views/jury_validations.py b/app/views/jury_validations.py
index 4ecdb443..b7527e78 100644
--- a/app/views/jury_validations.py
+++ b/app/views/jury_validations.py
@@ -944,6 +944,8 @@ def etud_bilan_ects(etudid: int):
     ects_by_diplome = {}
     titre_by_diplome = {}  # { diplome : titre }
     validations_by_diplome = {}  # { diplome : query validations UEs }
+    validations_by_ue_code = defaultdict(list)  # { ue_code : [validation] }
+    validations_by_niveau_sem = defaultdict(list)  # { niveau_sem : [validation] }
     for diplome, formsemestres in formsemestre_by_diplome.items():
         formsemestre = formsemestres[0]
         titre_by_diplome[diplome] = formsemestre.formation.get_titre_version()
@@ -962,6 +964,62 @@ def etud_bilan_ects(etudid: int):
             (validation.ue.ects or 0.0)
             for validation in validations_by_diplome[diplome]
         )
+        for validation in validations:
+            validations_by_ue_code[validation.ue.ue_code].append(validation)
+            validations_by_niveau_sem[
+                (
+                    (
+                        validation.ue.niveau_competence.id
+                        if validation.ue.niveau_competence
+                        else None
+                    ),
+                    validation.ue.semestre_idx,
+                )
+            ].append(validation)
+
+    ref_comp_ids = {
+        v.ue.formation.referentiel_competence_id
+        for validations in validations_by_ue_code.values()
+        for v in validations
+        if v.ue.formation.referentiel_competence_id is not None
+    }
+
+    ue_warnings = []
+    if len(ref_comp_ids) > 1:
+        ue_warnings.append(
+            """plusieurs référentiels de compétences utilisés&nbsp;!
+            (ok si plusieurs diplôme différents suivis)"""
+        )
+    for ue_code, validations in validations_by_ue_code.items():
+        ectss = {v.ue.ects for v in validations}
+        if len(ectss) > 1:
+            ects_str = ", ".join(
+                f"{v.ue.acronyme}: {v.ue.ects} ects" for v in validations
+            )
+            ue_acros = ", ".join({v.ue.acronyme for v in validations})
+            ue_warnings.append(
+                f"""Les UEs {ue_acros} ont le même code ({ue_code
+                }) mais des ECTS différents: {ects_str}"""
+            )
+    for (niveau_id, semestre_idx), validations in validations_by_niveau_sem.items():
+        if not validations:
+            continue  # safeguard
+        formation = validations[0].ue.formation
+        ue_acros = ", ".join({v.ue.acronyme for v in validations})
+        if niveau_id is None and formation.is_apc():
+            ue_warnings.append(
+                f"""Les UEs {ue_acros} du S{semestre_idx
+                               } n'ont pas de niveau de compétence associé !"""
+            )
+        ectss = {v.ue.ects for v in validations}
+        if len(ectss) > 1:
+            ects_str = ", ".join(
+                f"{v.ue.acronyme}: {v.ue.ects} ects" for v in validations
+            )
+            ue_warnings.append(
+                f"""Les UEs {ue_acros} du même code niveau de compétence
+                ({validations[0].ue.niveau_competence}) ont des ECTS différents: {ects_str}"""
+            )
 
     return render_template(
         "jury/etud_bilan_ects.j2",
@@ -969,5 +1027,8 @@ def etud_bilan_ects(etudid: int):
         ects_by_diplome=ects_by_diplome,
         formsemestre_by_diplome=formsemestre_by_diplome,
         titre_by_diplome=titre_by_diplome,
+        title=f"Bilan ECTS {etud.nomprenom}",
+        ue_warnings=ue_warnings,
         validations_by_diplome=validations_by_diplome,
+        sco=ScoData(etud=etud),
     )
-- 
GitLab