diff --git a/app/but/jury_but.py b/app/but/jury_but.py index 4fd878337..74fd9687c 100644 --- a/app/but/jury_but.py +++ b/app/but/jury_but.py @@ -117,15 +117,15 @@ class DecisionsProposees: self.codes = [] "Les codes attribuables par ce jury" if include_communs: - self.codes = self.codes_communs + self.codes = self.codes_communs.copy() if isinstance(code, list): - self.codes = code + self.codes_communs + self.codes = code + self.codes elif code is not None: - self.codes = [code] + self.codes_communs + self.codes = [code] + self.codes self.code_valide: str = code_valide "La décision actuelle enregistrée" self.explanation: str = explanation - "Explication en à afficher à côté de la décision" + "Explication à afficher à côté de la décision" def __repr__(self) -> str: return f"""<{self.__class__.__name__} valid={self.code_valide @@ -193,48 +193,72 @@ class DecisionsProposeesAnnee(DecisionsProposees): self.parcour = None "Le parcours considéré (celui du semestre pair, ou à défaut impair)" self.ues_impair, self.ues_pair = self.compute_ues_annee() # pylint: disable=all + self.decisions_ues = { + ue.id: DecisionsProposeesUE(etud, formsemestre_impair, ue) + for ue in self.ues_impair + } + "{ue_id : DecisionsProposeesUE} pour toutes les UE de l'année" + self.decisions_ues.update( + { + ue.id: DecisionsProposeesUE(etud, formsemestre_pair, ue) + for ue in self.ues_pair + } + ) assert self.parcour is not None self.rcues_annee = self.compute_rcues_annee() "RCUEs de l'année" - self.nb_competences = len( - ApcNiveau.niveaux_annee_de_parcours(self.parcour, self.annee_but).all() - ) # note that .count() won't give the same res + self.niveaux_competences = ApcNiveau.niveaux_annee_de_parcours( + self.parcour, self.annee_but + ).all() # XXX à trier, selon l'ordre des UE associées ? + "liste des niveaux de compétences associés à cette année" + self.decisions_rcue_by_niveau = self.compute_decisions_niveaux() + "les décisions rcue associées aux niveau_id" + self.nb_competences = len(self.niveaux_competences) + "le nombre de niveaux de compétences à valider cette année" self.nb_validables = len( [rcue for rcue in self.rcues_annee if rcue.est_validable()] ) + "le nombre de comp. validables (éventuellement par compensation)" self.nb_rcues_under_8 = len( [rcue for rcue in self.rcues_annee if not rcue.est_suffisant()] ) + "le nb de comp. sous la barre de 8/20" # année ADM si toutes RCUE validées (sinon PASD) admis = self.nb_validables == self.nb_competences - valide_moitie_rcue = self.nb_validables > self.nb_competences // 2 + self.valide_moitie_rcue = self.nb_validables > (self.nb_competences // 2) # Peut passer si plus de la moitié validables et tous > 8 - passage_de_droit = valide_moitie_rcue and (self.nb_rcues_under_8 == 0) + self.passage_de_droit = self.valide_moitie_rcue and (self.nb_rcues_under_8 == 0) # XXX TODO ajouter condition pour passage en S5 + # Enfin calcule les codes des UE: + for dec_ue in self.decisions_ues.values(): + dec_ue.compute_codes() + # Reste à attribuer ADM, ADJ, PASD, PAS1NCI, RED, NAR - expl_rcues = f"{self.nb_validables} validables sur {self.nb_competences}" + expl_rcues = ( + f"{self.nb_validables} niveau validable(s) sur {self.nb_competences}" + ) if admis: self.codes = [sco_codes.ADM] + self.codes self.explanation = expl_rcues - elif passage_de_droit: + elif self.passage_de_droit: self.codes = [sco_codes.PASD, sco_codes.ADJ] + self.codes self.explanation = expl_rcues - elif valide_moitie_rcue: # mais au moins 1 rcue insuffisante + elif self.valide_moitie_rcue: # mais au moins 1 rcue insuffisante self.codes = [sco_codes.PAS1NCI, sco_codes.ADJ] + self.codes self.explanation = expl_rcues + f" et {self.nb_rcues_under_8} < 8" else: self.codes = [sco_codes.RED, sco_codes.NAR, sco_codes.ADJ] + self.codes - self.explanation = expl_rcues + f" et {self.nb_rcues_under_8} < 8" + self.explanation = expl_rcues + f" et {self.nb_rcues_under_8} niveau < 8" # def infos(self) -> str: "informations, for debugging purpose" return f"""DecisionsProposeesAnnee etud: {self.etud} - formsemestre_pair: {self.formsemestre_pair} formsemestre_impair: {self.formsemestre_impair} + formsemestre_pair: {self.formsemestre_pair} RCUEs: {self.rcues_annee} nb_competences: {self.nb_competences} nb_nb_validables: {self.nb_validables} @@ -286,7 +310,7 @@ class DecisionsProposeesAnnee(DecisionsProposees): # Parcour dans lequel l'étudiant est inscrit, et liste des UEs if res.etuds_parcour_id[etudid] is None: # pas de parcour: prend toutes les UEs (non bonus) - ues = res.etud_ues(etudid) + ues = list(res.etud_ues(etudid)) else: parcour = ApcParcours.query.get(res.etuds_parcour_id[etudid]) if parcour is not None: @@ -343,10 +367,34 @@ class DecisionsProposeesAnnee(DecisionsProposees): raise ScoValueError(f"pas de RCUE pour l'UE {ue_pair.acronyme}") rcues_annee.append(rcue) if len(ues_impair_sans_rcue) > 0: - ue = ues_impair_sans_rcue.pop() + ue = UniteEns.query.get(ues_impair_sans_rcue.pop()) raise ScoValueError(f"pas de RCUE pour l'UE {ue.acronyme}") return rcues_annee + def compute_decisions_niveaux(self) -> dict[int, "DecisionsProposeesRCUE"]: + """Pour chaque niveau de compétence de cette année, donne le DecisionsProposeesRCUE + ou None s'il n'y en a pas (ne devrait pas arriver car + compute_rcues_annee vérifie déjà cela). + Return: { niveau_id : DecisionsProposeesRCUE } + """ + # Retrouve le RCUE associé à chaque niveau + rc_niveaux = [] + for niveau in self.niveaux_competences: + rcue = None + for rc in self.rcues_annee: + if rc.ue_1.niveau_competence_id == niveau.id: + rcue = rc + break + dec_rcue = DecisionsProposeesRCUE(self, rcue) + rc_niveaux.append((dec_rcue, niveau.id)) + # prévient les UE concernées :-) + self.decisions_ues[dec_rcue.rcue.ue_1.id].set_rcue(dec_rcue.rcue) + self.decisions_ues[dec_rcue.rcue.ue_2.id].set_rcue(dec_rcue.rcue) + # Ordonne par numéro d'UE + rc_niveaux.sort(key=lambda x: x[0].rcue.ue_1.numero) + decisions_rcue_by_niveau = {x[1]: x[0] for x in rc_niveaux} + return decisions_rcue_by_niveau + class DecisionsProposeesRCUE(DecisionsProposees): """Liste des codes de décisions que l'on peut proposer pour @@ -371,7 +419,7 @@ class DecisionsProposeesRCUE(DecisionsProposees): validation = rcue.query_validations().first() if validation is not None: self.code_valide = validation.code - if rcue.est_compense(): + if rcue.est_compensable(): self.codes.insert(0, sco_codes.CMP) elif rcue.est_validable(): self.codes.insert(0, sco_codes.ADM) @@ -412,6 +460,7 @@ class DecisionsProposeesUE(DecisionsProposees): super().__init__(etud=etud) self.ue: UniteEns = ue self.rcue: RegroupementCoherentUE = None + "Le rcu auquel est rattaché cette UE, ou None" # Une UE peut être validée plusieurs fois en cas de redoublement (qu'elle soit capitalisée ou non) # mais ici on a restreint au formsemestre donc une seule (prend la première) self.validation = ScolarFormSemestreValidation.query.filter_by( @@ -423,17 +472,7 @@ class DecisionsProposeesUE(DecisionsProposees): self.explanation = "UE bonus, pas de décision de jury" self.codes = [] # aucun code proposé return - # 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 sco_codes.CODES_ANNEE_ARRET - ): # DEF, DEM, ABAN, ABL - self.explanation = f"l'année a le code {decision_annee.code}" - self.codes = [decision_annee.code] # sans les codes communs - return + # Moyenne de l'UE ? res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre) if not ue.id in res.etud_moy_ue: @@ -442,23 +481,25 @@ class DecisionsProposeesUE(DecisionsProposees): if not etud.id in res.etud_moy_ue[ue.id]: self.explanation = "Étudiant sans résultat dans cette UE" return - moy_ue = res.etud_moy_ue[ue.id][etud.id] - if moy_ue > (sco_codes.ParcoursBUT.BARRE_MOY - sco_codes.NOTES_TOLERANCE): + self.moy_ue = res.etud_moy_ue[ue.id][etud.id] + + def set_rcue(self, rcue: RegroupementCoherentUE): + """Rattache cette UE à un RCUE. Cela peut modifier les codes + proposés (si compensation)""" + self.rcue = rcue + + def compute_codes(self): + """Calcul des .codes attribuables et de l'explanation associée""" + if self.moy_ue > (sco_codes.ParcoursBUT.BARRE_MOY - sco_codes.NOTES_TOLERANCE): self.codes.insert(0, sco_codes.ADM) self.explanation = (f"Moyenne >= {sco_codes.ParcoursBUT.BARRE_MOY}/20",) - - # Compensation dans un RCUE ? - rcues = but_validations.find_rcues(formsemestre, ue, etud) - for rcue in rcues: - if rcue.est_validable(): - self.codes.insert(0, sco_codes.CMP) - self.explanation = f"Compensée par {rcue.other_ue(ue)} (moyenne RCUE={scu.fmt_note(rcue.moy_rcue)}/20" - self.rcue = rcue - return # s'arrête au 1er RCU validable - - # Échec à valider cette UE - self.codes = [sco_codes.AJ, sco_codes.ADJ] + self.codes - self.explanation = "notes insuffisantes" + elif self.rcue and self.rcue.est_compensable(): + self.codes.insert(0, sco_codes.CMP) + self.explanation = "compensable dans le RCUE" + else: + # Échec à valider cette UE + self.codes = [sco_codes.AJ, sco_codes.ADJ] + self.codes + self.explanation = "notes insuffisantes" class BUTCursusEtud: # WIP TODO diff --git a/app/models/but_validations.py b/app/models/but_validations.py index 294d61a0c..51d172872 100644 --- a/app/models/but_validations.py +++ b/app/models/but_validations.py @@ -67,7 +67,7 @@ class ApcValidationRCUE(db.Model): # 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. + 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. """ @@ -139,7 +139,7 @@ class RegroupementCoherentUE: etudid=self.etud.id, ) .join(UniteEns, UniteEns.id == ApcValidationRCUE.ue2_id) - .join(ApcNiveau, UniteEns.niveau_id == ApcNiveau.id) + .join(ApcNiveau, UniteEns.niveau_competence_id == ApcNiveau.id) .filter(ApcNiveau.id == niveau.id) ) @@ -157,7 +157,7 @@ class RegroupementCoherentUE: """ return self.query_validations().count() > 0 - def est_compense(self): + def est_compensable(self): """Vrai si ce RCUE est validable par compensation c'est à dire que sa moyenne est > 10 avec une UE < 10 """