############################################################################## # ScoDoc # Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## """Jury BUT: logique de gestion """ from operator import attrgetter from app.comp.res_but import ResultatsSemestreBUT from app.comp import res_sem from app.models import but_validations from app.models.but_refcomp import ( ApcAnneeParcours, ApcCompetence, ApcNiveau, ApcParcoursNiveauCompetence, ) from app.models.but_validations import ApcValidationAnnee, ApcValidationRCUE from app.models.etudiants import Identite from app.models.formations import Formation from app.models.formsemestre import FormSemestre from app.models.ues import UniteEns from app.scodoc import sco_codes_parcours as codes from app.scodoc import sco_utils as scu from app.scodoc.sco_exceptions import ScoException class RegroupementCoherentUE: def __init__( self, etud: Identite, formsemestre_1: FormSemestre, ue_1: UniteEns, formsemestre_2: FormSemestre, ue_2: UniteEns, ): self.formsemestre_1 = formsemestre_1 self.ue_1 = ue_1 self.formsemestre_2 = formsemestre_2 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] else: self.moy_ue_1 = None 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_1.id][etud.id] else: self.moy_ue_2 = None # Calcul de la moyenne au RCUE if (self.moy_ue_1 is not None) and (self.moy_ue_2 is not None): # Moyenne RCUE non pondérée (pour le moment) self.moy_rcue = (self.moy_ue_1 + self.moy_ue_2) / 2 else: self.moy_rcue = None class DecisionsProposees: # Codes toujours proposés sauf si include_communs est faux: codes_communs = [codes.RAT, codes.DEF, codes.ABAN, codes.DEM, codes.UEBSL] def __init__(self, code: str = None, explanation="", include_communs=True): if include_communs: self.codes = self.codes_communs else: self.codes = [] if isinstance(code, list): self.codes = code + self.codes_communs elif code is not None: self.codes = [code] + self.codes_communs self.explanation = explanation def __repr__(self) -> str: return f"""<{self.__class__.__name__} codes={self.codes} explanation={self.explanation}""" def decisions_ue_proposees( etud: Identite, formsemestre: FormSemestre, ue: UniteEns ) -> DecisionsProposees: """Liste des codes de décisions que l'on peut proposer pour cette UE de cet étudiant dans ce semestre. si DEF ou DEM ou ABAN ou ABL sur année BUT: seulement DEF, DEM, ABAN, ABL si moy_ue > 10, ADM sinon si compensation dans RCUE: CMP sinon: ADJ, AJ et proposer toujours: RAT, DEF, ABAN, DEM, UEBSL """ if ue.type == codes.UE_SPORT: return DecisionsProposees( explanation="UE bonus, pas de décision de jury", include_communs=False ) # Code sur année ? decision_annee = ApcValidationAnnee.query.filter_by( etudid=etud.id, annee_scolaire=formsemestre.annee_scolaire() ).first() if ( decision_annee is not None and decision_annee.code in codes.CODES_ANNEE_ARRET ): # DEF, DEM, ABAN, ABL return DecisionsProposees( code=decision_annee.code, explanation=f"l'année a le code {decision_annee.code}", include_communs=False, ) # Moyenne de l'UE ? res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre) if not ue.id in res.etud_moy_ue: return DecisionsProposees(explanation="UE sans résultat") if not etud.id in res.etud_moy_ue[ue.id]: return DecisionsProposees(explanation="Étudiant sans résultat dans cette UE") moy_ue = res.etud_moy_ue[ue.id][etud.id] if moy_ue > (codes.ParcoursBUT.BARRE_MOY - codes.NOTES_TOLERANCE): return DecisionsProposees( code=codes.ADM, explanation=f"Moyenne >= {codes.ParcoursBUT.BARRE_MOY}/20", ) # Compensation dans le RCUE ? other_ue, other_formsemestre = but_validations.get_other_ue_rcue(ue, etud.id) if other_ue is not None: # inscrit à une autre UE du même RCUE other_res: ResultatsSemestreBUT = res_sem.load_formsemestre_results( other_formsemestre ) if (other_ue.id in other_res.etud_moy_ue) and ( etud.id in other_res.etud_moy_ue[other_ue.id] ): other_moy_ue = other_res.etud_moy_ue[other_ue.id][etud.id] # Moyenne RCUE: non pondérée (pour le moment) moy_rcue = (moy_ue + other_moy_ue) / 2 if moy_rcue > codes.NOTES_BARRE_GEN_COMPENSATION: # 10-epsilon return DecisionsProposees( code=codes.CMP, explanation=f"Compensée par {other_ue} (moyenne RCUE={scu.fmt_note(moy_rcue)}/20", ) return DecisionsProposees( code=[codes.AJ, codes.ADJ], explanation="notes insuffisantes", ) def decisions_rcue_proposees( etud: Identite, formsemestre_1: FormSemestre, ue_1: UniteEns, formsemestre_2: FormSemestre, ue_2: UniteEns, ) -> DecisionsProposees: """Liste des codes de décisions que l'on peut proposer pour le RCUE de cet étudiant dans ces semestres. ADM, CMP, ADJ, AJ, RAT, DEF, ABAN La validation des deux UE du niveau d’une compétence emporte la validation de l’ensemble des UE du niveau inférieur de cette même compétence. """ # class BUTCursusEtud: """Validation du cursus d'un étudiant""" def __init__(self, formsemestre: FormSemestre, etud: Identite): if formsemestre.formation.referentiel_competence is None: raise ScoException("BUTCursusEtud: pas de référentiel de compétences") assert len(etud.formsemestre_inscriptions) > 0 self.formsemestre = formsemestre self.etud = etud # # La dernière inscription en date va donner le parcours (donc les compétences à valider) self.last_inscription = sorted( etud.formsemestre_inscriptions, key=attrgetter("formsemestre.date_debut") )[-1] def est_diplomable(self) -> bool: """Vrai si toutes les compétences sont validables""" return all( self.competence_validable(competence) for competence in self.competences_du_parcours() ) def est_diplome(self) -> bool: """Vrai si BUT déjà validé""" # vrai si la troisième année est validée # On cherche les validations de 3ieme annee (ordre=3) avec le même référentiel # de formation que nous. return ( ApcValidationAnnee.query.filter_by(etudid=self.etud.id, ordre=3) .join(FormSemestre, FormSemestre.id == ApcValidationAnnee.formsemestre_id) .join(Formation, FormSemestre.formation_id == Formation.id) .filter( Formation.referentiel_competence_id == self.formsemestre.formation.referentiel_competence_id ) .count() > 0 ) def competences_du_parcours(self) -> list[ApcCompetence]: """Construit liste des compétences du parcours, qui doivent être validées pour obtenir le diplôme. Le parcours est celui de la dernière inscription. """ parcour = self.last_inscription.parcour query = self.formsemestre.formation.formation.query_competences_parcour(parcour) if query is None: return [] return query.all() def competence_validee(self, competence: ApcCompetence) -> bool: """Vrai si la compétence est validée, c'est à dire que tous ses niveaux sont validés (ApcValidationRCUE). """ validations = ( ApcValidationRCUE.query.filter_by(etudid=self.etud.id) .join(UniteEns, UniteEns.id == ApcValidationRCUE.ue1_id) .join(ApcNiveau, ApcNiveau.id == UniteEns.niveau_competence_id) .join(ApcCompetence, ApcCompetence.id == ApcNiveau.competence_id) ) def competence_validable(self, competence: ApcCompetence): """Vrai si la compétence est "validable" automatiquement, c'est à dire que les conditions de notes sont satisfaites pour l'acquisition de son niveau le plus élevé, qu'il ne manque que l'enregistrement de la décision. En vertu de la règle "La validation des deux UE du niveau d’une compétence emporte la validation de l'ensemble des UE du niveau inférieur de cette même compétence.", il suffit de considérer le dernier niveau dans lequel l'étudiant est inscrit. """ pass def ues_emportees(self, niveau: ApcNiveau) -> list[tuple[FormSemestre, UniteEns]]: """La liste des UE à valider si on valide ce niveau. Ne liste que les UE qui ne sont pas déjà acquises. Selon la règle donéne par l'arrêté BUT: * La validation des deux UE du niveau d’une compétence emporte la validation de l'ensemble des UE du niveau inférieur de cette même compétence. """ pass