From 937a96d0869a68b08a45b4d1d3749e5c1cd7fc14 Mon Sep 17 00:00:00 2001
From: Emmanuel Viennet
Date: Sun, 2 Jul 2023 16:02:09 +0200
Subject: [PATCH] =?UTF-8?q?Jury=20BUT:=20cas=20du=20passage=20en=20BUT3=20?=
=?UTF-8?q?avec=20BUT1=20non=20valid=C3=A9.=20Corrige=20validation=20ADSUP?=
=?UTF-8?q?=20UEs.=20Test=20unitaire:=20geii90.?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
app/but/cursus_but.py | 2 +-
app/but/jury_but.py | 81 ++++++++++++++-----
app/models/but_refcomp.py | 3 +
.../jury/erase_decisions_annee_formation.j2 | 4 +-
.../ressources/yaml/cursus_but_geii_lyon.yaml | 81 +++++++++++++++++++
5 files changed, 148 insertions(+), 23 deletions(-)
diff --git a/app/but/cursus_but.py b/app/but/cursus_but.py
index 622cdbb00..97a555ca6 100644
--- a/app/but/cursus_but.py
+++ b/app/but/cursus_but.py
@@ -102,7 +102,7 @@ class EtudCursusBUT:
"Liste des inscriptions aux sem. de la formation, triées par indice et chronologie"
self.parcour: ApcParcours = self.inscriptions[-1].parcour
"Le parcours à valider: celui du DERNIER semestre suivi (peut être None)"
- self.niveaux_by_annee = {}
+ self.niveaux_by_annee: dict[int, list[ApcNiveau]] = {}
"{ annee:int : liste des niveaux à valider }"
self.niveaux: dict[int, ApcNiveau] = {}
"cache les niveaux"
diff --git a/app/but/jury_but.py b/app/but/jury_but.py
index 05978de2d..954481ad0 100644
--- a/app/but/jury_but.py
+++ b/app/but/jury_but.py
@@ -342,21 +342,11 @@ class DecisionsProposeesAnnee(DecisionsProposees):
# Cas particulier du passage en BUT 3: nécessité d'avoir validé toutes les UEs du BUT 1.
if self.passage_de_droit and self.annee_but == 2:
inscription = formsemestre.etuds_inscriptions.get(etud.id)
- if inscription:
- ues_but1_non_validees = cursus_but.etud_ues_de_but1_non_validees(
- etud, self.formsemestre.formation, self.parcour
- )
- self.passage_de_droit = not ues_but1_non_validees
- explanation += (
- f"""UEs de BUT1 non validées: {
- ', '.join(ue.acronyme for ue in ues_but1_non_validees)
- }. """
- if ues_but1_non_validees
- else ""
- )
- else:
+ if not inscription or inscription.etat != scu.INSCRIT:
# pas inscrit dans le semestre courant ???
self.passage_de_droit = False
+ else:
+ self.passage_de_droit, explanation = self.passage_de_droit_en_but3()
# Enfin calcule les codes des UEs:
for dec_ue in self.decisions_ues.values():
@@ -423,6 +413,53 @@ class DecisionsProposeesAnnee(DecisionsProposees):
)
self.codes = [self.codes[0]] + sorted(self.codes[1:])
+ def passage_de_droit_en_but3(self) -> tuple[bool, str]:
+ """Vérifie si les conditions supplémentaires de passage BUT2 vers BUT3 sont satisfaites"""
+ cursus: EtudCursusBUT = EtudCursusBUT(self.etud, self.formsemestre.formation)
+ niveaux_but1 = cursus.niveaux_by_annee[1]
+
+ niveaux_but1_non_valides = []
+ for niveau in niveaux_but1:
+ ok = False
+ validation_par_annee = cursus.validation_par_competence_et_annee[
+ niveau.competence_id
+ ]
+ if validation_par_annee:
+ validation_niveau = validation_par_annee.get("BUT1")
+ if validation_niveau and validation_niveau.code in CODES_RCUE_VALIDES:
+ ok = True
+ if not ok:
+ niveaux_but1_non_valides.append(niveau)
+
+ # Les niveaux de BUT1 manquants passent-ils en ADSUP ?
+ # en vertu de l'article 4.3,
+ # "La validation des deux UE du niveau d’une compétence emporte la validation de
+ # l’ensemble des UE du niveau inférieur de cette même compétence."
+ explanation = ""
+ ok = True
+ for niveau_but1 in niveaux_but1_non_valides:
+ niveau_but2 = niveau_but1.competence.niveaux.filter_by(annee="BUT2").first()
+ if niveau_but2:
+ rcue = self.rcue_by_niveau.get(niveau_but2.id)
+ if (rcue is None) or (
+ not rcue.est_validable() and not rcue.code_valide()
+ ):
+ # le RCUE de BUT2 n'est ni validable (avec les notes en cours) ni déjà validé
+ ok = False
+ explanation += (
+ f"Compétence {niveau_but1} de BUT 1 non validée.
"
+ )
+ else:
+ explanation += (
+ f"Compétence {niveau_but1} de BUT 1 validée par ce BUT2.
"
+ )
+ else:
+ ok = False
+ explanation += f"""Compétence {
+ niveau_but1} de BUT 1 non validée et non existante en BUT2.
"""
+
+ return ok, explanation
+
# WIP TODO XXX def get_moyenne_annuelle(self)
def infos(self) -> str:
@@ -1235,13 +1272,16 @@ class DecisionsProposeesRCUE(DecisionsProposees):
self, semestre_id: int, ordre_inferieur: int, competence: ApcCompetence
):
"""Au besoin, enregistre une validation d'UE ADSUP pour le niveau de compétence
- semestre_id : l'indice du semestre concerné (le pair ou l'impair)
+ semestre_id : l'indice du semestre concerné (le pair ou l'impair du niveau courant)
"""
- # Les validations d'UE impaires existantes pour ce niveau inférieur ?
+ semestre_id_inferieur = semestre_id - 2
+ if semestre_id_inferieur < 1:
+ return
+ # Les validations d'UE existantes pour ce niveau inférieur ?
validations_ues: list[ScolarFormSemestreValidation] = (
ScolarFormSemestreValidation.query.filter_by(etudid=self.etud.id)
.join(UniteEns)
- .filter_by(semestre_idx=semestre_id)
+ .filter_by(semestre_idx=semestre_id_inferieur)
.join(ApcNiveau)
.filter_by(ordre=ordre_inferieur)
.join(ApcCompetence)
@@ -1256,13 +1296,14 @@ class DecisionsProposeesRCUE(DecisionsProposees):
# Il faut créer une validation d'UE
# cherche l'UE de notre formation associée à ce niveau
# et warning si il n'y en a pas
- ue = self._get_ue_inferieure(semestre_id, ordre_inferieur, competence)
+ ue = self._get_ue_inferieure(
+ semestre_id_inferieur, ordre_inferieur, competence
+ )
if not ue:
# programme incomplet ou mal paramétré
flash(
- f"""Impossible de valider l'UE inférieure du niveau {
- ordre_inferieur
- } de la compétence {competence.titre}
+ f"""Impossible de valider l'UE inférieure de la compétence {
+ competence.titre} (niveau {ordre_inferieur})
car elle n'existe pas dans la formation
""",
"warning",
diff --git a/app/models/but_refcomp.py b/app/models/but_refcomp.py
index c4983bdd2..b33e2f2bb 100644
--- a/app/models/but_refcomp.py
+++ b/app/models/but_refcomp.py
@@ -364,6 +364,9 @@ class ApcNiveau(db.Model, XMLModel):
return f"""<{self.__class__.__name__} {self.id} ordre={self.ordre!r} annee={
self.annee!r} {self.competence!r}>"""
+ def __str__(self):
+ return f"""{self.competence.titre} niveau {self.ordre}"""
+
def to_dict(self, with_app_critiques=True):
"as a dict, recursif (ou non) sur les AC"
return {
diff --git a/app/templates/jury/erase_decisions_annee_formation.j2 b/app/templates/jury/erase_decisions_annee_formation.j2
index fc8fd0ce0..5f81e69ce 100644
--- a/app/templates/jury/erase_decisions_annee_formation.j2
+++ b/app/templates/jury/erase_decisions_annee_formation.j2
@@ -35,6 +35,8 @@ quelle que soit leur origine.
+{% endif %}
+
Autres actions:
@@ -60,8 +62,6 @@ quelle que soit leur origine.
-{% endif %}
-
{% endblock %}
\ No newline at end of file
diff --git a/tests/ressources/yaml/cursus_but_geii_lyon.yaml b/tests/ressources/yaml/cursus_but_geii_lyon.yaml
index 701585d61..fefdc9b6b 100644
--- a/tests/ressources/yaml/cursus_but_geii_lyon.yaml
+++ b/tests/ressources/yaml/cursus_but_geii_lyon.yaml
@@ -109,6 +109,16 @@ FormSemestres:
idx: 1
date_debut: 2022-09-02
date_fin: 2023-01-12
+ S3:
+ idx: 3
+ codes_parcours: ['AII']
+ date_debut: 2022-09-01
+ date_fin: 2023-01-15
+ S4:
+ idx: 4
+ codes_parcours: ['AII']
+ date_debut: 2023-01-16
+ date_fin: 2023-07-10
Etudiants:
geii8:
@@ -1326,3 +1336,74 @@ Etudiants:
# moy_rcue: 13.5000 # Pas de moyenne calculée
est_compensable: False
decision_annee: ATJ # Passage tout de même en S3
+ #
+ # ----------------------- geii90 : ADSUP envoyés par BUT2 vers BUT1
+ #
+ geii90:
+ prenom: etugeii90
+ civilite: M
+ code_nip: geii90
+ formsemestres:
+ S1: # 2 UEs, les deux en AJ
+ notes_modules: # on joue avec les SAE seulement car elles sont "diagonales"
+ "S1.1": 9.5000
+ "S1.2": 8.5000
+ attendu: # les codes jury que l'on doit vérifier
+ deca:
+ passage_de_droit: False
+ nb_competences: 2
+ nb_rcue_annee: 0
+ decisions_ues:
+ "UE11":
+ codes: [ "AJ", "..." ]
+ "UE12":
+ codes: [ "AJ", "..." ]
+ S2: # pareil, mais le jury le fait passer en S3
+ notes_modules: # on joue avec les SAE seulement car elles sont "diagonales"
+ "S2.1": 9.8000
+ "S2.2": 9.9000
+ attendu: # les codes jury que l'on doit vérifier
+ deca:
+ passage_de_droit: False # d'apres les notes, on ne peut pas passer
+ autorisations_inscription: [2] # et le jury manuel nous fait passer
+ nb_competences: 2
+ nb_rcue_annee: 2
+ valide_moitie_rcue: False
+ codes: [ "ADJ", "ATJ", "RED", "..." ]
+ code_valide: RED # le code proposé en auto
+ decisions_ues:
+ "UE21":
+ codes: [ "AJ", "..." ]
+ code_valide: AJ
+ moy_ue: 9.8
+ "UE22":
+ code_valide: AJ
+ moy_ue: 9.9
+ decisions_rcues: # on repère ici les RCUE par l'acronyme de leur 1ere UE (donc du S1)
+ "UE11":
+ code_valide: AJ # le code proposé en auto
+ rcue:
+ # moy_rcue: 14.0000 # Pas de moyenne calculée
+ est_compensable: False
+ "UE12":
+ code_valide: AJ # le code proposé en auto
+ rcue:
+ # moy_rcue: 13.5000 # Pas de moyenne calculée
+ est_compensable: False
+ decision_annee: ADJ # Passage tout de même en S3 !
+ S3: # le S3 avec 4 niveaux
+ parcours: AII
+ notes_modules: # combinaison pour avoir ADM AJ AJ AJ
+ "AII3": 9
+ "ER3": 10.75
+ "AU3": 8
+ attendu: # les codes jury que l'on doit vérifier
+ deca:
+ passage_de_droit: False # d'apres les notes, on ne peut pas passer
+ autorisations_inscription: [4] # passe en S4
+ nb_competences: 4
+ S4: # le S4 avec 4 niveaux
+ parcours: AII
+ notes_modules: # combinaison pour avoir ADM ADM ADM AJ
+ "PF4": 12
+ "SAE4AII": 8