############################################################################## # 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 les UEs paire/impaire 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 courante 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: # UE capitalisée self.moy_ue_1 = self.validation_ue_best_impair.moy_ue self.ue_1 = self.validation_ue_best_impair.ue if ( res_impair and self.validation_ue_best_impair and self.validation_ue_best_impair.ue ): self.ue_status_impair = res_impair.get_etud_ue_status( etud.id, self.validation_ue_best_impair.ue.id ) 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 # }