diff --git a/app/but/jury_but.py b/app/but/jury_but.py index 31367a98e..2b61c4697 100644 --- a/app/but/jury_but.py +++ b/app/but/jury_but.py @@ -284,15 +284,11 @@ class DecisionsProposeesAnnee(DecisionsProposees): # ---- Décision année et autorisation self.autorisations_recorded = False "vrai si on a enregistré l'autorisation de passage" - self.validation = ( - ApcValidationAnnee.query.filter_by( - etudid=self.etud.id, - ordre=self.annee_but, - ) - .join(Formation) - .filter_by(formation_code=self.formsemestre.formation.formation_code) - .first() - ) + self.validation = ApcValidationAnnee.query.filter_by( + etudid=self.etud.id, + ordre=self.annee_but, + referentiel_competence_id=self.formsemestre.formation.referentiel_competence_id, + ).first() "Validation actuellement enregistrée pour cette année BUT" self.code_valide = self.validation.code if self.validation is not None else None "Le code jury annuel enregistré, ou None" @@ -689,13 +685,13 @@ class DecisionsProposeesAnnee(DecisionsProposees): self.validation = ApcValidationAnnee( etudid=self.etud.id, formsemestre=self.formsemestre_impair, - formation_id=self.formsemestre.formation_id, + referentiel_competence_id=self.formsemestre.formation.referentiel_competence_id, ordre=self.annee_but, annee_scolaire=self.annee_scolaire(), code=code, ) else: # Update validation année BUT - self.validation.etud = self.etud + assert self.validation.etudid == self.etud.id self.validation.formsemestre = self.formsemestre_impair self.validation.formation_id = self.formsemestre.formation_id self.validation.ordre = self.annee_but @@ -852,13 +848,10 @@ class DecisionsProposeesAnnee(DecisionsProposees): # Efface les validations concernant l'année BUT # de ce semestre - validations = ( - ApcValidationAnnee.query.filter_by( - etudid=self.etud.id, - ordre=self.annee_but, - ) - .join(Formation) - .filter_by(formation_code=self.formsemestre.formation.formation_code) + validations = ApcValidationAnnee.query.filter_by( + etudid=self.etud.id, + ordre=self.annee_but, + referentiel_competence_id=self.formsemestre.formation.referentiel_competence_id, ) for validation in validations: db.session.delete(validation) @@ -1287,15 +1280,11 @@ class DecisionsProposeesRCUE(DecisionsProposees): 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, - ) - .join(Formation) - .filter_by(formation_code=self.deca.formsemestre.formation.formation_code) - .all() - ) + validations_annee: ApcValidationAnnee = ApcValidationAnnee.query.filter_by( + etudid=self.etud.id, + ordre=annee_inferieure, + referentiel_competence_id=self.deca.formsemestre.formation.referentiel_competence_id, + ).all() if len(validations_annee) > 1: log( f"warning: {len(validations_annee)} validations d'année\n{validations_annee}" @@ -1332,8 +1321,8 @@ class DecisionsProposeesRCUE(DecisionsProposees): validation_annee = ApcValidationAnnee( etudid=self.etud.id, ordre=annee_inferieure, + referentiel_competence_id=self.deca.formsemestre.formation.referentiel_competence_id, code=sco_codes.ADSUP, - formation_id=self.deca.formsemestre.formation_id, # met cette validation sur l'année scolaire actuelle, pas la précédente annee_scolaire=self.deca.formsemestre.annee_scolaire(), ) @@ -1575,16 +1564,11 @@ class DecisionsProposeesUE(DecisionsProposees): # 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=ordre, -# ) -# .join(Formation) -# .filter( -# Formation.formation_code == self.formsemestre.formation.formation_code +# referentiel_competence_id=self.formsemestre.formation.referentiel_competence_id # ) # .count() # > 0 diff --git a/app/comp/jury.py b/app/comp/jury.py index 1c43158da..ee2c13738 100644 --- a/app/comp/jury.py +++ b/app/comp/jury.py @@ -231,12 +231,11 @@ def erase_decisions_annee_formation( .all() ) # Année BUT - validations += ( - ApcValidationAnnee.query.filter_by(etudid=etud.id, ordre=annee) - .join(Formation) - .filter_by(formation_code=formation.formation_code) - .all() - ) + validations += ApcValidationAnnee.query.filter_by( + etudid=etud.id, + ordre=annee, + referentiel_competence_id=formation.referentiel_competence_id, + ).all() # Autorisations vers les semestres suivants ceux de l'année: validations += ( ScolarAutorisationInscription.query.filter_by( diff --git a/app/comp/res_but.py b/app/comp/res_but.py index 163d56f1d..b2ba3eb5c 100644 --- a/app/comp/res_but.py +++ b/app/comp/res_but.py @@ -337,17 +337,15 @@ class ResultatsSemestreBUT(NotesTableCompat): if self.validations_annee: return self.validations_annee annee_but = (self.formsemestre.semestre_id + 1) // 2 - validations = ( - ApcValidationAnnee.query.filter_by(ordre=annee_but) - .join(Formation) - .filter_by(formation_code=self.formsemestre.formation.formation_code) - .join( - FormSemestreInscription, - db.and_( - FormSemestreInscription.etudid == ApcValidationAnnee.etudid, - FormSemestreInscription.formsemestre_id == self.formsemestre.id, - ), - ) + validations = ApcValidationAnnee.query.filter_by( + ordre=annee_but, + referentiel_competence_id=self.formsemestre.formation.referentiel_competence_id, + ).join( + FormSemestreInscription, + db.and_( + FormSemestreInscription.etudid == ApcValidationAnnee.etudid, + FormSemestreInscription.formsemestre_id == self.formsemestre.id, + ), ) validation_by_etud = {} for validation in validations: diff --git a/app/models/but_refcomp.py b/app/models/but_refcomp.py index 824e7e3b6..c4983bdd2 100644 --- a/app/models/but_refcomp.py +++ b/app/models/but_refcomp.py @@ -94,6 +94,11 @@ class ApcReferentielCompetences(db.Model, XMLModel): backref="referentiel_competence", order_by="Formation.acronyme, Formation.version", ) + validations_annee = db.relationship( + "ApcValidationAnnee", + backref="referentiel_competence", + lazy="dynamic", + ) def __repr__(self): return f"" diff --git a/app/models/but_validations.py b/app/models/but_validations.py index 00dc61952..aedf4cbaf 100644 --- a/app/models/but_validations.py +++ b/app/models/but_validations.py @@ -2,8 +2,6 @@ """Décisions de jury (validations) des RCUE et années du BUT """ -from typing import Union - from app import db from app.models import CODE_STR_LEN @@ -106,71 +104,14 @@ class ApcValidationRCUE(db.Model): } -# unused -# def find_rcues( -# formsemestre: FormSemestre, ue: UniteEns, etud: Identite, inscription_etat: str -# ) -> list[RegroupementCoherentUE]: -# """Les RCUE (niveau de compétence) à considérer pour cet étudiant dans -# ce semestre pour cette UE. - -# Cherche les UEs du même niveau de compétence auxquelles l'étudiant est inscrit. -# En cas de redoublement, il peut y en avoir plusieurs, donc plusieurs RCUEs. - -# Résultat: la liste peut être vide. -# """ -# if (ue.niveau_competence is None) or (ue.semestre_idx is None): -# return [] - -# if ue.semestre_idx % 2: # S1, S3, S5 -# other_semestre_idx = ue.semestre_idx + 1 -# else: -# other_semestre_idx = ue.semestre_idx - 1 - -# cursor = db.session.execute( -# text( -# """SELECT -# ue.id, formsemestre.id -# FROM -# notes_ue ue, -# notes_formsemestre_inscription inscr, -# notes_formsemestre formsemestre - -# WHERE -# inscr.etudid = :etudid -# AND inscr.formsemestre_id = formsemestre.id - -# AND formsemestre.semestre_id = :other_semestre_idx -# AND ue.formation_id = formsemestre.formation_id -# AND ue.niveau_competence_id = :ue_niveau_competence_id -# AND ue.semestre_idx = :other_semestre_idx -# """ -# ), -# { -# "etudid": etud.id, -# "other_semestre_idx": other_semestre_idx, -# "ue_niveau_competence_id": ue.niveau_competence_id, -# }, -# ) -# rcues = [] -# for ue_id, formsemestre_id in cursor: -# other_ue = UniteEns.query.get(ue_id) -# other_formsemestre = FormSemestre.get_formsemestre(formsemestre_id) -# rcues.append( -# RegroupementCoherentUE( -# etud, formsemestre, ue, other_formsemestre, other_ue, inscription_etat -# ) -# ) -# # safety check: 1 seul niveau de comp. concerné: -# assert len({rcue.ue_1.niveau_competence_id for rcue in rcues}) == 1 -# return rcues - - class ApcValidationAnnee(db.Model): """Validation des années du BUT""" __tablename__ = "apc_validation_annee" # Assure unicité de la décision: - __table_args__ = (db.UniqueConstraint("etudid", "annee_scolaire", "ordre"),) + __table_args__ = ( + db.UniqueConstraint("etudid", "ordre", "referentiel_competence_id"), + ) id = db.Column(db.Integer, primary_key=True) etudid = db.Column( db.Integer, @@ -184,10 +125,8 @@ class ApcValidationAnnee(db.Model): db.Integer, db.ForeignKey("notes_formsemestre.id"), nullable=True ) "le semestre origine, normalement l'IMPAIR (le 1er) de l'année" - formation_id = db.Column( - db.Integer, - db.ForeignKey("notes_formations.id"), - nullable=False, + referentiel_competence_id = db.Column( + db.Integer, db.ForeignKey("apc_referentiel_competences.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()) @@ -207,17 +146,22 @@ class ApcValidationAnnee(db.Model): "dict pour bulletins" return { "annee_scolaire": self.annee_scolaire, - "date": self.date.isoformat(), + "date": self.date.isoformat() if self.date else "", "code": self.code, "ordre": self.ordre, } def html(self) -> str: "Affichage html" + date_str = ( + f"""le {self.date.strftime("%d/%m/%Y")} à {self.date.strftime("%Hh%M")}""" + if self.date + else "(sans date)" + ) return f"""Validation année BUT{self.ordre} émise par {self.formsemestre.html_link_status() if self.formsemestre else "-"} : {self.code} - le {self.date.strftime("%d/%m/%Y")} à {self.date.strftime("%Hh%M")} + {date_str} """ @@ -259,15 +203,11 @@ def dict_decision_jury(etud: Identite, formsemestre: FormSemestre) -> dict: decisions["descr_decisions_rcue"] = "" decisions["descr_decisions_niveaux"] = "" # --- Année: prend la validation pour l'année scolaire de ce semestre - validation = ( - ApcValidationAnnee.query.filter_by( - etudid=etud.id, - annee_scolaire=formsemestre.annee_scolaire(), - ) - .join(Formation) - .filter(Formation.formation_code == formsemestre.formation.formation_code) - .first() - ) + validation = ApcValidationAnnee.query.filter_by( + etudid=etud.id, + annee_scolaire=formsemestre.annee_scolaire(), + referentiel_competence_id=formsemestre.formation.referentiel_competence_id, + ).first() if validation: decisions["decision_annee"] = validation.to_dict_bul() else: diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index efbceb74b..cd296353f 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -867,15 +867,12 @@ class FormSemestre(db.Model): .order_by(UniteEns.numero) .all() ) - vals_annee = ( # issues de ce formsemestre seulement + vals_annee = ( # issues de cette année scolaire seulement ApcValidationAnnee.query.filter_by( etudid=etudid, annee_scolaire=self.annee_scolaire(), - ) - .join(ApcValidationAnnee.formsemestre) - .join(FormSemestre.formation) - .filter(Formation.formation_code == self.formation.formation_code) - .all() + referentiel_competence_id=self.formation.referentiel_competence_id, + ).all() ) H = [] for vals in (vals_sem, vals_ues, vals_rcues, vals_annee): diff --git a/app/scodoc/sco_apogee_csv.py b/app/scodoc/sco_apogee_csv.py index 1348cbe8c..b8addd5be 100644 --- a/app/scodoc/sco_apogee_csv.py +++ b/app/scodoc/sco_apogee_csv.py @@ -491,15 +491,11 @@ class ApoEtud(dict): # ne trouve pas de semestre impair self.validation_annee_but = None return - self.validation_annee_but: ApcValidationAnnee = ( - ApcValidationAnnee.query.filter_by( - formsemestre_id=formsemestre.id, - etudid=self.etud["etudid"], - formation_id=self.cur_sem[ - "formation_id" - ], # XXX utiliser formation_code - ).first() - ) + self.validation_annee_but: ApcValidationAnnee = ApcValidationAnnee.query.filter_by( + formsemestre_id=formsemestre.id, + etudid=self.etud["etudid"], + referentiel_competence_id=self.cur_res.formsemestre.formation.referentiel_competence_id, + ).first() self.is_nar = ( self.validation_annee_but and self.validation_annee_but.code == NAR ) diff --git a/migrations/versions/829683efddc4_change_apcvalidationannee.py b/migrations/versions/829683efddc4_change_apcvalidationannee.py new file mode 100644 index 000000000..a2353e693 --- /dev/null +++ b/migrations/versions/829683efddc4_change_apcvalidationannee.py @@ -0,0 +1,112 @@ +"""Change ApcValidationAnnee + +Revision ID: 829683efddc4 +Revises: c701224fa255 +Create Date: 2023-06-28 09:47:16.591028 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.orm import sessionmaker # added by ev + +# revision identifiers, used by Alembic. +revision = "829683efddc4" +down_revision = "c701224fa255" +branch_labels = None +depends_on = None + +Session = sessionmaker() + + +# Voir https://stackoverflow.com/questions/24082542/check-if-a-table-column-exists-in-the-database-using-sqlalchemy-and-alembic +from sqlalchemy import inspect + + +def column_exists(table_name, column_name): + bind = op.get_context().bind + insp = inspect(bind) + columns = insp.get_columns(table_name) + return any(c["name"] == column_name for c in columns) + + +def upgrade(): + if column_exists("apc_validation_annee", "referentiel_competence_id"): + return # utile durant developpement + # Enleve la contrainte erronée + with op.batch_alter_table("apc_validation_annee", schema=None) as batch_op: + batch_op.drop_constraint( + "apc_validation_annee_etudid_annee_scolaire_ordre_key", type_="unique" + ) + # Ajoute colonne referentiel, nullable pour l'instant + batch_op.add_column( + sa.Column("referentiel_competence_id", sa.Integer(), nullable=True) + ) + + # Affecte le referentiel des anciennes validations + bind = op.get_bind() + session = Session(bind=bind) + session.execute( + sa.text( + """ + UPDATE apc_validation_annee AS a + SET referentiel_competence_id = ( + SELECT f.referentiel_competence_id + FROM notes_formations f + WHERE f.id = a.formation_id + ) + """ + ) + ) + # En principe, on n'a pas pu entrer de validation sur des formations sans referentiel + # par prudence, on les supprime avant d'ajouter la contrainte + session.execute( + sa.text( + "DELETE FROM apc_validation_annee WHERE referentiel_competence_id is NULL" + ) + ) + op.alter_column( + "apc_validation_annee", + "referentiel_competence_id", + nullable=False, + ) + op.create_foreign_key( + "apc_validation_annee_refcomp_fkey", + "apc_validation_annee", + "apc_referentiel_competences", + ["referentiel_competence_id"], + ["id"], + ) + # Efface les validations d'année dupliquées + # (garde le premier code dans l'ordre alphabétique... mieux que rien) + session.execute( + sa.text( + """ + DELETE FROM apc_validation_annee t1 + WHERE EXISTS ( + SELECT 1 + FROM apc_validation_annee t2 + WHERE t1.etudid = t2.etudid + AND t1.referentiel_competence_id = t2.referentiel_competence_id + AND t1.ordre = t2.ordre + AND t1.code > t2.code + ); + """ + ) + ) + # Et ajoute la contrainte unicité de décision année par étudiant/ref. comp.: + op.create_unique_constraint( + "apc_validation_annee_etudid_ordre_refcomp_key", + "apc_validation_annee", + ["etudid", "ordre", "referentiel_competence_id"], + ) + op.drop_column("apc_validation_annee", "formation_id") + + +def downgrade(): + # Se contente de ré-ajouter la colonne formation_id sans re-générer son contenu + with op.batch_alter_table("apc_validation_annee", schema=None) as batch_op: + # batch_op.drop_constraint( + # "apc_validation_annee_etudid_ordre_refcomp_key", type_="unique" + # ) + # batch_op.drop_column("referentiel_competence_id") + batch_op.add_column(sa.Column("formation_id", sa.Integer(), nullable=True))