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 680e3ff98..516d8fc51 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -859,7 +859,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 b2686445a..3a173f037 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