From 015ec66aec9fa21c58e0c7cbf0b38c722b8681f0 Mon Sep 17 00:00:00 2001 From: IDK Date: Tue, 27 Jun 2023 23:22:32 +0200 Subject: [PATCH] Nouvelle gestion RCUE --- app/but/rcue.py | 243 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 243 insertions(+) create mode 100644 app/but/rcue.py diff --git a/app/but/rcue.py b/app/but/rcue.py new file mode 100644 index 000000000..26763468c --- /dev/null +++ b/app/but/rcue.py @@ -0,0 +1,243 @@ +############################################################################## +# ScoDoc +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. +# See LICENSE +############################################################################## + +"""Jury BUT: un RCUE, ou Regroupe Cohérent d'UEs +""" +from typing import Union +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.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, + ).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, + ).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, + self.ue_cur_pair.id if self.ue_cur_pair else None, + ) + self.validation_ue_best_impair = _best_autre_ue_validation( + etud.id, + niveau.id, + semestre_id_impair, + self.ue_cur_impair.id if self.ue_cur_impair else None, + ) + + # Suis-je complet ? + 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 + 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 + elif self.validation_ue_best_impair: + self.moy_ue_1 = self.validation_ue_best_pair.moy_ue + self.ue_1 = self.ue_cur_impair + 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 + + 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 + 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""" + 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 (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) -> 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 codes_cursus.CODES_RCUE_VALIDES + ): + return validation + return None + + +def _best_autre_ue_validation( + etudid: int, niveau_id: int, semestre_id: int, ue_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 + 1) + .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 ue_id is not None: + validations = [v for v in validations if v.ue_id != ue_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 +# }