From 73c263b89519960af15935b528ebc44db377626c Mon Sep 17 00:00:00 2001
From: Emmanuel Viennet <emmanuel.viennet@gmail.com>
Date: Sun, 25 Aug 2024 22:00:29 +0200
Subject: [PATCH] =?UTF-8?q?D=C3=A9livrance=20dipl=C3=B4me=20BUT:=20v=C3=A9?=
 =?UTF-8?q?rifie=20ECTS=20du=20parcours?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 app/but/cursus_but.py                | 89 ++++++++++++++++++++++++----
 app/tables/jury_recap.py             |  2 +-
 app/templates/but/validate_dut120.j2 |  2 +-
 sco_version.py                       |  2 +-
 4 files changed, 79 insertions(+), 16 deletions(-)

diff --git a/app/but/cursus_but.py b/app/but/cursus_but.py
index fc1ee3cf..0abaf31a 100644
--- a/app/but/cursus_but.py
+++ b/app/but/cursus_but.py
@@ -18,7 +18,6 @@ from collections.abc import Iterable
 from operator import attrgetter
 
 from flask import g, url_for
-from flask_sqlalchemy.query import Query
 
 from app import db, log
 from app.comp.res_but import ResultatsSemestreBUT
@@ -38,7 +37,12 @@ from app.models.formsemestre import FormSemestre
 from app.models.ues import UniteEns
 from app.models.validations import ScolarFormSemestreValidation
 from app.scodoc import codes_cursus as sco_codes
-from app.scodoc.codes_cursus import code_ue_validant, CODES_UE_VALIDES, UE_STANDARD
+from app.scodoc.codes_cursus import (
+    code_ue_validant,
+    CODES_UE_VALIDES,
+    CursusBUT,
+    UE_STANDARD,
+)
 from app.scodoc.sco_exceptions import ScoNoReferentielCompetences, ScoValueError
 from app.scodoc import sco_cursus_dut
 
@@ -56,24 +60,39 @@ class SituationEtudCursusBUT(sco_cursus_dut.SituationEtudCursusClassic):
         return False
 
     def parcours_validated(self):
-        "True si le parcours (ici diplôme BUT) est validé"
-        return but_parcours_validated(
-            self.etud.id, self.cur_sem.formation.referentiel_competence_id
-        )
+        """True si le parcours (ici diplôme BUT) est validé.
+        Considère le parcours du semestre en cours (res).
+        """
+        parcour_id = self.nt.etuds_parcour_id.get(self.etud.id)
+        return but_parcours_validated(self.etud.id, parcour_id)
 
 
-def but_parcours_validated(etudid: int, referentiel_competence_id: int) -> bool:
-    """Détermine si le parcours BUT est validé:
-    ne regarde que si une validation BUT3 est enregistrée
-    """
+def but_annee_validated(
+    etudid: int, referentiel_competence_id: int, annee: int = 3
+) -> bool:
+    """Vrai si une validation de l'année BUT est enregistrée"""
     return any(
         sco_codes.code_annee_validant(v.code)
         for v in ApcValidationAnnee.query.filter_by(
-            etudid=etudid, ordre=3, referentiel_competence_id=referentiel_competence_id
+            etudid=etudid,
+            ordre=annee,
+            referentiel_competence_id=referentiel_competence_id,
         )
     )
 
 
+def but_parcours_validated(etudid: int, parcour_id: int | None) -> bool:
+    """Détermine si le parcours BUT est validé.
+    = 180 ECTS acquis dans les UEs du parcours.
+    """
+    if parcour_id is None:
+        return False  # étudiant non inscrit à un parcours
+    # Les ECTS
+    validations = but_validations_ues_parcours(etudid, parcour_id)
+    ects_acquis = validations_count_ects(validations)
+    return ects_acquis >= CursusBUT.ECTS_DIPLOME
+
+
 class EtudCursusBUT:
     """L'état de l'étudiant dans son cursus BUT
     Liste des niveaux validés/à valider
@@ -395,9 +414,18 @@ def but_ects_valides(
     Si annees_but est spécifié, un iterable "BUT1, "BUT2" par exemple, ne prend que ces années.
     """
     validations = but_validations_ues(etud, referentiel_competence_id, annees_but)
+    return validations_count_ects(validations)
+
+
+def validations_count_ects(validations: list[ScolarFormSemestreValidation]) -> int:
+    """Somme les ECTS validés par ces UEs, en éliminant les éventuels
+    doublons (niveaux de compétences validés plusieurs fois)"""
     ects_dict = {}
     for v in validations:
-        key = (v.ue.semestre_idx, v.ue.niveau_competence.id)
+        key = (
+            v.ue.semestre_idx,
+            v.ue.niveau_competence.id if v.ue.niveau_competence else None,
+        )
         if v.code in CODES_UE_VALIDES:
             ects_dict[key] = v.ue.ects or 0.0
 
@@ -427,8 +455,12 @@ def but_validations_ues(
     validations = validations.join(ApcCompetence).filter_by(
         referentiel_id=referentiel_competence_id
     )
+    return sorted_validations(validations)
+
 
-    # Tri (nb: fait en python pour gérer les validations externes qui n'ont pas de formsemestre)
+def sorted_validations(validations) -> list[ScolarFormSemestreValidation]:
+    """Tri (nb: fait en python pour gérer les validations externes qui
+    n'ont pas de formsemestre)"""
     return sorted(
         validations,
         key=lambda v: (
@@ -439,6 +471,37 @@ def but_validations_ues(
     )
 
 
+def but_validations_ues_parcours(
+    etudid: int, parcour_id: int
+) -> list[ScolarFormSemestreValidation]:
+    """Query les validations d'UEs pour cet étudiant
+    dans des UEs appartenant à ce parcours ou à son tronc commun.
+    """
+    # Rappel:
+    # Les UEs associées à un parcours:
+    # UniteEns.query.join(UEParcours).filter(UEParcours.parcours_id == parcour.id) )
+    # Les UEs associées au tronc commun (à aucun parcours)
+    # UniteEns.query.filter(~UniteEns.id.in_(UEParcours.query.with_entities(UEParcours.ue_id)))
+
+    # Les validations d'UE de ce parcours ou du tronc commun pour cet étudiant:
+    validations = (
+        ScolarFormSemestreValidation.query.filter_by(etudid=etudid)
+        .filter(ScolarFormSemestreValidation.ue_id != None)
+        .join(UniteEns)
+        .filter(
+            db.or_(
+                UniteEns.id.in_(
+                    UEParcours.query.with_entities(UEParcours.ue_id).filter(
+                        UEParcours.parcours_id == parcour_id
+                    )
+                ),
+                ~UniteEns.id.in_(UEParcours.query.with_entities(UEParcours.ue_id)),
+            )
+        )
+    )
+    return sorted_validations(validations)
+
+
 def etud_ues_de_but1_non_validees(
     etud: Identite, formation: Formation, parcour: ApcParcours
 ) -> list[UniteEns]:
diff --git a/app/tables/jury_recap.py b/app/tables/jury_recap.py
index 8c538ad6..56d27e4e 100644
--- a/app/tables/jury_recap.py
+++ b/app/tables/jury_recap.py
@@ -113,7 +113,7 @@ class TableJury(TableRecap):
             if res.is_apc and res.formsemestre.semestre_id == 6:
                 # on ne vérifie le diplôme que dans ce cas pour ne pas ralentir
                 if cursus_but.but_parcours_validated(
-                    etud.id, res.formsemestre.formation.referentiel_competence_id
+                    etud.id, res.etuds_parcour_id.get(etud.id)
                 ):
                     row.add_cell(
                         "autorisations_inscription",
diff --git a/app/templates/but/validate_dut120.j2 b/app/templates/but/validate_dut120.j2
index fd13de8a..e06f04fa 100644
--- a/app/templates/but/validate_dut120.j2
+++ b/app/templates/but/validate_dut120.j2
@@ -44,7 +44,7 @@ une formation utilisant une autre version de référentiel, pensez à revalider
         </div>
     </form>
 {% else %}
-    <div class="warning">
+    <div class="warning space-before-24">
     {% if validation %}
         DUT déjà validé dans cette spécialité
         <b>{{formsemestre.formation.referentiel_competence.get_title()}}</b>
diff --git a/sco_version.py b/sco_version.py
index e0aa0a8f..1671505e 100644
--- a/sco_version.py
+++ b/sco_version.py
@@ -1,7 +1,7 @@
 # -*- mode: python -*-
 # -*- coding: utf-8 -*-
 
-SCOVERSION = "9.7.11"
+SCOVERSION = "9.7.12"
 
 SCONAME = "ScoDoc"
 
-- 
GitLab