283 lines
10 KiB
Python
283 lines
10 KiB
Python
# -*- coding: UTF-8 -*
|
|
|
|
"""Décisions de jury (validations) des RCUE et années du BUT
|
|
"""
|
|
|
|
import flask_sqlalchemy
|
|
from sqlalchemy.sql import text
|
|
from typing import Union
|
|
|
|
from app import db
|
|
|
|
from app.models import CODE_STR_LEN
|
|
from app.models.but_refcomp import ApcNiveau
|
|
from app.models.etudiants import Identite
|
|
from app.models.ues import UniteEns
|
|
from app.models.formsemestre import FormSemestre
|
|
from app.scodoc import sco_codes_parcours as sco_codes
|
|
|
|
|
|
class ApcValidationRCUE(db.Model):
|
|
"""Validation des niveaux de compétences
|
|
|
|
aka "regroupements cohérents d'UE" dans le jargon BUT.
|
|
|
|
le formsemestre est celui du semestre PAIR du niveau de compétence
|
|
"""
|
|
|
|
__tablename__ = "apc_validation_rcue"
|
|
# Assure unicité de la décision:
|
|
__table_args__ = (
|
|
db.UniqueConstraint("etudid", "formsemestre_id", "ue1_id", "ue2_id"),
|
|
)
|
|
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
etudid = db.Column(
|
|
db.Integer,
|
|
db.ForeignKey("identite.id", ondelete="CASCADE"),
|
|
index=True,
|
|
nullable=False,
|
|
)
|
|
formsemestre_id = db.Column(
|
|
db.Integer, db.ForeignKey("notes_formsemestre.id"), index=True, nullable=True
|
|
)
|
|
# Les deux UE associées à ce niveau:
|
|
ue1_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id"), nullable=False)
|
|
ue2_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id"), nullable=False)
|
|
# optionnel, le parcours dans lequel se trouve la compétence:
|
|
parcours_id = db.Column(db.Integer, db.ForeignKey("apc_parcours.id"), nullable=True)
|
|
date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
|
code = db.Column(db.String(CODE_STR_LEN), nullable=False, index=True)
|
|
|
|
etud = db.relationship("Identite", backref="apc_validations_rcues")
|
|
formsemestre = db.relationship("FormSemestre", backref="apc_validations_rcues")
|
|
ue1 = db.relationship("UniteEns", foreign_keys=ue1_id)
|
|
ue2 = db.relationship("UniteEns", foreign_keys=ue2_id)
|
|
parcour = db.relationship("ApcParcours")
|
|
|
|
def __repr__(self):
|
|
return f"<{self.__class__.__name__} {self.id} {self.etud} {self.ue1}/{self.ue2}:{self.code!r}>"
|
|
|
|
def niveau(self) -> ApcNiveau:
|
|
"""Le niveau de compétence associé à cet RCUE."""
|
|
# Par convention, il est donné par la seconde UE
|
|
return self.ue2.niveau_competence
|
|
|
|
|
|
# Attention: ce n'est pas un modèle mais une classe ordinaire:
|
|
class RegroupementCoherentUE:
|
|
"""Le regroupement cohérent d'UE, dans la terminologie du BUT, est le couple d'UEs
|
|
de la même année (BUT1,2,3) liées au *même niveau de compétence*.
|
|
|
|
La moyenne (10/20) au RCU déclenche la compensation des UE.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
etud: Identite,
|
|
formsemestre_1: FormSemestre,
|
|
ue_1: UniteEns,
|
|
formsemestre_2: FormSemestre,
|
|
ue_2: UniteEns,
|
|
):
|
|
from app.comp import res_sem
|
|
from app.comp.res_but import ResultatsSemestreBUT
|
|
|
|
# Ordonne les UE dans le sens croissant (S1,S2) ou (S3,S4)...
|
|
if formsemestre_1.semestre_id > formsemestre_2.semestre_id:
|
|
(ue_1, formsemestre_1), (ue_2, formsemestre_2) = (
|
|
(
|
|
ue_2,
|
|
formsemestre_2,
|
|
),
|
|
(ue_1, formsemestre_1),
|
|
)
|
|
assert formsemestre_1.semestre_id % 2 == 1
|
|
assert formsemestre_2.semestre_id % 2 == 0
|
|
assert abs(formsemestre_1.semestre_id - formsemestre_2.semestre_id) == 1
|
|
assert ue_1.niveau_competence_id == ue_2.niveau_competence_id
|
|
self.etud = etud
|
|
self.formsemestre_1 = formsemestre_1
|
|
"semestre impair"
|
|
self.ue_1 = ue_1
|
|
self.formsemestre_2 = formsemestre_2
|
|
"semestre pair"
|
|
self.ue_2 = ue_2
|
|
# Stocke les moyennes d'UE
|
|
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre_1)
|
|
if ue_1.id in res.etud_moy_ue and etud.id in res.etud_moy_ue[ue_1.id]:
|
|
self.moy_ue_1 = res.etud_moy_ue[ue_1.id][etud.id]
|
|
self.moy_ue_1_val = self.moy_ue_1 # toujours float, peut être NaN
|
|
else:
|
|
self.moy_ue_1 = None
|
|
self.moy_ue_1_val = 0.0
|
|
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre_2)
|
|
if ue_2.id in res.etud_moy_ue and etud.id in res.etud_moy_ue[ue_2.id]:
|
|
self.moy_ue_2 = res.etud_moy_ue[ue_2.id][etud.id]
|
|
self.moy_ue_2_val = self.moy_ue_2
|
|
else:
|
|
self.moy_ue_2 = None
|
|
self.moy_ue_2_val = 0.0
|
|
# Calcul de la moyenne au RCUE
|
|
if (self.moy_ue_1 is not None) and (self.moy_ue_2 is not None):
|
|
# Moyenne RCUE (les pondérations par défaut sont 1.)
|
|
self.moy_rcue = (
|
|
self.moy_ue_1 * ue_1.coef_rcue + self.moy_ue_2 * ue_2.coef_rcue
|
|
) / (ue_1.coef_rcue + ue_2.coef_rcue)
|
|
else:
|
|
self.moy_rcue = None
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<{self.__class__.__name__} {self.ue_1.acronyme}({self.moy_ue_1}) {self.ue_2.acronyme}({self.moy_ue_2})>"
|
|
|
|
def query_validations(
|
|
self,
|
|
) -> flask_sqlalchemy.BaseQuery: # list[ApcValidationRCUE]
|
|
"""Les validations de jury enregistrées pour ce RCUE"""
|
|
niveau = self.ue_2.niveau_competence
|
|
|
|
return (
|
|
ApcValidationRCUE.query.filter_by(
|
|
etudid=self.etud.id,
|
|
)
|
|
.join(UniteEns, UniteEns.id == ApcValidationRCUE.ue2_id)
|
|
.join(ApcNiveau, UniteEns.niveau_competence_id == ApcNiveau.id)
|
|
.filter(ApcNiveau.id == niveau.id)
|
|
)
|
|
|
|
def other_ue(self, ue: UniteEns) -> UniteEns:
|
|
"""L'autre UE du regroupement. Si ue ne fait pas partie du regroupement, ValueError"""
|
|
if ue.id == self.ue_1.id:
|
|
return self.ue_2
|
|
elif ue.id == self.ue_2.id:
|
|
return self.ue_1
|
|
raise ValueError(f"ue {ue} hors RCUE {self}")
|
|
|
|
def est_enregistre(self) -> bool:
|
|
"""Vrai si ce RCUE, donc le niveau de compétences correspondant
|
|
a une décision jury enregistrée
|
|
"""
|
|
return self.query_validations().count() > 0
|
|
|
|
def est_compensable(self):
|
|
"""Vrai si ce RCUE est validable par compensation
|
|
c'est à dire que sa moyenne est > 10 avec une UE < 10
|
|
"""
|
|
return (
|
|
(self.moy_rcue is not None)
|
|
and (self.moy_rcue > sco_codes.BUT_BARRE_RCUE)
|
|
and (
|
|
(self.moy_ue_1_val < sco_codes.NOTES_BARRE_GEN)
|
|
or (self.moy_ue_2_val < sco_codes.NOTES_BARRE_GEN)
|
|
)
|
|
)
|
|
|
|
def est_suffisant(self) -> bool:
|
|
"""Vrai si ce RCUE est > 8"""
|
|
return (self.moy_rcue is not None) and (
|
|
self.moy_rcue > sco_codes.BUT_RCUE_SUFFISANT
|
|
)
|
|
|
|
def est_validable(self) -> bool:
|
|
"""Vrai si ce RCU satisfait les conditions pour être validé
|
|
Pour cela, il suffit que la moyenne des UE qui le constitue soit > 10
|
|
"""
|
|
return (self.moy_rcue is not None) and (
|
|
self.moy_rcue > sco_codes.BUT_BARRE_RCUE
|
|
)
|
|
|
|
def code_valide(self) -> Union[ApcValidationRCUE, None]:
|
|
"Si ce RCUE est ADM, CMP ou ADJ, la validation. Sinon, None"
|
|
validation = self.query_validations().first()
|
|
if (validation is not None) and (
|
|
validation.code in {sco_codes.ADM, sco_codes.ADJ, sco_codes.CMP}
|
|
):
|
|
return validation
|
|
return None
|
|
|
|
|
|
def find_rcues(
|
|
formsemestre: FormSemestre, ue: UniteEns, etud: Identite
|
|
) -> 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.query.get(formsemestre_id)
|
|
rcues.append(
|
|
RegroupementCoherentUE(etud, formsemestre, ue, other_formsemestre, other_ue)
|
|
)
|
|
# 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"),)
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
etudid = db.Column(
|
|
db.Integer,
|
|
db.ForeignKey("identite.id", ondelete="CASCADE"),
|
|
index=True,
|
|
nullable=False,
|
|
)
|
|
ordre = db.Column(db.Integer, nullable=False)
|
|
"numéro de l'année: 1, 2, 3"
|
|
formsemestre_id = db.Column(
|
|
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
|
|
date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
|
code = db.Column(db.String(CODE_STR_LEN), nullable=False, index=True)
|
|
|
|
etud = db.relationship("Identite", backref="apc_validations_annees")
|
|
formsemestre = db.relationship("FormSemestre", backref="apc_validations_annees")
|
|
|
|
def __repr__(self):
|
|
return f"<{self.__class__.__name__} {self.id} {self.etud} BUT{self.ordre}/{self.annee_scolaire}:{self.code!r}>"
|