253 lines
9.9 KiB
Python
253 lines
9.9 KiB
Python
##############################################################################
|
|
# ScoDoc
|
|
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
|
# See LICENSE
|
|
##############################################################################
|
|
|
|
"""Jury BUT: un RCUE, ou Regroupe Cohérent d'UEs
|
|
"""
|
|
from flask_sqlalchemy.query import Query
|
|
|
|
from app.comp.res_but import ResultatsSemestreBUT
|
|
from app.models import (
|
|
ApcNiveau,
|
|
ApcValidationRCUE,
|
|
Identite,
|
|
ScolarFormSemestreValidation,
|
|
UniteEns,
|
|
)
|
|
from app.scodoc import codes_cursus
|
|
from app.scodoc.codes_cursus import BUT_CODES_ORDER
|
|
|
|
|
|
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 RCUE déclenche la compensation des UEs.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
etud: Identite,
|
|
niveau: ApcNiveau,
|
|
res_pair: ResultatsSemestreBUT,
|
|
res_impair: ResultatsSemestreBUT,
|
|
semestre_id_impair: int,
|
|
cur_ues_pair: list[UniteEns],
|
|
cur_ues_impair: list[UniteEns],
|
|
):
|
|
"""
|
|
res_pair, res_impair: résultats des formsemestre de l'année en cours, ou None
|
|
cur_ues_pair, cur_ues_impair: ues auxquelles l'étudiant est inscrit cette année
|
|
"""
|
|
self.semestre_id_impair = semestre_id_impair
|
|
self.semestre_id_pair = semestre_id_impair + 1
|
|
self.etud: Identite = etud
|
|
self.niveau: ApcNiveau = niveau
|
|
"Le niveau de compétences de ce RCUE"
|
|
# Chercher l'UE en cours pour pair, impair
|
|
# une UE à laquelle l'étudiant est inscrit (non dispensé)
|
|
# dans l'un des formsemestre en cours
|
|
ues = [ue for ue in cur_ues_pair if ue.niveau_competence_id == niveau.id]
|
|
self.ue_cur_pair = ues[0] if ues else None
|
|
"UE paire en cours"
|
|
ues = [ue for ue in cur_ues_impair if ue.niveau_competence_id == niveau.id]
|
|
self.ue_cur_impair = ues[0] if ues else None
|
|
"UE impaire en cours"
|
|
|
|
self.validation_ue_cur_pair = (
|
|
ScolarFormSemestreValidation.query.filter_by(
|
|
etudid=etud.id,
|
|
formsemestre_id=res_pair.formsemestre.id,
|
|
ue_id=self.ue_cur_pair.id,
|
|
).first()
|
|
if self.ue_cur_pair
|
|
else None
|
|
)
|
|
self.validation_ue_cur_impair = (
|
|
ScolarFormSemestreValidation.query.filter_by(
|
|
etudid=etud.id,
|
|
formsemestre_id=res_impair.formsemestre.id,
|
|
ue_id=self.ue_cur_impair.id,
|
|
).first()
|
|
if self.ue_cur_impair
|
|
else None
|
|
)
|
|
|
|
# Autres validations pour l'UE paire
|
|
self.validation_ue_best_pair = best_autre_ue_validation(
|
|
etud.id,
|
|
niveau.id,
|
|
semestre_id_impair + 1,
|
|
res_pair.formsemestre.id if (res_pair and self.ue_cur_pair) else None,
|
|
)
|
|
self.validation_ue_best_impair = best_autre_ue_validation(
|
|
etud.id,
|
|
niveau.id,
|
|
semestre_id_impair,
|
|
res_impair.formsemestre.id if (res_impair and self.ue_cur_impair) else None,
|
|
)
|
|
|
|
# Suis-je complet ? (= en cours ou validé sur les deux moitiés)
|
|
self.complete = (self.ue_cur_pair or self.validation_ue_best_pair) and (
|
|
self.ue_cur_impair or self.validation_ue_best_impair
|
|
)
|
|
if not self.complete:
|
|
self.moy_rcue = None
|
|
|
|
# Stocke les moyennes d'UE
|
|
self.res_impair = None
|
|
"résultats formsemestre de l'UE si elle est courante, None sinon"
|
|
self.ue_status_impair = None
|
|
if self.ue_cur_impair:
|
|
ue_status = res_impair.get_etud_ue_status(etud.id, self.ue_cur_impair.id)
|
|
self.moy_ue_1 = ue_status["moy"] if ue_status else None # avec capitalisée
|
|
self.ue_1 = self.ue_cur_impair
|
|
self.res_impair = res_impair
|
|
self.ue_status_impair = ue_status
|
|
elif self.validation_ue_best_impair:
|
|
self.moy_ue_1 = self.validation_ue_best_impair.moy_ue
|
|
self.ue_1 = self.validation_ue_best_impair.ue
|
|
else:
|
|
self.moy_ue_1, self.ue_1 = None, None
|
|
self.moy_ue_1_val = self.moy_ue_1 if self.moy_ue_1 is not None else 0.0
|
|
|
|
self.res_pair = None
|
|
"résultats formsemestre de l'UE si elle est courante, None sinon"
|
|
self.ue_status_pair = None
|
|
if self.ue_cur_pair:
|
|
ue_status = res_pair.get_etud_ue_status(etud.id, self.ue_cur_pair.id)
|
|
self.moy_ue_2 = ue_status["moy"] if ue_status else None # avec capitalisée
|
|
self.ue_2 = self.ue_cur_pair
|
|
self.res_pair = res_pair
|
|
self.ue_status_pair = ue_status
|
|
elif self.validation_ue_best_pair:
|
|
self.moy_ue_2 = self.validation_ue_best_pair.moy_ue
|
|
self.ue_2 = self.validation_ue_best_pair.ue
|
|
else:
|
|
self.moy_ue_2, self.ue_2 = None, None
|
|
self.moy_ue_2_val = self.moy_ue_2 if self.moy_ue_2 is not None else 0.0
|
|
|
|
# Calcul de la moyenne au RCUE (utilise les moy d'UE capitalisées ou antérieures)
|
|
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 * self.ue_1.coef_rcue
|
|
+ self.moy_ue_2 * self.ue_2.coef_rcue
|
|
) / (self.ue_1.coef_rcue + self.ue_2.coef_rcue)
|
|
else:
|
|
self.moy_rcue = None
|
|
|
|
def __repr__(self) -> str:
|
|
return f"""<{self.__class__.__name__} {
|
|
self.ue_1.acronyme if self.ue_1 else "?"}({self.moy_ue_1}) {
|
|
self.ue_2.acronyme if self.ue_2 else "?"}({self.moy_ue_2})>"""
|
|
|
|
def __str__(self) -> str:
|
|
return f"""RCUE {
|
|
self.ue_1.acronyme if self.ue_1 else "?"}({self.moy_ue_1}) + {
|
|
self.ue_2.acronyme if self.ue_2 else "?"}({self.moy_ue_2})"""
|
|
|
|
def query_validations(
|
|
self,
|
|
) -> Query: # list[ApcValidationRCUE]
|
|
"""Les validations de jury enregistrées pour ce RCUE"""
|
|
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 == self.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 (uniquement) par compensation
|
|
c'est à dire que sa moyenne est > 10 avec une UE < 10.
|
|
Note: si ADM, est_compensable est faux.
|
|
"""
|
|
return (
|
|
(self.moy_rcue is not None)
|
|
and (self.moy_rcue > codes_cursus.BUT_BARRE_RCUE)
|
|
and (
|
|
(self.moy_ue_1_val < codes_cursus.NOTES_BARRE_GEN)
|
|
or (self.moy_ue_2_val < codes_cursus.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 > codes_cursus.BUT_RCUE_SUFFISANT
|
|
)
|
|
|
|
def est_validable(self) -> bool:
|
|
"""Vrai si ce RCUE satisfait les conditions pour être validé,
|
|
c'est à dire que la moyenne des UE qui le constituent soit > 10
|
|
"""
|
|
return (self.moy_rcue is not None) and (
|
|
self.moy_rcue > codes_cursus.BUT_BARRE_RCUE
|
|
)
|
|
|
|
def code_valide(self) -> 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 codes_cursus.CODES_RCUE_VALIDES
|
|
):
|
|
return validation
|
|
return None
|
|
|
|
|
|
def best_autre_ue_validation(
|
|
etudid: int, niveau_id: int, semestre_id: int, formsemestre_id: int
|
|
) -> ScolarFormSemestreValidation:
|
|
"""La "meilleure" validation validante d'UE pour ce niveau/semestre"""
|
|
validations = (
|
|
ScolarFormSemestreValidation.query.filter_by(etudid=etudid)
|
|
.join(UniteEns)
|
|
.filter_by(semestre_idx=semestre_id)
|
|
.join(ApcNiveau)
|
|
.filter(ApcNiveau.id == niveau_id)
|
|
)
|
|
validations = [v for v in validations if codes_cursus.code_ue_validant(v.code)]
|
|
# Elimine l'UE en cours si elle existe
|
|
if formsemestre_id is not None:
|
|
validations = [v for v in validations if v.formsemestre_id != formsemestre_id]
|
|
validations = sorted(validations, key=lambda v: BUT_CODES_ORDER.get(v.code, 0))
|
|
return validations[-1] if validations else None
|
|
|
|
|
|
# def compute_ues_by_niveau(
|
|
# niveaux: list[ApcNiveau],
|
|
# ) -> dict[int, tuple[list[UniteEns], list[UniteEns]]]:
|
|
# """UEs à valider cette année pour cet étudiant, selon son parcours.
|
|
# Considérer les UEs associées aux niveaux et non celles des formsemestres
|
|
# en cours. Notez que même si l'étudiant n'est pas inscrit ("dispensé") à une UE
|
|
# dans le formsemestre origine, elle doit apparaitre sur la page jury.
|
|
# Return: { niveau_id : ( [ues impair], [ues pair]) }
|
|
# """
|
|
# # Les UEs associées à ce niveau, toutes formations confondues
|
|
# return {
|
|
# niveau.id: (
|
|
# [ue for ue in niveau.ues if ue.semestre_idx % 2],
|
|
# [ue for ue in niveau.ues if not (ue.semestre_idx % 2)],
|
|
# )
|
|
# for niveau in niveaux
|
|
# }
|