Jury BUT: modification validation année: unqiue sur ref. comp.

This commit is contained in:
Emmanuel Viennet 2023-06-30 09:34:29 +02:00
commit 61b46db4dd
8 changed files with 174 additions and 143 deletions

View File

@ -284,15 +284,11 @@ class DecisionsProposeesAnnee(DecisionsProposees):
# ---- Décision année et autorisation # ---- Décision année et autorisation
self.autorisations_recorded = False self.autorisations_recorded = False
"vrai si on a enregistré l'autorisation de passage" "vrai si on a enregistré l'autorisation de passage"
self.validation = ( self.validation = ApcValidationAnnee.query.filter_by(
ApcValidationAnnee.query.filter_by(
etudid=self.etud.id, etudid=self.etud.id,
ordre=self.annee_but, ordre=self.annee_but,
) referentiel_competence_id=self.formsemestre.formation.referentiel_competence_id,
.join(Formation) ).first()
.filter_by(formation_code=self.formsemestre.formation.formation_code)
.first()
)
"Validation actuellement enregistrée pour cette année BUT" "Validation actuellement enregistrée pour cette année BUT"
self.code_valide = self.validation.code if self.validation is not None else None self.code_valide = self.validation.code if self.validation is not None else None
"Le code jury annuel enregistré, ou None" "Le code jury annuel enregistré, ou None"
@ -689,13 +685,13 @@ class DecisionsProposeesAnnee(DecisionsProposees):
self.validation = ApcValidationAnnee( self.validation = ApcValidationAnnee(
etudid=self.etud.id, etudid=self.etud.id,
formsemestre=self.formsemestre_impair, formsemestre=self.formsemestre_impair,
formation_id=self.formsemestre.formation_id, referentiel_competence_id=self.formsemestre.formation.referentiel_competence_id,
ordre=self.annee_but, ordre=self.annee_but,
annee_scolaire=self.annee_scolaire(), annee_scolaire=self.annee_scolaire(),
code=code, code=code,
) )
else: # Update validation année BUT 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.formsemestre = self.formsemestre_impair
self.validation.formation_id = self.formsemestre.formation_id self.validation.formation_id = self.formsemestre.formation_id
self.validation.ordre = self.annee_but self.validation.ordre = self.annee_but
@ -852,13 +848,10 @@ class DecisionsProposeesAnnee(DecisionsProposees):
# Efface les validations concernant l'année BUT # Efface les validations concernant l'année BUT
# de ce semestre # de ce semestre
validations = ( validations = ApcValidationAnnee.query.filter_by(
ApcValidationAnnee.query.filter_by(
etudid=self.etud.id, etudid=self.etud.id,
ordre=self.annee_but, ordre=self.annee_but,
) referentiel_competence_id=self.formsemestre.formation.referentiel_competence_id,
.join(Formation)
.filter_by(formation_code=self.formsemestre.formation.formation_code)
) )
for validation in validations: for validation in validations:
db.session.delete(validation) db.session.delete(validation)
@ -1287,15 +1280,11 @@ class DecisionsProposeesRCUE(DecisionsProposees):
if annee_inferieure < 1: if annee_inferieure < 1:
return return
# Garde-fou: Année déjà validée ? # Garde-fou: Année déjà validée ?
validations_annee: ApcValidationAnnee = ( validations_annee: ApcValidationAnnee = ApcValidationAnnee.query.filter_by(
ApcValidationAnnee.query.filter_by(
etudid=self.etud.id, etudid=self.etud.id,
ordre=annee_inferieure, ordre=annee_inferieure,
) referentiel_competence_id=self.deca.formsemestre.formation.referentiel_competence_id,
.join(Formation) ).all()
.filter_by(formation_code=self.deca.formsemestre.formation.formation_code)
.all()
)
if len(validations_annee) > 1: if len(validations_annee) > 1:
log( log(
f"warning: {len(validations_annee)} validations d'année\n{validations_annee}" f"warning: {len(validations_annee)} validations d'année\n{validations_annee}"
@ -1332,8 +1321,8 @@ class DecisionsProposeesRCUE(DecisionsProposees):
validation_annee = ApcValidationAnnee( validation_annee = ApcValidationAnnee(
etudid=self.etud.id, etudid=self.etud.id,
ordre=annee_inferieure, ordre=annee_inferieure,
referentiel_competence_id=self.deca.formsemestre.formation.referentiel_competence_id,
code=sco_codes.ADSUP, 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 # met cette validation sur l'année scolaire actuelle, pas la précédente
annee_scolaire=self.deca.formsemestre.annee_scolaire(), annee_scolaire=self.deca.formsemestre.annee_scolaire(),
) )
@ -1575,16 +1564,11 @@ class DecisionsProposeesUE(DecisionsProposees):
# def est_annee_validee(self, ordre: int) -> bool: # def est_annee_validee(self, ordre: int) -> bool:
# """Vrai si l'année BUT ordre est validée""" # """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 ( # return (
# ApcValidationAnnee.query.filter_by( # ApcValidationAnnee.query.filter_by(
# etudid=self.etud.id, # etudid=self.etud.id,
# ordre=ordre, # ordre=ordre,
# ) # referentiel_competence_id=self.formsemestre.formation.referentiel_competence_id
# .join(Formation)
# .filter(
# Formation.formation_code == self.formsemestre.formation.formation_code
# ) # )
# .count() # .count()
# > 0 # > 0

View File

@ -231,12 +231,11 @@ def erase_decisions_annee_formation(
.all() .all()
) )
# Année BUT # Année BUT
validations += ( validations += ApcValidationAnnee.query.filter_by(
ApcValidationAnnee.query.filter_by(etudid=etud.id, ordre=annee) etudid=etud.id,
.join(Formation) ordre=annee,
.filter_by(formation_code=formation.formation_code) referentiel_competence_id=formation.referentiel_competence_id,
.all() ).all()
)
# Autorisations vers les semestres suivants ceux de l'année: # Autorisations vers les semestres suivants ceux de l'année:
validations += ( validations += (
ScolarAutorisationInscription.query.filter_by( ScolarAutorisationInscription.query.filter_by(

View File

@ -337,18 +337,16 @@ class ResultatsSemestreBUT(NotesTableCompat):
if self.validations_annee: if self.validations_annee:
return self.validations_annee return self.validations_annee
annee_but = (self.formsemestre.semestre_id + 1) // 2 annee_but = (self.formsemestre.semestre_id + 1) // 2
validations = ( validations = ApcValidationAnnee.query.filter_by(
ApcValidationAnnee.query.filter_by(ordre=annee_but) ordre=annee_but,
.join(Formation) referentiel_competence_id=self.formsemestre.formation.referentiel_competence_id,
.filter_by(formation_code=self.formsemestre.formation.formation_code) ).join(
.join(
FormSemestreInscription, FormSemestreInscription,
db.and_( db.and_(
FormSemestreInscription.etudid == ApcValidationAnnee.etudid, FormSemestreInscription.etudid == ApcValidationAnnee.etudid,
FormSemestreInscription.formsemestre_id == self.formsemestre.id, FormSemestreInscription.formsemestre_id == self.formsemestre.id,
), ),
) )
)
validation_by_etud = {} validation_by_etud = {}
for validation in validations: for validation in validations:
if validation.etudid in validation_by_etud: if validation.etudid in validation_by_etud:

View File

@ -94,6 +94,11 @@ class ApcReferentielCompetences(db.Model, XMLModel):
backref="referentiel_competence", backref="referentiel_competence",
order_by="Formation.acronyme, Formation.version", order_by="Formation.acronyme, Formation.version",
) )
validations_annee = db.relationship(
"ApcValidationAnnee",
backref="referentiel_competence",
lazy="dynamic",
)
def __repr__(self): def __repr__(self):
return f"<ApcReferentielCompetences {self.id} {self.specialite!r} {self.departement!r}>" return f"<ApcReferentielCompetences {self.id} {self.specialite!r} {self.departement!r}>"

View File

@ -2,8 +2,6 @@
"""Décisions de jury (validations) des RCUE et années du BUT """Décisions de jury (validations) des RCUE et années du BUT
""" """
from typing import Union
from app import db from app import db
from app.models import CODE_STR_LEN 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): class ApcValidationAnnee(db.Model):
"""Validation des années du BUT""" """Validation des années du BUT"""
__tablename__ = "apc_validation_annee" __tablename__ = "apc_validation_annee"
# Assure unicité de la décision: # 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) id = db.Column(db.Integer, primary_key=True)
etudid = db.Column( etudid = db.Column(
db.Integer, db.Integer,
@ -184,10 +125,8 @@ class ApcValidationAnnee(db.Model):
db.Integer, db.ForeignKey("notes_formsemestre.id"), nullable=True db.Integer, db.ForeignKey("notes_formsemestre.id"), nullable=True
) )
"le semestre origine, normalement l'IMPAIR (le 1er) de l'année" "le semestre origine, normalement l'IMPAIR (le 1er) de l'année"
formation_id = db.Column( referentiel_competence_id = db.Column(
db.Integer, db.Integer, db.ForeignKey("apc_referentiel_competences.id"), nullable=False
db.ForeignKey("notes_formations.id"),
nullable=False,
) )
annee_scolaire = db.Column(db.Integer, nullable=False) # eg 2021 annee_scolaire = db.Column(db.Integer, nullable=False) # eg 2021
date = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
@ -207,17 +146,22 @@ class ApcValidationAnnee(db.Model):
"dict pour bulletins" "dict pour bulletins"
return { return {
"annee_scolaire": self.annee_scolaire, "annee_scolaire": self.annee_scolaire,
"date": self.date.isoformat(), "date": self.date.isoformat() if self.date else "",
"code": self.code, "code": self.code,
"ordre": self.ordre, "ordre": self.ordre,
} }
def html(self) -> str: def html(self) -> str:
"Affichage html" "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 <b>année BUT{self.ordre}</b> émise par return f"""Validation <b>année BUT{self.ordre}</b> émise par
{self.formsemestre.html_link_status() if self.formsemestre else "-"} {self.formsemestre.html_link_status() if self.formsemestre else "-"}
: <b>{self.code}</b> : <b>{self.code}</b>
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_rcue"] = ""
decisions["descr_decisions_niveaux"] = "" decisions["descr_decisions_niveaux"] = ""
# --- Année: prend la validation pour l'année scolaire de ce semestre # --- Année: prend la validation pour l'année scolaire de ce semestre
validation = ( validation = ApcValidationAnnee.query.filter_by(
ApcValidationAnnee.query.filter_by(
etudid=etud.id, etudid=etud.id,
annee_scolaire=formsemestre.annee_scolaire(), annee_scolaire=formsemestre.annee_scolaire(),
) referentiel_competence_id=formsemestre.formation.referentiel_competence_id,
.join(Formation) ).first()
.filter(Formation.formation_code == formsemestre.formation.formation_code)
.first()
)
if validation: if validation:
decisions["decision_annee"] = validation.to_dict_bul() decisions["decision_annee"] = validation.to_dict_bul()
else: else:

View File

@ -867,15 +867,12 @@ class FormSemestre(db.Model):
.order_by(UniteEns.numero) .order_by(UniteEns.numero)
.all() .all()
) )
vals_annee = ( # issues de ce formsemestre seulement vals_annee = ( # issues de cette année scolaire seulement
ApcValidationAnnee.query.filter_by( ApcValidationAnnee.query.filter_by(
etudid=etudid, etudid=etudid,
annee_scolaire=self.annee_scolaire(), annee_scolaire=self.annee_scolaire(),
) referentiel_competence_id=self.formation.referentiel_competence_id,
.join(ApcValidationAnnee.formsemestre) ).all()
.join(FormSemestre.formation)
.filter(Formation.formation_code == self.formation.formation_code)
.all()
) )
H = [] H = []
for vals in (vals_sem, vals_ues, vals_rcues, vals_annee): for vals in (vals_sem, vals_ues, vals_rcues, vals_annee):

View File

@ -491,15 +491,11 @@ class ApoEtud(dict):
# ne trouve pas de semestre impair # ne trouve pas de semestre impair
self.validation_annee_but = None self.validation_annee_but = None
return return
self.validation_annee_but: ApcValidationAnnee = ( self.validation_annee_but: ApcValidationAnnee = ApcValidationAnnee.query.filter_by(
ApcValidationAnnee.query.filter_by(
formsemestre_id=formsemestre.id, formsemestre_id=formsemestre.id,
etudid=self.etud["etudid"], etudid=self.etud["etudid"],
formation_id=self.cur_sem[ referentiel_competence_id=self.cur_res.formsemestre.formation.referentiel_competence_id,
"formation_id"
], # XXX utiliser formation_code
).first() ).first()
)
self.is_nar = ( self.is_nar = (
self.validation_annee_but and self.validation_annee_but.code == NAR self.validation_annee_but and self.validation_annee_but.code == NAR
) )

View File

@ -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))