From 6891d9f1c166c3c5b4d8e5ce74c6ce116dbcbfc9 Mon Sep 17 00:00:00 2001
From: Emmanuel Viennet
Date: Thu, 15 Jun 2023 08:49:05 +0200
Subject: [PATCH] =?UTF-8?q?BUT:=20jury:=20validation=20des=20niveaux=20inf?=
=?UTF-8?q?=C3=A9rieurs.=20WIP?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
app/but/cursus_but.py | 36 +--
app/but/jury_but.py | 223 +++++++++++++++++-
app/comp/res_but.py | 5 +-
app/models/but_validations.py | 12 +-
app/models/formsemestre.py | 2 +-
app/scodoc/codes_cursus.py | 20 +-
app/scodoc/sco_apogee_csv.py | 9 +-
app/scodoc/sco_formsemestre_validation.py | 7 +-
...1224fa255_validation_niveaux_inferieurs.py | 63 +++++
sco_version.py | 2 +-
tests/ressources/yaml/cursus_but_gccd_cy.yaml | 129 ++++++----
11 files changed, 414 insertions(+), 94 deletions(-)
create mode 100644 migrations/versions/c701224fa255_validation_niveaux_inferieurs.py
diff --git a/app/but/cursus_but.py b/app/but/cursus_but.py
index 4654a9cb5..89a7bd669 100644
--- a/app/but/cursus_but.py
+++ b/app/but/cursus_but.py
@@ -104,7 +104,7 @@ class EtudCursusBUT:
self.parcour: ApcParcours = self.inscriptions[-1].parcour
"Le parcours à valider: celui du DERNIER semestre suivi (peut être None)"
self.niveaux_by_annee = {}
- "{ annee : liste des niveaux à valider }"
+ "{ annee:int : liste des niveaux à valider }"
self.niveaux: dict[int, ApcNiveau] = {}
"cache les niveaux"
for annee in (1, 2, 3):
@@ -118,21 +118,6 @@ class EtudCursusBUT:
self.niveaux.update(
{niveau.id: niveau for niveau in self.niveaux_by_annee[annee]}
)
- # Probablement inutile:
- # # Cherche les validations de jury enregistrées pour chaque niveau
- # self.validations_by_niveau = collections.defaultdict(lambda: [])
- # " { niveau_id : [ ApcValidationRCUE ] }"
- # for validation_rcue in ApcValidationRCUE.query.filter_by(etud=etud):
- # self.validations_by_niveau[validation_rcue.niveau().id].append(
- # validation_rcue
- # )
- # self.validation_by_niveau = {
- # niveau_id: sorted(
- # validations, key=lambda v: sco_codes.BUT_CODES_ORDERED[v.code]
- # )[0]
- # for niveau_id, validations in self.validations_by_niveau.items()
- # }
- # "{ niveau_id : meilleure validation pour ce niveau }"
self.validation_par_competence_et_annee = {}
"""{ competence_id : { 'BUT1' : validation_rcue (la "meilleure"), ... } }"""
@@ -146,7 +131,7 @@ class EtudCursusBUT:
# prend la "meilleure" validation
if (not previous_validation) or (
sco_codes.BUT_CODES_ORDERED[validation_rcue.code]
- > sco_codes.BUT_CODES_ORDERED[previous_validation["code"]]
+ > sco_codes.BUT_CODES_ORDERED[previous_validation.code]
):
self.validation_par_competence_et_annee[niveau.competence.id][
niveau.annee
@@ -206,6 +191,23 @@ class EtudCursusBUT:
)
return d
+ def load_validation_by_niveau(self) -> dict[int, list[ApcValidationRCUE]]:
+ """Cherche les validations de jury enregistrées pour chaque niveau
+ Résultat: { niveau_id : [ ApcValidationRCUE ] }
+ meilleure validation pour ce niveau
+ """
+ validations_by_niveau = collections.defaultdict(lambda: [])
+ for validation_rcue in ApcValidationRCUE.query.filter_by(etud=self.etud):
+ validations_by_niveau[validation_rcue.niveau().id].append(validation_rcue)
+ validation_by_niveau = {
+ niveau_id: sorted(
+ validations, key=lambda v: sco_codes.BUT_CODES_ORDERED[v.code]
+ )[0]
+ for niveau_id, validations in validations_by_niveau.items()
+ if validations
+ }
+ return validation_by_niveau
+
class FormSemestreCursusBUT:
"""L'état des étudiants d'un formsemestre dans leur cursus BUT
diff --git a/app/but/jury_but.py b/app/but/jury_but.py
index 1caa44118..12e14bfb8 100644
--- a/app/but/jury_but.py
+++ b/app/but/jury_but.py
@@ -58,6 +58,7 @@ DecisionsProposeesUE: décisions de jury sur une UE du BUT
DecisionsProposeesRCUE appelera .set_compensable()
si on a la possibilité de la compenser dans le RCUE.
"""
+from datetime import datetime
import html
from operator import attrgetter
import re
@@ -68,6 +69,7 @@ from flask import flash, g, url_for
from app import db
from app import log
+from app.but.cursus_but import EtudCursusBUT
from app.comp.res_but import ResultatsSemestreBUT
from app.comp import res_sem
@@ -92,6 +94,7 @@ from app.models.validations import ScolarFormSemestreValidation
from app.scodoc import sco_cache
from app.scodoc import codes_cursus as sco_codes
from app.scodoc.codes_cursus import (
+ code_rcue_validant,
BUT_CODES_ORDERED,
CODES_RCUE_VALIDES,
CODES_UE_CAPITALISANTS,
@@ -275,6 +278,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
if self.formsemestre_impair is not None:
self.validation = ApcValidationAnnee.query.filter_by(
etudid=self.etud.id,
+ formation_id=self.formsemestre.formation_id,
formsemestre_id=formsemestre_impair.id,
ordre=self.annee_but,
).first()
@@ -755,6 +759,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
self.validation = ApcValidationAnnee(
etudid=self.etud.id,
formsemestre=self.formsemestre_impair,
+ formation_id=self.formsemestre.formation_id,
ordre=self.annee_but,
annee_scolaire=self.annee_scolaire(),
code=code,
@@ -900,6 +905,9 @@ class DecisionsProposeesAnnee(DecisionsProposees):
)
validations = ApcValidationAnnee.query.filter_by(
etudid=self.etud.id,
+ # XXX efface les validations émise depuis ce semestre
+ # et pas toutes celles concernant cette l'année...
+ # (utiliser formation_id pour changer cette politique)
formsemestre_id=self.formsemestre_impair.id,
ordre=self.annee_but,
)
@@ -1035,6 +1043,9 @@ class DecisionsProposeesRCUE(DecisionsProposees):
):
super().__init__(etud=dec_prop_annee.etud)
self.deca = dec_prop_annee
+ self.referentiel_competence_id = (
+ self.deca.formsemestre.formation.referentiel_competence_id
+ )
self.rcue = rcue
if rcue is None: # RCUE non dispo, eg un seul semestre
self.codes = []
@@ -1139,7 +1150,8 @@ class DecisionsProposeesRCUE(DecisionsProposees):
dec_ue.record(sco_codes.ADJR)
# Valide les niveaux inférieurs de la compétence (code ADSUP)
- # TODO
+ if code in CODES_RCUE_VALIDES:
+ self.valide_niveau_inferieur()
if self.rcue.formsemestre_1 is not None:
sco_cache.invalidate_formsemestre(
@@ -1177,6 +1189,189 @@ class DecisionsProposeesRCUE(DecisionsProposees):
return f"{niveau_titre}-{ordre}"
return ""
+ def valide_niveau_inferieur(self) -> None:
+ """Appelé juste après la validation d'un RCUE.
+ *La validation des deux UE du niveau d’une compétence emporte la validation de
+ l’ensemble des UEs du niveau inférieur de cette même compétence.*
+ """
+ if not self.rcue or not self.rcue.ue_1 or not self.rcue.ue_1.niveau_competence:
+ return
+ competence: ApcCompetence = self.rcue.ue_1.niveau_competence.competence
+ ordre_inferieur = self.rcue.ue_1.niveau_competence.ordre - 1
+ if ordre_inferieur < 1:
+ return # pas de niveau inferieur
+
+ # --- Si le RCUE inférieur est déjà validé, ne fait rien
+ validations_rcue = (
+ ApcValidationRCUE.query.filter_by(etudid=self.etud.id)
+ .join(UniteEns, UniteEns.id == ApcValidationRCUE.ue1_id)
+ .join(ApcNiveau)
+ .filter_by(ordre=ordre_inferieur)
+ .join(ApcCompetence)
+ .filter_by(id=competence.id)
+ .all()
+ )
+ if [v for v in validations_rcue if code_rcue_validant(v.code)]:
+ return # déjà validé
+
+ # --- Validations des UEs
+ ues, ue1, ue2 = self._get_ues_inferieures(competence, ordre_inferieur)
+ # Pour chaque UE inférieure non validée, valide:
+ for ue in ues:
+ validations_ue = ScolarFormSemestreValidation.query.filter_by(
+ etudid=self.etud.id, ue_id=ue.id
+ ).all()
+ if [
+ validation
+ for validation in validations_ue
+ if sco_codes.code_ue_validant(validation.code)
+ ]:
+ continue # on a déjà une validation
+ # aucune validation validante
+ validation_ue = validations_ue[0] if validations_ue else None
+ if validation_ue:
+ # Modifie validation existante
+ validation_ue.code = sco_codes.ADSUP
+ validation_ue.event_date = datetime.now()
+ if validation_ue.formsemestre_id is not None:
+ sco_cache.invalidate_formsemestre(
+ formsemestre_id=validation_ue.formsemestre_id
+ )
+ log(f"updating {validation_ue}")
+ else:
+ # Ajoute une validation,
+ # pas de formsemestre ni de note car pas une capitalisation
+ validation_ue = ScolarFormSemestreValidation(
+ etudid=self.etud.id,
+ code=sco_codes.ADSUP,
+ ue_id=ue.id,
+ is_external=True, # pas rattachée à un formsemestre
+ )
+ log(f"recording {validation_ue}")
+ db.session.add(validation_ue)
+
+ # Valide le RCUE inférieur
+ if validations_rcue:
+ # Met à jour validation existante
+ validation_rcue = validations_rcue[0]
+ validation_rcue.code = sco_codes.ADSUP
+ validation_rcue.date = datetime.now()
+ log(f"updating {validation_rcue}")
+ if validation_rcue.formsemestre_id is not None:
+ sco_cache.invalidate_formsemestre(
+ formsemestre_id=validation_rcue.formsemestre_id
+ )
+ else:
+ # Crée nouvelle validation
+ validation_rcue = ApcValidationRCUE(
+ etudid=self.etud.id, ue1_id=ue1.id, ue2_id=ue2.id, code=sco_codes.ADSUP
+ )
+ log(f"recording {validation_rcue}")
+ db.session.add(validation_rcue)
+ db.session.commit()
+ self.valide_annee_inferieure()
+
+ def valide_annee_inferieure(self) -> None:
+ """Si tous les RCUEs de l'année inférieure sont validés, la valide"""
+ # Indice de l'année inférieure:
+ annee_courante = self.rcue.ue_1.niveau_competence.annee # "BUT2"
+ if not re.match(r"^BUT\d$", annee_courante):
+ log("Warning: valide_annee_inferieure invalid annee_courante")
+ return
+ annee_inferieure = int(annee_courante[3]) - 1
+ if annee_inferieure < 1:
+ return
+ # Garde-fou: Année déjà validée ?
+ validations_annee: ApcValidationAnnee = ApcValidationAnnee.query.filter_by(
+ etudid=self.etud.id,
+ ordre=annee_inferieure,
+ formation_id=self.rcue.formsemestre_1.formation_id,
+ ).all()
+ if len(validations_annee) > 1:
+ log(
+ f"warning: {len(validations_annee)} validations d'année\n{validations_annee}"
+ )
+ if [
+ validation_annee
+ for validation_annee in validations_annee
+ if sco_codes.code_annee_validant(validation_annee.code)
+ ]:
+ return # déja valide
+ validation_annee = validations_annee[0] if validations_annee else None
+ # Liste des niveaux à valider:
+ # ici on sort l'artillerie lourde
+ cursus: EtudCursusBUT = EtudCursusBUT(
+ self.etud, self.rcue.formsemestre_1.formation
+ )
+ niveaux_a_valider = cursus.niveaux_by_annee[annee_inferieure]
+ # Pour chaque niveau, cherche validation RCUE
+ validations_by_niveau = cursus.load_validation_by_niveau()
+ ok = True
+ for niveau in niveaux_a_valider:
+ validation_niveau: ApcValidationRCUE = validations_by_niveau.get(niveau.id)
+ if not validation_niveau or not sco_codes.code_rcue_validant(
+ validation_niveau.code
+ ):
+ ok = False
+
+ # Si tous OK, émet validation année
+ if validation_annee: # Modifie la validation antérieure (non validante)
+ validation_annee.code = sco_codes.ADSUP
+ validation_annee.date = datetime.now()
+ log(f"updating {validation_annee}")
+ else:
+ validation_annee = ApcValidationAnnee(
+ etudid=self.etud.id,
+ ordre=annee_inferieure,
+ code=sco_codes.ADSUP,
+ formation_id=self.rcue.formsemestre_1.formation_id,
+ # met cette validation sur l'année scolaire actuelle, pas la précédente (??)
+ annee_scolaire=self.rcue.formsemestre_1.annee_scolaire(),
+ )
+ log(f"recording {validation_annee}")
+ db.session.add(validation_annee)
+ db.session.commit()
+
+ def _get_ues_inferieures(
+ self, competence: ApcCompetence, ordre_inferieur: int
+ ) -> tuple[list[UniteEns], UniteEns, UniteEns]:
+ """Les UEs de cette formation associées au niveau de compétence inférieur ?
+ Note: on ne cherche que dans la formation courante, pas les UEs de
+ même code d'autres formations.
+ """
+ formation: Formation = self.rcue.formsemestre_1.formation
+ ues: list[UniteEns] = (
+ UniteEns.query.filter_by(formation_id=formation.id)
+ .filter(UniteEns.semestre_idx != None)
+ .join(ApcNiveau)
+ .filter_by(ordre=ordre_inferieur)
+ .join(ApcCompetence)
+ .filter_by(id=competence.id)
+ .all()
+ )
+ log(f"valide_niveau_inferieur: {competence} UEs inférieures: {ues}")
+ if len(ues) != 2: # on n'a pas 2 UE associées au niveau inférieur !
+ flash(
+ "Impossible de valider le niveau de compétence inférieur: pas 2 UEs associées'",
+ "warning",
+ )
+ return
+ ues_impaires = [ue for ue in ues if ue.semestre_idx % 2]
+ if len(ues_impaires) != 1:
+ flash(
+ "Impossible de valider le niveau de compétence inférieur: pas d'UE impaire associée"
+ )
+ return
+ ue1 = ues_impaires[0]
+ ues_paires = [ue for ue in ues if not ue.semestre_idx % 2]
+ if len(ues_paires) != 1:
+ flash(
+ "Impossible de valider le niveau de compétence inférieur: pas d'UE paire associée"
+ )
+ return
+ ue2 = ues_paires[0]
+ return ues, ue1, ue2
+
class DecisionsProposeesUE(DecisionsProposees):
"""Décisions de jury sur une UE du BUT
@@ -1383,23 +1578,29 @@ class BUTCursusEtud: # WIP TODO
for competence in self.competences_du_parcours()
)
- def est_diplome(self) -> bool:
- """Vrai si BUT déjà validé"""
- # vrai si la troisième année est validée
- # On cherche les validations de 3ieme annee (ordre=3) avec le même référentiel
- # de formation que nous.
+ def est_annee_validee(self, ordre: int) -> bool:
+ """Vrai si l'année BUT ordre est validée"""
+ # On cherche les validations d'annee avec le même
+ # code formation que nous.
return (
- ApcValidationAnnee.query.filter_by(etudid=self.etud.id, ordre=3)
- .join(FormSemestre, FormSemestre.id == ApcValidationAnnee.formsemestre_id)
- .join(Formation, FormSemestre.formation_id == Formation.id)
+ ApcValidationAnnee.query.filter_by(
+ etudid=self.etud.id,
+ ordre=ordre,
+ formation_id=self.formsemestre.formation_id,
+ )
+ .join(Formation)
.filter(
- Formation.referentiel_competence_id
- == self.formsemestre.formation.referentiel_competence_id
+ Formation.formation_code == self.formsemestre.formation.formation_code
)
.count()
> 0
)
+ def est_diplome(self) -> bool:
+ """Vrai si BUT déjà validé"""
+ # vrai si la troisième année est validée
+ return self.est_annee_validee(3)
+
def competences_du_parcours(self) -> list[ApcCompetence]:
"""Construit liste des compétences du parcours, qui doivent être
validées pour obtenir le diplôme.
diff --git a/app/comp/res_but.py b/app/comp/res_but.py
index e4602327b..a91b1dbbc 100644
--- a/app/comp/res_but.py
+++ b/app/comp/res_but.py
@@ -307,9 +307,10 @@ class ResultatsSemestreBUT(NotesTableCompat):
return ues_ids
def etud_has_decision(self, etudid) -> bool:
- """True s'il y a une décision de jury pour cet étudiant émanant de ce formsemestre.
+ """True s'il y a une décision (quelconque) de jury
+ émanant de ce formsemestre pour cet étudiant.
prend aussi en compte les autorisations de passage.
- Sous-classée en BUT pour les RCUEs et années.
+ Ici sous-classée (BUT) pour les RCUEs et années.
"""
return bool(
super().etud_has_decision(etudid)
diff --git a/app/models/but_validations.py b/app/models/but_validations.py
index a21cd071f..d9b0e7e2d 100644
--- a/app/models/but_validations.py
+++ b/app/models/but_validations.py
@@ -320,7 +320,12 @@ class ApcValidationAnnee(db.Model):
db.Integer, db.ForeignKey("notes_formsemestre.id"), nullable=True
)
"le semestre IMPAIR (le 1er) de l'année"
- annee_scolaire = db.Column(db.Integer, nullable=False) # 2021
+ formation_id = db.Column(
+ db.Integer,
+ db.ForeignKey("notes_formations.id"),
+ nullable=False,
+ )
+ annee_scolaire = db.Column(db.Integer, nullable=False) # eg 2021
date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
code = db.Column(db.String(CODE_STR_LEN), nullable=False, index=True)
@@ -348,7 +353,7 @@ def dict_decision_jury(etud: Identite, formsemestre: FormSemestre) -> dict:
"""
Un dict avec les décisions de jury BUT enregistrées:
- decision_rcue : list[dict]
- - decision_annee : dict
+ - decision_annee : dict (décision issue de ce semestre seulement (à confirmer ?))
Ne reprend pas les décisions d'UE, non spécifiques au BUT.
"""
decisions = {}
@@ -383,8 +388,7 @@ def dict_decision_jury(etud: Identite, formsemestre: FormSemestre) -> dict:
etudid=etud.id,
annee_scolaire=formsemestre.annee_scolaire(),
)
- .join(ApcValidationAnnee.formsemestre)
- .join(FormSemestre.formation)
+ .join(Formation)
.filter(Formation.formation_code == formsemestre.formation.formation_code)
.first()
)
diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py
index e4b3e47b5..47957c849 100644
--- a/app/models/formsemestre.py
+++ b/app/models/formsemestre.py
@@ -865,7 +865,7 @@ class FormSemestre(db.Model):
.order_by(UniteEns.numero)
.all()
)
- vals_annee = (
+ vals_annee = ( # issues de ce formsemestre seulement
ApcValidationAnnee.query.filter_by(
etudid=etudid,
annee_scolaire=self.annee_scolaire(),
diff --git a/app/scodoc/codes_cursus.py b/app/scodoc/codes_cursus.py
index f2285ac12..228fb6d25 100644
--- a/app/scodoc/codes_cursus.py
+++ b/app/scodoc/codes_cursus.py
@@ -122,6 +122,7 @@ ABAN = "ABAN"
ABL = "ABL"
ADM = "ADM" # moyenne gen., barres UE, assiduité: sem. validé
ADC = "ADC" # admis par compensation (eg moy(S1, S2) > 10)
+ADSUP = "ADSUP" # BUT: UE ou RCUE validé par niveau supérieur
ADJ = "ADJ" # admis par le jury
ADJR = "ADJR" # UE admise car son RCUE est ADJ
ATT = "ATT" #
@@ -162,6 +163,7 @@ CODES_EXPL = {
ADJ: "Validé par le Jury",
ADJR: "UE validée car son RCUE est validé ADJ par le jury",
ADM: "Validé",
+ ADSUP: "UE ou RCUE validé car le niveau supérieur est validé",
AJ: "Ajourné (ou UE/BC de BUT en attente pour problème de moyenne)",
ATB: "Décision en attente d'un autre semestre (au moins une UE sous la barre)",
ATJ: "Décision en attente d'un autre semestre (assiduité insuffisante)",
@@ -195,17 +197,18 @@ CODES_SEM_ATTENTES = {ATT, ATB, ATJ} # semestre en attente
CODES_SEM_REO = {NAR} # reorientation
CODES_UE_VALIDES_DE_DROIT = {ADM, CMP} # validation "de droit"
-CODES_UE_VALIDES = CODES_UE_VALIDES_DE_DROIT | {ADJ, ADJR}
+CODES_UE_VALIDES = CODES_UE_VALIDES_DE_DROIT | {ADJ, ADJR, ADSUP}
"UE validée"
CODES_UE_CAPITALISANTS = {ADM}
"UE capitalisée"
CODES_RCUE_VALIDES_DE_DROIT = {ADM, CMP}
-CODES_RCUE_VALIDES = CODES_RCUE_VALIDES_DE_DROIT | {ADJ}
+CODES_RCUE_VALIDES = CODES_RCUE_VALIDES_DE_DROIT | {ADJ, ADSUP}
"Niveau RCUE validé"
# Pour le BUT:
-CODES_ANNEE_BUT_VALIDES_DE_DROIT = {ADM, PASD}
+CODES_ANNEE_BUT_VALIDES_DE_DROIT = {ADM} # PASD était ici mais retiré en juin 23
+CODES_ANNEE_BUT_VALIDES = {ADM, ADSUP}
CODES_ANNEE_ARRET = {DEF, DEM, ABAN, ABL}
BUT_BARRE_UE8 = 8.0 - NOTES_TOLERANCE
BUT_BARRE_UE = BUT_BARRE_RCUE = 10.0 - NOTES_TOLERANCE
@@ -229,6 +232,7 @@ BUT_CODES_ORDERED = {
PASD: 50,
PAS1NCI: 60,
ADJR: 90,
+ ADSUP: 90,
ADJ: 100,
ADM: 100,
}
@@ -249,6 +253,16 @@ def code_ue_validant(code: str) -> bool:
return code in CODES_UE_VALIDES
+def code_rcue_validant(code: str) -> bool:
+ "Vrai si ce code d'RCUE est validant"
+ return code in CODES_RCUE_VALIDES
+
+
+def code_annee_validant(code: str) -> bool:
+ "Vrai si code d'année BUT validant"
+ return code in CODES_ANNEE_BUT_VALIDES
+
+
DEVENIR_EXPL = {
NEXT: "Passage au semestre suivant",
REDOANNEE: "Redoublement année",
diff --git a/app/scodoc/sco_apogee_csv.py b/app/scodoc/sco_apogee_csv.py
index 898647e5a..1c02c3c16 100644
--- a/app/scodoc/sco_apogee_csv.py
+++ b/app/scodoc/sco_apogee_csv.py
@@ -473,7 +473,10 @@ class ApoEtud(dict):
)
def _but_load_validation_annuelle(self):
- "charge la validation de jury BUT annuelle"
+ """charge la validation de jury BUT annuelle.
+ Ici impose qu'elle soit issue d'un semestre de l'année en cours
+ (pas forcément nécessaire, voir selon les retours des équipes ?)
+ """
# le semestre impair de l'année scolaire
if self.cur_res.formsemestre.semestre_id % 2:
formsemestre = self.cur_res.formsemestre
@@ -490,7 +493,9 @@ class ApoEtud(dict):
return
self.validation_annee_but: ApcValidationAnnee = (
ApcValidationAnnee.query.filter_by(
- formsemestre_id=formsemestre.id, etudid=self.etud["etudid"]
+ formsemestre_id=formsemestre.id,
+ etudid=self.etud["etudid"],
+ formation_id=self.cur_sem["formation_id"],
).first()
)
self.is_nar = (
diff --git a/app/scodoc/sco_formsemestre_validation.py b/app/scodoc/sco_formsemestre_validation.py
index 19af25fe9..1822e3a65 100644
--- a/app/scodoc/sco_formsemestre_validation.py
+++ b/app/scodoc/sco_formsemestre_validation.py
@@ -66,6 +66,7 @@ from app.scodoc import sco_photos
from app.scodoc import sco_preferences
from app.scodoc import sco_pv_dict
+
# ------------------------------------------------------------------------------------
def formsemestre_validation_etud_form(
formsemestre_id=None, # required
@@ -1063,8 +1064,6 @@ def formsemestre_validate_previous_ue(formsemestre_id, etudid):
"""Form. saisie UE validée hors ScoDoc
(pour étudiants arrivant avec un UE antérieurement validée).
"""
- from app.scodoc import sco_formations
-
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
formation: Formation = Formation.query.get_or_404(sem["formation_id"])
@@ -1087,8 +1086,8 @@ def formsemestre_validate_previous_ue(formsemestre_id, etudid):
dans un semestre hors ScoDoc.
Les UE validées dans ScoDoc sont déjà
automatiquement prises en compte. Cette page n'est utile que pour les étudiants ayant
- suivi un début de cursus dans un autre établissement, ou bien dans un semestre géré sans
- ScoDoc et qui redouble ce semestre
+ suivi un début de cursus dans un autre établissement, ou bien dans un semestre géré
+ sans ScoDoc et qui redouble ce semestre
(ne pas utiliser pour les semestres précédents !).
Notez que l'UE est validée, avec enregistrement immédiat de la décision et
diff --git a/migrations/versions/c701224fa255_validation_niveaux_inferieurs.py b/migrations/versions/c701224fa255_validation_niveaux_inferieurs.py
new file mode 100644
index 000000000..08f275091
--- /dev/null
+++ b/migrations/versions/c701224fa255_validation_niveaux_inferieurs.py
@@ -0,0 +1,63 @@
+"""validation niveaux inferieurs
+
+Revision ID: c701224fa255
+Revises: d84bc592584e
+Create Date: 2023-06-11 11:08:05.553898
+
+"""
+from alembic import op
+import sqlalchemy as sa
+from sqlalchemy.orm import sessionmaker # added by ev
+
+# revision identifiers, used by Alembic.
+revision = "c701224fa255"
+down_revision = "d84bc592584e"
+branch_labels = None
+depends_on = None
+
+Session = sessionmaker()
+
+
+def upgrade():
+ # Ajoute la colonne formation_id, nullable, la peuple puis la rend non nullable
+ op.add_column(
+ "apc_validation_annee", sa.Column("formation_id", sa.Integer(), nullable=True)
+ )
+ op.create_foreign_key(
+ "apc_validation_annee_formation_id_fkey",
+ "apc_validation_annee",
+ "notes_formations",
+ ["formation_id"],
+ ["id"],
+ )
+
+ # Affecte la formation des anciennes validations
+ bind = op.get_bind()
+ session = Session(bind=bind)
+ session.execute(
+ sa.text(
+ """
+ UPDATE apc_validation_annee AS a
+ SET formation_id = (
+ SELECT f.id
+ FROM notes_formations f
+ JOIN notes_formsemestre s ON f.id = s.formation_id
+ WHERE s.id = a.formsemestre_id
+ )
+ WHERE a.formsemestre_id IS NOT NULL;
+ """
+ )
+ )
+ op.alter_column(
+ "apc_validation_annee",
+ "formation_id",
+ nullable=False,
+ )
+
+
+def downgrade():
+ with op.batch_alter_table("apc_validation_annee", schema=None) as batch_op:
+ batch_op.drop_constraint(
+ "apc_validation_annee_formation_id_fkey", type_="foreignkey"
+ )
+ batch_op.drop_column("formation_id")
diff --git a/sco_version.py b/sco_version.py
index 28f9304a4..8d1008831 100644
--- a/sco_version.py
+++ b/sco_version.py
@@ -1,7 +1,7 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
-SCOVERSION = "9.4.83"
+SCOVERSION = "9.4.84"
SCONAME = "ScoDoc"
diff --git a/tests/ressources/yaml/cursus_but_gccd_cy.yaml b/tests/ressources/yaml/cursus_but_gccd_cy.yaml
index 56446387a..668951d2c 100644
--- a/tests/ressources/yaml/cursus_but_gccd_cy.yaml
+++ b/tests/ressources/yaml/cursus_but_gccd_cy.yaml
@@ -1,122 +1,122 @@
# Tests unitaires jury BUT
# Essais avec un BUT GCCD (GC-CD) et un parcours de S1 à S6
-# Le GCCD est un programme à 5 compétences, dont certaines
+# Le GCCD est un programme à 5 compétences, dont certaines
# terminent en S4 ou en S6 selon les parcours.
ReferentielCompetences:
- filename: but-GCCD-05012022-081630.xml
+ filename: but-GCCD-05012022-081630.xml
specialite: GCCD
Formation:
filename: scodoc_formation_BUT_GC-CD_v2.xml
# Association des UEs aux compétences:
ues:
- # S1 tronc commun:
- 'UE1.1':
+ # S1 tronc commun:
+ "UE1.1":
annee: BUT1
competence: "Solutions Bâtiment"
- 'UE1.2':
+ "UE1.2":
annee: BUT1
competence: "Solutions TP"
- 'UE1.3':
+ "UE1.3":
annee: BUT1
competence: "Dimensionner"
- 'UE1.4':
+ "UE1.4":
annee: BUT1
competence: Organiser
- 'UE1.5':
+ "UE1.5":
annee: BUT1
competence: Piloter
- # S2 tronc commun:
- 'UE2.1':
+ # S2 tronc commun:
+ "UE2.1":
annee: BUT1
competence: "Solutions Bâtiment"
- 'UE2.2':
+ "UE2.2":
annee: BUT1
competence: "Solutions TP"
- 'UE2.3':
+ "UE2.3":
annee: BUT1
competence: "Dimensionner"
- 'UE2.4':
+ "UE2.4":
annee: BUT1
competence: Organiser
- 'UE2.5':
+ "UE2.5":
annee: BUT1
competence: Piloter
-
+
# S3 : Tronc commun
- 'UE3.1':
+ "UE3.1":
annee: BUT2
competence: "Solutions Bâtiment"
- 'UE3.2':
+ "UE3.2":
annee: BUT2
competence: "Solutions TP"
- 'UE3.3':
+ "UE3.3":
annee: BUT2
competence: "Dimensionner"
- 'UE3.4':
+ "UE3.4":
annee: BUT2
competence: Organiser
- 'UE3.5':
+ "UE3.5":
annee: BUT2
competence: Piloter
# S4 Tronc commun
- 'UE4.1':
+ "UE4.1":
annee: BUT2
competence: "Solutions Bâtiment"
- 'UE4.2':
+ "UE4.2":
annee: BUT2
competence: "Solutions TP"
- 'UE4.3':
+ "UE4.3":
annee: BUT2
competence: "Dimensionner"
- 'UE4.4':
+ "UE4.4":
annee: BUT2
competence: Organiser
- 'UE4.5':
+ "UE4.5":
annee: BUT2
competence: Piloter
# S5 Parcours BAT + TP
- 'UE5.1': # Parcours BAT seulement
+ "UE5.1": # Parcours BAT seulement
annee: BUT3
parcours: BAT # + RAPEB, BEC
competence: "Solutions Bâtiment"
- 'UE5.2': # Parcours TP seulement
+ "UE5.2": # Parcours TP seulement
annee: BUT3
parcours: TP # + BEC
competence: "Solutions TP"
- 'UE5.3':
+ "UE5.3":
annee: BUT3
parcours: [RAPEB, BEC]
competence: "Dimensionner"
- 'UE5.4':
+ "UE5.4":
annee: BUT3
parcours: [BAT, TP]
competence: Organiser
- 'UE5.5':
+ "UE5.5":
annee: BUT3
parcours: [BAT, TP]
competence: Piloter
# S6 Parcours BAT + TP
- 'UE6.1': # Parcours BAT seulement
+ "UE6.1": # Parcours BAT seulement
annee: BUT3
parcours: BAT # + RAPEB, BEC
competence: "Solutions Bâtiment"
- 'UE6.2': # Parcours TP seulement
+ "UE6.2": # Parcours TP seulement
annee: BUT3
- parcours: [TP,BEC]
+ parcours: [TP, BEC]
competence: "Solutions TP"
- 'UE6.3':
+ "UE6.3":
annee: BUT3
- parcours: [RAPEB,BEC]
+ parcours: [RAPEB, BEC]
competence: "Dimensionner"
- 'UE6.4':
+ "UE6.4":
annee: BUT3
parcours: [BAT, TP]
competence: Organiser
- 'UE6.5':
+ "UE6.5":
annee: BUT3
- parcours: [BAT,TP]
+ parcours: [BAT, TP]
competence: Piloter
modules_parcours:
@@ -126,8 +126,8 @@ Formation:
# - tous les module de S1 à S4 dans tous les parcours
# - SAE communes en S1 et S2 mais différenciées par parcours ensuite
# - en S5, ressources différenciées: on ne les mentionne pas toutes ici
- BAT: [ "R[1-4].*", "SAÉ [1-2]", "SAÉ *.BAT.*", "R5.0[1-7]", "R5.14" ]
- TP: [ "R[1-4].*", "SAÉ [1-2]", "SAÉ *.TP.*", "R5.0[1-4]", "R5.0[89]" ]
+ BAT: ["R[1-4].*", "SAÉ [1-2]", "SAÉ *.BAT.*", "R5.0[1-7]", "R5.14"]
+ TP: ["R[1-4].*", "SAÉ [1-2]", "SAÉ *.TP.*", "R5.0[1-4]", "R5.0[89]"]
FormSemestres:
# S1 et S2 avec les parcours BAT et TP:
@@ -135,32 +135,32 @@ FormSemestres:
idx: 1
date_debut: 2021-09-01
date_fin: 2022-01-15
- codes_parcours: ['BAT', 'TP']
- S2:
+ codes_parcours: ["BAT", "TP"]
+ S2:
idx: 2
date_debut: 2022-01-15
date_fin: 2022-06-30
- codes_parcours: ['BAT', 'TP']
+ codes_parcours: ["BAT", "TP"]
S3:
idx: 3
date_debut: 2022-09-01
date_fin: 2023-01-15
- codes_parcours: ['BAT', 'TP']
+ codes_parcours: ["BAT", "TP"]
S4:
idx: 4
date_debut: 2023-01-16
date_fin: 2023-06-30
- codes_parcours: ['BAT', 'TP']
+ codes_parcours: ["BAT", "TP"]
S5:
idx: 5
date_debut: 2023-09-01
date_fin: 2024-01-15
- codes_parcours: ['BAT', 'TP']
+ codes_parcours: ["BAT", "TP"]
S6:
idx: 6
date_debut: 2024-01-16
date_fin: 2024-06-30
- codes_parcours: ['BAT', 'TP']
+ codes_parcours: ["BAT", "TP"]
Etudiants:
A_ok: # Etudiant parcours BAT qui va tout valider directement
@@ -171,10 +171,18 @@ Etudiants:
parcours: BAT
notes_modules:
"R1.01": 11 # toutes UEs
+ "SAÉ 1-2": EXC
S2:
parcours: BAT
notes_modules:
"R2.01": 12 # toutes UEs
+ attendu: # les codes jury que l'on doit vérifier
+ deca:
+ passage_de_droit: True
+ autorisations_inscription: [3]
+ code_valide:
+ nb_competences: 5
+ nb_rcue_annee: 4
S3:
parcours: BAT
notes_modules:
@@ -186,7 +194,7 @@ Etudiants:
S5:
parcours: BAT
- dispense_ues: ['UE5.2', 'UE5.3']
+ dispense_ues: ["UE5.2", "UE5.3"]
notes_modules:
"R5.01": 15 # toutes UE
"SAÉ 5.BAT.01": 10 # UE5.1
@@ -202,6 +210,7 @@ Etudiants:
parcours: TP
notes_modules:
"R1.01": 11 # toutes UEs
+ "SAÉ 1-2": EXC
S2:
parcours: TP
notes_modules:
@@ -217,10 +226,32 @@ Etudiants:
S5:
parcours: TP
- dispense_ues: ['UE5.1', 'UE5.3']
+ dispense_ues: ["UE5.1", "UE5.3"]
notes_modules:
"R5.01": 15 # toutes UE
"SAÉ 5.BAT.01": 10 # UE5.1
"SAÉ 5.BAT.02": 11 # UE5.4
S6:
parcours: TP
+
+ C: # Etudiant qui passe sans un RCUE et valide en BUT2
+ prenom: Étudiant_TP_but2
+ civilite: M
+ formsemestres:
+ S1:
+ parcours: TP
+ notes_modules:
+ "R1.01": 11 # toutes UEs
+ "SAÉ 1-2": 8 # plombe l'UE 2
+ S2:
+ parcours: TP
+ notes_modules:
+ "R2.01": 11 # toutes UEs
+ S3:
+ parcours: TP
+ notes_modules:
+ "R3.01": 12 # toutes UEs
+ S4:
+ parcours: TP
+ notes_modules:
+ "R4.01": 14 # toutes UE