From 5d30b9233ba4b1229425f227e487516132cad726 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 28 Jun 2023 21:25:38 +0200 Subject: [PATCH] WIP: nouvelles gestion jury BUT. --- app/but/cursus_but.py | 1 - app/but/jury_but.py | 897 ++++++++---------- app/but/jury_but_pv.py | 2 +- app/but/jury_but_results.py | 6 +- app/but/jury_but_view.py | 39 +- app/models/but_validations.py | 140 +-- app/models/events.py | 7 +- app/models/validations.py | 2 +- app/scodoc/sco_formsemestre_edit.py | 8 +- app/scodoc/sco_utils.py | 2 +- app/static/js/scodoc.js | 491 +++++----- app/tables/jury_recap.py | 13 +- app/templates/but/documentation_codes_jury.j2 | 12 + app/templates/pn/ue_infos.j2 | 1 + app/views/notes.py | 48 +- sco_version.py | 2 +- 16 files changed, 749 insertions(+), 922 deletions(-) diff --git a/app/but/cursus_but.py b/app/but/cursus_but.py index 5b5b6f29c2..5eda000dca 100644 --- a/app/but/cursus_but.py +++ b/app/but/cursus_but.py @@ -36,7 +36,6 @@ from app.models import Scolog, ScolarAutorisationInscription from app.models.but_validations import ( ApcValidationAnnee, ApcValidationRCUE, - RegroupementCoherentUE, ) from app.models.etudiants import Identite from app.models.formations import Formation diff --git a/app/but/jury_but.py b/app/but/jury_but.py index bb73af4311..d4e7dd8cf2 100644 --- a/app/but/jury_but.py +++ b/app/but/jury_but.py @@ -25,8 +25,8 @@ Utilisation: - on enregistre la décision (dans ScolarFormSemestreValidation pour les UE, ApcValidationRCUE pour les RCUE, et ApcValidationAnnee pour les années) - Si RCUE validé, on déclenche d'éventuelles validations: - ("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.") + ("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.") Les jurys de semestre BUT impairs entrainent systématiquement la génération d'une autorisation d'inscription dans le semestre pair suivant: `ScolarAutorisationInscription`. @@ -60,7 +60,6 @@ DecisionsProposeesUE: décisions de jury sur une UE du BUT """ from datetime import datetime import html -from operator import attrgetter import re from typing import Union @@ -71,6 +70,7 @@ from app import db from app import log from app.but import cursus_but from app.but.cursus_but import EtudCursusBUT +from app.but.rcue import RegroupementCoherentUE from app.comp.res_but import ResultatsSemestreBUT from app.comp import res_sem @@ -83,7 +83,6 @@ from app.models import Scolog, ScolarAutorisationInscription from app.models.but_validations import ( ApcValidationAnnee, ApcValidationRCUE, - RegroupementCoherentUE, ) from app.models.etudiants import Identite from app.models.formations import Formation @@ -186,10 +185,10 @@ class DecisionsProposeesAnnee(DecisionsProposees): """Décisions de jury sur une année (ETP) du BUT Le texte: - La poursuite d'études dans un semestre pair d’une même année est de droit - pour tout étudiant. La poursuite d’études dans un semestre impair est - possible si et seulement si l’étudiant a obtenu : - - la moyenne à plus de la moitié des regroupements cohérents d’UE; + La poursuite d'études dans un semestre pair d'une même année est de droit + pour tout étudiant. La poursuite d'études dans un semestre impair est + possible si et seulement si l'étudiant a obtenu : + - la moyenne à plus de la moitié des regroupements cohérents d'UE; - et une moyenne égale ou supérieure à 8 sur 20 à chaque RCUE. La poursuite d'études dans le semestre 5 nécessite de plus la validation de toutes les UE des semestres 1 et 2 dans les conditions de validation @@ -220,129 +219,102 @@ class DecisionsProposeesAnnee(DecisionsProposees): raise ScoNoReferentielCompetences(formation=formsemestre.formation) super().__init__(etud=etud) self.formsemestre = formsemestre - "le formsemestre utilisé pour construire ce deca" - self.formsemestre_id = formsemestre.id - "l'id du formsemestre utilisé pour construire ce deca" - formsemestre_impair, formsemestre_pair = self.comp_formsemestres(formsemestre) - assert ( - (formsemestre_pair is None) - or (formsemestre_impair is None) - or ( - ((formsemestre_pair.semestre_id - formsemestre_impair.semestre_id) == 1) - and ( - formsemestre_pair.formation.referentiel_competence_id - == formsemestre_impair.formation.referentiel_competence_id - ) - ) - ) - # Si les années scolaires sont distinctes, on est "à cheval" - self.a_cheval = ( - formsemestre_impair - and formsemestre_pair - and formsemestre_impair.annee_scolaire() - != formsemestre_pair.annee_scolaire() - ) - "vrai si on groupe deux semestres d'années scolaires différentes" + "le formsemestre d'origine, utilisé pour construire ce deca" # Si on part d'un semestre IMPAIR, il n'y aura pas de décision année proposée # (mais on pourra évidemment valider des UE et même des RCUE) self.jury_annuel: bool = formsemestre.semestre_id in (2, 4, 6) "vrai si jury de fin d'année scolaire (sem. pair, propose code annuel)" - - self.formsemestre_impair = formsemestre_impair - "le 1er semestre du groupement (S1, S3, S5)" - self.formsemestre_pair = formsemestre_pair - "le second formsemestre (S2, S4, S6), de la même année scolaire ou d'une précédente" - formsemestre_last = formsemestre_pair or formsemestre_impair - "le formsemestre le plus avancé (en indice de semestre) dans le groupement" - - self.annee_but = (formsemestre_last.semestre_id + 1) // 2 + self.annee_but = (formsemestre.semestre_id + 1) // 2 "le rang de l'année dans le BUT: 1, 2, 3" assert self.annee_but in (1, 2, 3) - self.autorisations_recorded = False - "vrai si on a enregistré l'autorisation de passage" - self.rcues_annee = [] - """RCUEs de l'année - (peuvent concerner l'année scolaire antérieur pour les redoublants - avec UE capitalisées) - """ - self.inscription_etat = etud.inscription_etat(formsemestre_last.id) - "état de l'inscription dans le semestre le plus avancé (pair si année complète)" - self.inscription_etat_pair = ( - etud.inscription_etat(formsemestre_pair.id) - if formsemestre_pair is not None - else None + # ---- inscription et parcours + inscription = formsemestre.etuds_inscriptions.get(etud.id) + if inscription is None: + raise ValueError("Etudiant non inscrit au semestre") + self.inscription_etat = inscription.etat + "état de l'inscription dans le semestre origine" + self.parcour = inscription.parcour + "Le parcours considéré, qui est celui de l'étudiant dans le formsemestre origine" + self.formsemestre_impair, self.formsemestre_pair = self.comp_formsemestres( + formsemestre ) - self.inscription_etat_impair = ( - etud.inscription_etat(formsemestre_impair.id) - if formsemestre_impair is not None - else None - ) - - if self.formsemestre_impair is not None: - self.validation = ( - ApcValidationAnnee.query.filter_by( - etudid=self.etud.id, - ordre=self.annee_but, - ) - .join(Formation) - .filter_by(formation_code=self.formsemestre.formation.formation_code) - .first() - ) - else: - self.validation = None - if self.validation is not None: - self.code_valide = self.validation.code - self.parcour = None - "Le parcours considéré (celui du semestre pair, ou à défaut impair)" - if self.formsemestre_pair is not None: - self.res_pair: ResultatsSemestreBUT = res_sem.load_formsemestre_results( - self.formsemestre_pair - ) - else: - self.res_pair = None - if self.formsemestre_impair is not None: - self.res_impair: ResultatsSemestreBUT = res_sem.load_formsemestre_results( - self.formsemestre_impair - ) - else: - self.res_impair = None - - self.ues_impair, self.ues_pair = self.compute_ues_annee() # pylint: disable=all - self.decisions_ues = { - ue.id: DecisionsProposeesUE( - etud, formsemestre_impair, ue, self.inscription_etat_impair - ) - 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, self.inscription_etat_pair - ) - for ue in self.ues_pair - } - ) - self.rcues_annee = self.compute_rcues_annee() - - formation = ( - self.formsemestre_impair.formation + # ---- résultats et UEs en cours cette année: + self.res_impair: ResultatsSemestreBUT = ( + res_sem.load_formsemestre_results(self.formsemestre_impair) if self.formsemestre_impair - else self.formsemestre_pair.formation + else None ) - ( - parcours, - niveaux_by_parcours, - ) = formation.referentiel_competence.get_niveaux_by_parcours( - self.annee_but, [self.parcour] if self.parcour else None + self.res_pair: ResultatsSemestreBUT = ( + res_sem.load_formsemestre_results(self.formsemestre_pair) + if self.formsemestre_pair + else None + ) + self.cur_ues_impair = ( + list_ue_parcour_etud( + self.formsemestre_impair, self.etud, self.parcour, self.res_impair + ) + if self.formsemestre_impair + else [] + ) + self.cur_ues_pair = ( + list_ue_parcour_etud( + self.formsemestre_pair, self.etud, self.parcour, self.res_pair + ) + if self.formsemestre_pair + else [] + ) + # ---- Niveaux et RCUEs + niveaux_by_parcours = ( + formsemestre.formation.referentiel_competence.get_niveaux_by_parcours( + self.annee_but, [self.parcour] if self.parcour else None + )[1] ) self.niveaux_competences = niveaux_by_parcours["TC"] + ( niveaux_by_parcours[self.parcour.id] if self.parcour else [] ) - """liste non triée des niveaux de compétences associés à cette année pour cet étudiant. + """Les niveaux à valider pour cet étudiant dans cette année, compte tenu de son parcours. + Liste non triée des niveaux de compétences associés à cette année pour cet étudiant. = niveaux du tronc commun + niveau du parcours de l'étudiant. """ - self.decisions_rcue_by_niveau = self.compute_decisions_niveaux() + self.rcue_by_niveau = self._compute_rcues_annee() + """RCUEs de l'année + (peuvent être construits avec des UEs validées antérieurement: redoublants + avec UEs capitalisées, validation "antérieures") + """ + # ---- Décision année et autorisation + self.autorisations_recorded = False + "vrai si on a enregistré l'autorisation de passage" + self.validation = ( + ApcValidationAnnee.query.filter_by( + etudid=self.etud.id, + ordre=self.annee_but, + ) + .join(Formation) + .filter_by(formation_code=self.formsemestre.formation.formation_code) + .first() + ) + "Validation actuellement enregistrée pour cette année BUT" + self.code_valide = self.validation.code if self.validation is not None else None + "Le code jury annuel enregistré, ou None" + + # ---- Décisions d'UEs + self.decisions_ues = { + rcue.ue_1.id: DecisionsProposeesUE( + etud, self.formsemestre_impair, rcue, False, self.inscription_etat + ) + for rcue in self.rcue_by_niveau.values() + if rcue.ue_1 + } + self.decisions_ues.update( + { + rcue.ue_2.id: DecisionsProposeesUE( + etud, self.formsemestre_pair, rcue, True, self.inscription_etat + ) + for rcue in self.rcue_by_niveau.values() + if rcue.ue_2 + } + ) + self.decisions_rcue_by_niveau = self._compute_decisions_niveaux() "les décisions rcue associées aux niveau_id" self.dec_rcue_by_ue = self._dec_rcue_by_ue() "{ ue_id : DecisionsProposeesRCUE } pour toutes les UE associées à un niveau" @@ -371,12 +343,12 @@ class DecisionsProposeesAnnee(DecisionsProposees): self.passage_de_droit = self.valide_moitie_rcue and (self.nb_rcues_under_8 == 0) "Vrai si peut passer dans l'année BUT suivante: plus de la moitié validables et tous > 8" explanation = "" - # Cas particulier du passage en BUT 3: nécessité d’avoir validé toutes les UEs du BUT 1. + # Cas particulier du passage en BUT 3: nécessité d'avoir validé toutes les UEs du BUT 1. if self.passage_de_droit and self.annee_but == 2: inscription = formsemestre.etuds_inscriptions.get(etud.id) if inscription: ues_but1_non_validees = cursus_but.etud_ues_de_but1_non_validees( - etud, formation, inscription.parcour + etud, self.formsemestre.formation, self.parcour ) self.passage_de_droit = not ues_but1_non_validees explanation += ( @@ -434,7 +406,8 @@ class DecisionsProposeesAnnee(DecisionsProposees): sco_codes.ADJ, sco_codes.PASD, # voir #488 (discutable, conventions locales) ] + self.codes - explanation += f""" et {self.nb_rcues_under_8} niveau{'x' if self.nb_rcues_under_8 > 1 else ''} < 8""" + explanation += f""" et {self.nb_rcues_under_8 + } niveau{'x' if self.nb_rcues_under_8 > 1 else ''} < 8""" # Si l'un des semestres est extérieur, propose ADM if ( @@ -487,7 +460,7 @@ class DecisionsProposeesAnnee(DecisionsProposees): text += "" text += f""" -
  • RCUEs: {html.escape(str(self.rcues_annee))}
  • +
  • RCUEs: {html.escape(str(self.rcue_by_niveau))}
  • nb_competences: {getattr(self, "nb_competences", "-")}
  • nb_validables: {getattr(self, "nb_validables", "-")}
  • codes: {self.codes}
  • @@ -509,177 +482,107 @@ class DecisionsProposeesAnnee(DecisionsProposees): def comp_formsemestres( self, formsemestre: FormSemestre ) -> tuple[FormSemestre, FormSemestre]: - """Les deux formsemestres du niveau auquel appartient formsemestre. - Complète le niveau avec le formsemestre antérieur le plus récent. - L'"autre" formsemestre peut ainsi appartenir à l'année scolaire - antérieure (redoublants). + """Les deux formsemestres auquel est inscrit l'étudiant (ni DEM ni DEF) + du niveau auquel appartient formsemestre. + -> S_impair, S_pair + + Si l'origine est impair, S_impair est l'origine et S_pair est None + Si l'origine est paire, S_pair est l'origine, et S_impair l'antérieur + suivi par cet étudiant (ou None). """ if not formsemestre.formation.is_apc(): # garde fou return None, None - if formsemestre.semestre_id % 2 == 0: - other_semestre_id = formsemestre.semestre_id - 1 - else: - other_semestre_id = formsemestre.semestre_id + 1 - other_formsemestre = None + if formsemestre.semestre_id % 2: + idx_autre = formsemestre.semestre_id + 1 + else: + idx_autre = formsemestre.semestre_id - 1 + + # Cherche l'autre semestre de la même année scolaire: + autre_formsemestre = None for inscr in self.etud.formsemestre_inscriptions: if ( + (inscr.etat == scu.INSCRIT) + and # Même spécialité BUT (tolère ainsi des variantes de formation) ( inscr.formsemestre.formation.referentiel_competence == formsemestre.formation.referentiel_competence ) # L'autre semestre - and (inscr.formsemestre.semestre_id == other_semestre_id) - # Antérieur - and inscr.formsemestre.date_debut < formsemestre.date_debut - # Et plus le récent possible - and ( - (other_formsemestre is None) - or (other_formsemestre.date_debut < inscr.formsemestre.date_debut) - ) + and (inscr.formsemestre.semestre_id == idx_autre) + # de la même année scolaire + and inscr.formsemestre.annee_scolaire() == formsemestre.annee_scolaire() ): - other_formsemestre = inscr.formsemestre - if formsemestre.semestre_id % 2 == 0: - return other_formsemestre, formsemestre - return formsemestre, other_formsemestre + autre_formsemestre = inscr.formsemestre + break + # autre_formsemestre peut être None + if formsemestre.semestre_id % 2: + return formsemestre, autre_formsemestre + else: + return autre_formsemestre, formsemestre - def compute_ues_annee(self) -> list[list[UniteEns], list[UniteEns]]: - """UEs à valider cette année pour cet étudiant, selon son parcours. - Affecte self.parcour suivant l'inscription de l'étudiant et - ramène [ listes des UE du semestre impair, liste des UE du semestre pair ]. - """ - ues_sems = [] - for formsemestre, res in ( - (self.formsemestre_impair, self.res_impair), - (self.formsemestre_pair, self.res_pair), - ): - if (formsemestre is None) or (not formsemestre.formation.is_apc()): - ues = [] - else: - parcour, ues = list_ue_parcour_etud(formsemestre, self.etud, res) - if parcour is not None: - self.parcour = parcour - ues_sems.append(ues) - return ues_sems + def get_decisions_rcues_annee(self) -> list["DecisionsProposeesRCUE"]: + "Liste des DecisionsProposeesRCUE de l'année, tirée par numéro d'UE" + return self.decisions_rcue_by_niveau.values() - def check_ues_ready_jury(self) -> list[str]: - """Vérifie que les toutes les UEs (hors bonus) de l'année sont - bien associées à des niveaux de compétences. - Renvoie liste vide si ok, sinon liste de message explicatifs - """ - messages = [] - for ue in self.ues_impair + self.ues_pair: - if ue.niveau_competence is None: - messages.append( - f"UE {ue.acronyme} non associée à un niveau de compétence" - ) - if ue.semestre_idx is None: - messages.append( - f"UE {ue.acronyme} n'a pas d'indice de semestre dans la formation" - ) - return messages + def _compute_rcues_annee(self) -> dict[int, RegroupementCoherentUE]: + "calcule tous les RCUEs: { niveau_id : rcue }" + semestre_id_impair = ((self.formsemestre.semestre_id - 1) // 2) * 2 + 1 + return { + niveau.id: RegroupementCoherentUE( + self.etud, + niveau, + self.res_pair, + self.res_impair, + semestre_id_impair, + self.cur_ues_pair, + self.cur_ues_impair, + ) + for niveau in self.niveaux_competences + } - def compute_rcues_annee(self) -> list[RegroupementCoherentUE]: - """Liste des regroupements d'UE à considérer cette année. - On peut avoir un RCUE à cheval sur plusieurs années (redoublants avec UE capitalisées). - Si on n'a pas les deux semestres, aucun RCUE. - """ - if self.formsemestre_pair is None or self.formsemestre_impair is None: - return [] - rcues_annee = [] - ues_impair_sans_rcue = {ue.id for ue in self.ues_impair} - for ue_pair in self.ues_pair: - rcue = None - for ue_impair in self.ues_impair: - if self.a_cheval: - # l'UE paire DOIT être capitalisée pour être utilisée - if ( - self.decisions_ues[ue_pair.id].code_valide - not in CODES_UE_CAPITALISANTS - ): - continue # ignore cette UE antérieure non capitalisée - # et l'UE impaire doit être actuellement meilleure que - # celle éventuellement capitalisée - if ( - self.decisions_ues[ue_impair.id].ue_status - and self.decisions_ues[ue_impair.id].ue_status["is_capitalized"] - ): - continue # ignore cette UE car capitalisée et actuelle moins bonne - if ue_pair.niveau_competence_id == ue_impair.niveau_competence_id: - rcue = RegroupementCoherentUE( - self.etud, - self.formsemestre_impair, - self.decisions_ues[ue_impair.id], - self.formsemestre_pair, - self.decisions_ues[ue_pair.id], - self.inscription_etat, - ) - ues_impair_sans_rcue.discard(ue_impair.id) - break - # if rcue is None and not self.a_cheval: - # raise NoRCUEError(deca=self, ue=ue_pair) - if rcue is not None: - rcues_annee.append(rcue) - # Si jury annuel (pas à cheval), on doit avoir tous les RCUEs: - # if len(ues_impair_sans_rcue) > 0 and not self.a_cheval: - # ue = UniteEns.query.get(ues_impair_sans_rcue.pop()) - # raise NoRCUEError(deca=self, ue=ue) - return rcues_annee - - def compute_decisions_niveaux(self) -> dict[int, "DecisionsProposeesRCUE"]: + def _compute_decisions_niveaux(self) -> dict[int, "DecisionsProposeesRCUE"]: """Pour chaque niveau de compétence de cette année, construit - le DecisionsProposeesRCUE, ou None s'il n'y en a pas - (ne devrait pas arriver car compute_rcues_annee vérifie déjà cela). - + le DecisionsProposeesRCUE à partir du rcue déjà calculé. Appelé à la construction du deca, donc avant décisions manuelles. 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 - if rcue is not None: - dec_rcue = DecisionsProposeesRCUE(self, rcue, self.inscription_etat) - 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 + niv_rcue = sorted( + self.rcue_by_niveau.items(), + key=lambda x: x[1].ue_1.numero + if x[1].ue_1 + else x[1].ue_2.numero + if x[1].ue_2 + else 0, + ) + return { + niveau_id: DecisionsProposeesRCUE(self, rcue, self.inscription_etat) + for (niveau_id, rcue) in niv_rcue + } def _dec_rcue_by_ue(self) -> dict[int, "DecisionsProposeesRCUE"]: """construit dict { ue_id : DecisionsProposeesRCUE } à partir de self.decisions_rcue_by_niveau""" d = {} for dec_rcue in self.decisions_rcue_by_niveau.values(): - d[dec_rcue.rcue.ue_1.id] = dec_rcue - d[dec_rcue.rcue.ue_2.id] = dec_rcue + if dec_rcue.rcue.ue_1: + d[dec_rcue.rcue.ue_1.id] = dec_rcue + if dec_rcue.rcue.ue_2: + d[dec_rcue.rcue.ue_2.id] = dec_rcue return d - def formsemestre_ects(self) -> float: - "ECTS validés dans le formsemestre de départ du deca" - ues = self.ues_impair if self.formsemestre.semestre_id % 2 else self.ues_pair - return sum( - [ - self.decisions_ues[ue.id].ects_acquis() - for ue in ues - if ue.id in self.decisions_ues - ] - ) + def ects_annee(self) -> float: + "ECTS validés dans l'année BUT courante" + return sum([dec_ue.ects_acquis() for dec_ue in self.decisions_ues.values()]) def next_semestre_ids(self, code: str) -> set[int]: """Les indices des semestres dans lequels l'étudiant est autorisé à poursuivre après le semestre courant. """ - # La poursuite d'études dans un semestre pair d’une même année + # La poursuite d'études dans un semestre pair d'une même année # est de droit pour tout étudiant. # Pas de redoublements directs de S_impair vers S_impair # (pourront être traités manuellement) @@ -687,9 +590,9 @@ class DecisionsProposeesAnnee(DecisionsProposees): self.formsemestre.semestre_id % 2 ) and self.formsemestre.semestre_id < sco_codes.CursusBUT.NB_SEM: return {self.formsemestre.semestre_id + 1} - # La poursuite d’études dans un semestre impair est possible si - # et seulement si l’étudiant a obtenu : - # - la moyenne à plus de la moitié des regroupements cohérents d’UE ; + # La poursuite d'études dans un semestre impair est possible si + # et seulement si l'étudiant a obtenu : + # - la moyenne à plus de la moitié des regroupements cohérents d'UE ; # - et une moyenne égale ou supérieure à 8 sur 20 à chaque RCUE. # # La condition a paru trop stricte à de nombreux collègues. @@ -914,20 +817,20 @@ class DecisionsProposeesAnnee(DecisionsProposees): pour cette année: décisions d'UE, de RCUE, d'année, et autorisations d'inscription émises. Efface même si étudiant DEM ou DEF. - Si à cheval ou only_one_sem, n'efface que les décisions UE et les + Si only_one_sem, n'efface que les décisions UE et les autorisations de passage du semestre d'origine du deca. Dans tous les cas, efface les validations de l'année en cours. (commite la session.) """ - if only_one_sem or self.a_cheval: + if only_one_sem: # N'efface que les autorisations venant de ce semestre, # et les validations de ses UEs ScolarAutorisationInscription.delete_autorisation_etud( - self.etud.id, self.formsemestre_id + self.etud.id, self.formsemestre.id ) for dec_ue in self.decisions_ues.values(): - if dec_ue.formsemestre.id == self.formsemestre_id: + if dec_ue.formsemestre.id == self.formsemestre.id: dec_ue.erase() else: for dec_ue in self.decisions_ues.values(): @@ -968,7 +871,7 @@ class DecisionsProposeesAnnee(DecisionsProposees): # et autres UEs (en cas de changement d'architecture de formation depuis le jury ?) # for validation in ScolarFormSemestreValidation.query.filter_by( - etudid=self.etud.id, formsemestre_id=self.formsemestre_id + etudid=self.etud.id, formsemestre_id=self.formsemestre.id ): db.session.delete(validation) @@ -1026,14 +929,17 @@ class DecisionsProposeesAnnee(DecisionsProposees): for dec_rcue in self.decisions_rcue_by_niveau.values(): if dec_rcue.code_valide in CODES_RCUE_VALIDES: for ue in (dec_rcue.rcue.ue_1, dec_rcue.rcue.ue_2): - dec_ue = self.decisions_ues.get(ue.id) - if dec_ue: - if dec_ue.code_valide not in CODES_UE_VALIDES: + if ue: + dec_ue = self.decisions_ues.get(ue.id) + if dec_ue: + if dec_ue.code_valide not in CODES_UE_VALIDES: + messages.append( + f"L'UE {ue.acronyme} n'est pas validée mais son RCUE l'est !" + ) + else: messages.append( - f"L'UE {ue.acronyme} n'est pas validée mais son RCUE l'est !" + f"L'UE {ue.acronyme} n'a pas décision (???)" ) - else: - messages.append(f"L'UE {ue.acronyme} n'a pas décision (???)") return messages def valide_diplome(self) -> bool: @@ -1042,26 +948,25 @@ class DecisionsProposeesAnnee(DecisionsProposees): def list_ue_parcour_etud( - formsemestre: FormSemestre, etud: Identite, res: ResultatsSemestreBUT -) -> tuple[ApcParcours, list[UniteEns]]: - """Parcour dans lequel l'étudiant est inscrit, - et liste des UEs à valider pour ce semestre (sans les UE "dispensées") - """ - if res.etuds_parcour_id[etud.id] is None: - parcour = None + formsemestre: FormSemestre, + etud: Identite, + parcour: ApcParcours, + res: ResultatsSemestreBUT, +) -> list[UniteEns]: + """Liste des UEs suivies ce semestre (sans les UE "dispensées")""" + + if parcour is None: # pas de parcour: prend toutes les UEs (non bonus) ues = [ue for ue in res.etud_ues(etud.id) if ue.type == UE_STANDARD] ues.sort(key=lambda u: u.numero) else: - parcour = ApcParcours.query.get(res.etuds_parcour_id[etud.id]) ues = ( formsemestre.formation.query_ues_parcour(parcour) .filter(UniteEns.semestre_idx == formsemestre.semestre_id) .order_by(UniteEns.numero) .all() ) - ues = [ue for ue in ues if (etud.id, ue.id) not in res.dispense_ues] - return parcour, ues + return [ue for ue in ues if (etud.id, ue.id) not in res.dispense_ues] class DecisionsProposeesRCUE(DecisionsProposees): @@ -1117,8 +1022,11 @@ class DecisionsProposeesRCUE(DecisionsProposees): self.codes.insert(0, sco_codes.AJ) # Si au moins l'un des semestres est extérieur, propose ADM au cas où if ( - dec_prop_annee.formsemestre_impair.modalite == "EXT" - or dec_prop_annee.formsemestre_pair.modalite == "EXT" + dec_prop_annee.formsemestre_impair + and dec_prop_annee.formsemestre_impair.modalite == "EXT" + ) or ( + dec_prop_annee.formsemestre_pair + and dec_prop_annee.formsemestre_pair.modalite == "EXT" ): self.codes.insert(0, sco_codes.ADM) # S'il y a une décision enregistrée: si elle est plus favorable que celle que l'on @@ -1148,11 +1056,13 @@ class DecisionsProposeesRCUE(DecisionsProposees): """ if self.rcue is None: return False # pas de RCUE a enregistrer + if not (self.rcue.ue_1 and self.rcue.ue_2): + return False # on n'a pas les deux UEs if self.inscription_etat != scu.INSCRIT: return False if code and not code in self.codes: raise ScoValueError( - f"code UE invalide pour ue_id={self.ue.id}: {html.escape(code)}" + f"code RCUE invalide pour {self.rcue}: {html.escape(code)}" ) if code == self.code_valide: self.recorded = True @@ -1166,7 +1076,7 @@ class DecisionsProposeesRCUE(DecisionsProposees): else: self.validation = ApcValidationRCUE( etudid=self.etud.id, - formsemestre_id=self.rcue.formsemestre_2.id, + formsemestre_id=self.deca.formsemestre.id, # origine ue1_id=self.rcue.ue_1.id, ue2_id=self.rcue.ue_2.id, parcours_id=parcours_id, @@ -1190,7 +1100,9 @@ class DecisionsProposeesRCUE(DecisionsProposees): if dec_ue and dec_ue.code_valide not in CODES_UE_VALIDES: log(f"rcue.record: force ADJR sur {dec_ue}") flash( - f"""UEs du RCUE "{dec_ue.ue.niveau_competence.competence.titre}" passées en ADJR""" + f"""UEs du RCUE "{ + dec_ue.ue.niveau_competence.competence.titre + }" passées en ADJR""" ) dec_ue.record(sco_codes.ADJR) @@ -1198,13 +1110,13 @@ class DecisionsProposeesRCUE(DecisionsProposees): if code in CODES_RCUE_VALIDES: self.valide_niveau_inferieur() - if self.rcue.formsemestre_1 is not None: + if self.rcue.res_impair is not None: sco_cache.invalidate_formsemestre( - formsemestre_id=self.rcue.formsemestre_1.id + formsemestre_id=self.rcue.res_impair.formsemestre.id ) - if self.rcue.formsemestre_2 is not None: + if self.rcue.res_pair is not None: sco_cache.invalidate_formsemestre( - formsemestre_id=self.rcue.formsemestre_2.id + formsemestre_id=self.rcue.res_pair.formsemestre.id ) self.code_valide = code # mise à jour état self.recorded = True @@ -1236,13 +1148,13 @@ class DecisionsProposeesRCUE(DecisionsProposees): def valide_niveau_inferieur(self) -> None: """Appelé juste après la validation d'un RCUE. - *La validation des deux UE du niveau d’une compétence emporte la validation de - l’ensemble des UEs du niveau inférieur de cette même compétence.* + *La validation des deux UE du niveau d'une compétence emporte la validation de + l'ensemble des UEs du niveau inférieur de cette même compétence.* """ - if not self.rcue or not self.rcue.ue_1 or not self.rcue.ue_1.niveau_competence: + if not self.rcue: return - competence: ApcCompetence = self.rcue.ue_1.niveau_competence.competence - ordre_inferieur = self.rcue.ue_1.niveau_competence.ordre - 1 + competence: ApcCompetence = self.rcue.niveau.competence + ordre_inferieur = self.rcue.niveau.ordre - 1 if ordre_inferieur < 1: return # pas de niveau inferieur @@ -1259,43 +1171,14 @@ class DecisionsProposeesRCUE(DecisionsProposees): if [v for v in validations_rcue if code_rcue_validant(v.code)]: return # déjà validé - # --- Validations des UEs - ues, ue1, ue2 = self._get_ues_inferieures(competence, ordre_inferieur) - # Pour chaque UE inférieure non validée, valide: - for ue in ues: - validations_ue = ScolarFormSemestreValidation.query.filter_by( - etudid=self.etud.id, ue_id=ue.id - ).all() - if [ - validation - for validation in validations_ue - if sco_codes.code_ue_validant(validation.code) - ]: - continue # on a déjà une validation - # aucune validation validante - validation_ue = validations_ue[0] if validations_ue else None - if validation_ue: - # Modifie validation existante - validation_ue.code = sco_codes.ADSUP - validation_ue.event_date = datetime.now() - if validation_ue.formsemestre_id is not None: - sco_cache.invalidate_formsemestre( - formsemestre_id=validation_ue.formsemestre_id - ) - log(f"updating {validation_ue}") - else: - # Ajoute une validation, - # pas de formsemestre ni de note car pas une capitalisation - validation_ue = ScolarFormSemestreValidation( - etudid=self.etud.id, - code=sco_codes.ADSUP, - ue_id=ue.id, - is_external=True, # pas rattachée à un formsemestre - ) - log(f"recording {validation_ue}") - db.session.add(validation_ue) - - # Valide le RCUE inférieur + # --- Validations des UEs du niveau inférieur + self.valide_ue_inferieures( + self.rcue.semestre_id_impair, ordre_inferieur, competence + ) + self.valide_ue_inferieures( + self.rcue.semestre_id_pair, ordre_inferieur, competence + ) + # --- Valide le RCUE inférieur if validations_rcue: # Met à jour validation existante validation_rcue = validations_rcue[0] @@ -1308,20 +1191,93 @@ class DecisionsProposeesRCUE(DecisionsProposees): sco_cache.invalidate_formsemestre( formsemestre_id=validation_rcue.formsemestre_id ) - elif ue1 and ue2: + else: # Crée nouvelle validation - validation_rcue = ApcValidationRCUE( - etudid=self.etud.id, ue1_id=ue1.id, ue2_id=ue2.id, code=sco_codes.ADSUP + ue1 = self._get_ue_inferieure( + self.rcue.semestre_id_impair, ordre_inferieur, competence ) - db.session.add(validation_rcue) - db.session.commit() - log(f"recording {validation_rcue}") + ue2 = self._get_ue_inferieure( + self.rcue.semestre_id_pair, ordre_inferieur, competence + ) + if ue1 and ue2: + validation_rcue = ApcValidationRCUE( + etudid=self.etud.id, + ue1_id=ue1.id, + ue2_id=ue2.id, + code=sco_codes.ADSUP, + ) + db.session.add(validation_rcue) + db.session.commit() + log(f"recording {validation_rcue}") + self.valide_annee_inferieure() + def _get_ue_inferieure( + self, semestre_id: int, ordre_inferieur: int, competence: ApcCompetence + ) -> UniteEns: + "L'UE de la formation associée au semestre indiqué diu niveau de compétence" + return ( + UniteEns.query.filter_by( + formation_id=self.deca.formsemestre.formation_id, + semestre_idx=semestre_id, + ) + .join(ApcNiveau) + .filter_by(ordre=ordre_inferieur) + .join(ApcCompetence) + .filter_by(id=competence.id) + ).first() + + def valide_ue_inferieures( + self, semestre_id: int, ordre_inferieur: int, competence: ApcCompetence + ): + """Au besoin, enregistre une validation d'UE ADSUP pour le niveau de compétence + semestre_id : l'indice du semestre concerné (le pair ou l'impair) + """ + # Les validations d'UE impaires existantes pour ce niveau inférieur ? + validations_ues: list[ScolarFormSemestreValidation] = ( + ScolarFormSemestreValidation.query.filter_by(etudid=self.etud.id) + .join(UniteEns) + .filter_by(semestre_idx=semestre_id) + .join(ApcNiveau) + .filter_by(ordre=ordre_inferieur) + .join(ApcCompetence) + .filter_by(id=competence.id) + ).all() + validations_ues_validantes = [ + validation + for validation in validations_ues + if sco_codes.code_ue_validant(validation.code) + ] + if not validations_ues_validantes: + # Il faut créer une validation d'UE + # cherche l'UE de notre formation associée à ce niveau + # et warning si il n'y en a pas + ue = self._get_ue_inferieure(semestre_id, ordre_inferieur, competence) + if not ue: + # programme incomplet ou mal paramétré + flash( + f"""Impossible de valider l'UE inférieure du niveau { + ordre_inferieur + } de la compétence {competence.titre} + car elle n'existe pas dans la formation + """, + "warning", + ) + log("valide_ue_inferieures: UE manquante dans la formation") + else: + validation_ue = ScolarFormSemestreValidation( + etudid=self.etud.id, + code=sco_codes.ADSUP, + ue_id=ue.id, + is_external=True, # pas rattachée à un formsemestre + ) + db.session.add(validation_ue) + log(f"recording {validation_ue}") + def valide_annee_inferieure(self) -> None: """Si tous les RCUEs de l'année inférieure sont validés, la valide""" # Indice de l'année inférieure: - annee_courante = self.rcue.ue_1.niveau_competence.annee # "BUT2" + annee_courante = self.rcue.niveau.annee # "BUT2" if not re.match(r"^BUT\d$", annee_courante): log("Warning: valide_annee_inferieure invalid annee_courante") return @@ -1335,7 +1291,7 @@ class DecisionsProposeesRCUE(DecisionsProposees): ordre=annee_inferieure, ) .join(Formation) - .filter_by(formation_code=self.rcue.formsemestre_1.formation.formation_code) + .filter_by(formation_code=self.deca.formsemestre.formation.formation_code) .all() ) if len(validations_annee) > 1: @@ -1352,7 +1308,7 @@ class DecisionsProposeesRCUE(DecisionsProposees): # Liste des niveaux à valider: # ici on sort l'artillerie lourde cursus: EtudCursusBUT = EtudCursusBUT( - self.etud, self.rcue.formsemestre_1.formation + self.etud, self.deca.formsemestre.formation ) niveaux_a_valider = cursus.niveaux_by_annee[annee_inferieure] # Pour chaque niveau, cherche validation RCUE @@ -1375,54 +1331,14 @@ class DecisionsProposeesRCUE(DecisionsProposees): etudid=self.etud.id, ordre=annee_inferieure, code=sco_codes.ADSUP, - formation_id=self.rcue.formsemestre_1.formation_id, - # met cette validation sur l'année scolaire actuelle, pas la précédente (??) - annee_scolaire=self.rcue.formsemestre_1.annee_scolaire(), + formation_id=self.deca.formsemestre.formation_id, + # met cette validation sur l'année scolaire actuelle, pas la précédente + annee_scolaire=self.deca.formsemestre.annee_scolaire(), ) log(f"recording {validation_annee}") db.session.add(validation_annee) db.session.commit() - def _get_ues_inferieures( - self, competence: ApcCompetence, ordre_inferieur: int - ) -> tuple[list[UniteEns], UniteEns, UniteEns]: - """Les UEs de cette formation associées au niveau de compétence inférieur ? - Note: on ne cherche que dans la formation courante, pas les UEs de - même code d'autres formations. - """ - formation: Formation = self.rcue.formsemestre_1.formation - ues: list[UniteEns] = ( - UniteEns.query.filter_by(formation_id=formation.id) - .filter(UniteEns.semestre_idx != None) - .join(ApcNiveau) - .filter_by(ordre=ordre_inferieur) - .join(ApcCompetence) - .filter_by(id=competence.id) - .all() - ) - log(f"valide_niveau_inferieur: {competence} UEs inférieures: {ues}") - if len(ues) != 2: # on n'a pas 2 UE associées au niveau inférieur ! - flash( - "Impossible de valider le niveau de compétence inférieur: pas 2 UEs associées'", - "warning", - ) - return [], None, None - ues_impaires = [ue for ue in ues if ue.semestre_idx % 2] - if len(ues_impaires) != 1: - flash( - "Impossible de valider le niveau de compétence inférieur: pas d'UE impaire associée" - ) - return [], None, None - ue1 = ues_impaires[0] - ues_paires = [ue for ue in ues if not ue.semestre_idx % 2] - if len(ues_paires) != 1: - flash( - "Impossible de valider le niveau de compétence inférieur: pas d'UE paire associée" - ) - return [], None, None - ue2 = ues_paires[0] - return ues, ue1, ue2 - class DecisionsProposeesUE(DecisionsProposees): """Décisions de jury sur une UE du BUT @@ -1437,6 +1353,10 @@ class DecisionsProposeesUE(DecisionsProposees): sinon: ADJ, AJ et proposer toujours: RAT, DEF, ABAN, ADJR, DEM, UEBSL (codes_communs) + + + Le DecisionsProposeesUE peut concerner une UE du formsemestre, ou une validation + antérieure non éditable. """ # Codes toujours proposés sauf si include_communs est faux: @@ -1454,51 +1374,59 @@ class DecisionsProposeesUE(DecisionsProposees): self, etud: Identite, formsemestre: FormSemestre, - ue: UniteEns, + rcue: RegroupementCoherentUE = None, + paire: bool = False, inscription_etat: str = scu.INSCRIT, ): - # Une UE peut être validée plusieurs fois en cas de redoublement (qu'elle soit capitalisée ou non) + self.paire = paire + self.ue: UniteEns = rcue.ue_2 if paire else rcue.ue_1 + self.inscription_etat = inscription_etat + # 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) validation = ScolarFormSemestreValidation.query.filter_by( - etudid=etud.id, formsemestre_id=formsemestre.id, ue_id=ue.id + etudid=etud.id, formsemestre_id=formsemestre.id, ue_id=self.ue.id << XXX ).first() super().__init__( etud=etud, code_valide=validation.code if validation is not None else None, ) self.validation = validation + "validation dans le formsemestre courant" self.formsemestre = formsemestre - self.ue: UniteEns = ue - self.rcue: RegroupementCoherentUE = None + self.rcue: RegroupementCoherentUE = rcue "Le rcue auquel est rattaché cette UE, ou None" - self.inscription_etat = inscription_etat - "inscription: I, DEM, DEF dans le semestre de cette UE" + # Editable ou pas ? + # si ue courante, éditable. + self.editable = ( + (self.rcue.ue_cur_pair is not None) + if paire + else (self.rcue.ue_cur_impair is not None) + ) + res: ResultatsSemestreBUT = ( + self.rcue.res_pair if paire else self.rcue.res_impair + ) self.moy_ue = np.NaN self.moy_ue_with_cap = np.NaN self.ue_status = {} - if ue.type == sco_codes.UE_SPORT: - self.explanation = "UE bonus, pas de décision de jury" + if self.ue.type != sco_codes.UE_STANDARD: + self.explanation = "UE non standard, pas de décision de jury BUT" self.codes = [] # aucun code proposé return - if inscription_etat != scu.INSCRIT: + + if res and res.get_etud_etat(etud.id) != scu.INSCRIT: self.validation = None # cache toute validation - self.explanation = "non incrit (dem. ou déf.)" + self.explanation = "non inscrit (dem. ou déf.)" self.codes = [ - sco_codes.DEM if inscription_etat == scu.DEMISSION else sco_codes.DEF + sco_codes.DEM + if res.get_etud_etat(etud.id) == scu.DEMISSION + else sco_codes.DEF ] return # Moyenne de l'UE ? - res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre) - # Safety checks: - if not ue.id in res.etud_moy_ue: - self.explanation = "UE sans résultat" - return - if not etud.id in res.etud_moy_ue[ue.id]: - self.explanation = "Étudiant sans résultat dans cette UE" - return - ue_status = res.get_etud_ue_status(etud.id, ue.id) + ue_status = self.rcue.ue_status_pair if paire else self.rcue.ue_status_impair self.moy_ue = ue_status["cur_moy_ue"] self.moy_ue_with_cap = ue_status["moy"] self.ue_status = ue_status @@ -1508,11 +1436,6 @@ class DecisionsProposeesUE(DecisionsProposees): return f"""<{self.__class__.__name__} ue={self.ue.acronyme} valid={self.code_valide } codes={self.codes} explanation={self.explanation}>""" - def set_rcue(self, rcue: RegroupementCoherentUE): - """Rattache cette UE à un RCUE. Cela peut modifier les codes - proposés par compute_codes() (si compensation)""" - self.rcue = rcue - def compute_codes(self): """Calcul des .codes attribuables et de l'explanation associée""" if self.inscription_etat != scu.INSCRIT: @@ -1607,91 +1530,91 @@ class DecisionsProposeesUE(DecisionsProposees): return 0.0 -class BUTCursusEtud: # WIP TODO - """Validation du cursus d'un étudiant""" +# class BUTCursusEtud: # WIP TODO +# """Validation du cursus d'un étudiant""" - def __init__(self, formsemestre: FormSemestre, etud: Identite): - if formsemestre.formation.referentiel_competence is None: - raise ScoNoReferentielCompetences(formation=formsemestre.formation) - 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 __init__(self, formsemestre: FormSemestre, etud: Identite): +# if formsemestre.formation.referentiel_competence is None: +# raise ScoNoReferentielCompetences(formation=formsemestre.formation) +# 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_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_annee_validee(self, ordre: int) -> bool: - """Vrai si l'année BUT ordre est validée""" - # On cherche les validations d'annee avec le même - # code formation que nous. - return ( - ApcValidationAnnee.query.filter_by( - etudid=self.etud.id, - ordre=ordre, - ) - .join(Formation) - .filter( - Formation.formation_code == self.formsemestre.formation.formation_code - ) - .count() - > 0 - ) +# def est_annee_validee(self, ordre: int) -> bool: +# """Vrai si l'année BUT ordre est validée""" +# # On cherche les validations d'annee avec le même +# # code formation que nous. +# return ( +# ApcValidationAnnee.query.filter_by( +# etudid=self.etud.id, +# ordre=ordre, +# ) +# .join(Formation) +# .filter( +# Formation.formation_code == self.formsemestre.formation.formation_code +# ) +# .count() +# > 0 +# ) - def est_diplome(self) -> bool: - """Vrai si BUT déjà validé""" - # vrai si la troisième année est validée - return self.est_annee_validee(3) +# def est_diplome(self) -> bool: +# """Vrai si BUT déjà validé""" +# # vrai si la troisième année est validée +# return self.est_annee_validee(3) - 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 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). - """ - # XXX A REVOIR - 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_validee(self, competence: ApcCompetence) -> bool: +# """Vrai si la compétence est validée, c'est à dire que tous ses +# niveaux sont validés (ApcValidationRCUE). +# """ +# # XXX A REVOIR +# 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. +# 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 +# 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. +# 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 donnée 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 +# Selon la règle donnée 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 diff --git a/app/but/jury_but_pv.py b/app/but/jury_but_pv.py index e3119b2764..29630af045 100644 --- a/app/but/jury_but_pv.py +++ b/app/but/jury_but_pv.py @@ -153,7 +153,7 @@ def pvjury_table_but( etudid=etud.id, ), "cursus": _descr_cursus_but(etud), - "ects": f"{deca.formsemestre_ects():g}", + "ects": f"{deca.ects_annee():g}", "ues": deca.descr_ues_validation(line_sep=line_sep) if deca else "-", "niveaux": deca.descr_niveaux_validation(line_sep=line_sep) if deca diff --git a/app/but/jury_but_results.py b/app/but/jury_but_results.py index 00aa649ad8..089b2e772a 100644 --- a/app/but/jury_but_results.py +++ b/app/but/jury_but_results.py @@ -48,9 +48,9 @@ def _get_jury_but_etud_result( # --- Les RCUEs rcue_list = [] if deca: - for rcue in deca.rcues_annee: - dec_rcue = deca.dec_rcue_by_ue.get(rcue.ue_1.id) - if dec_rcue is not None: # None si l'UE n'est pas associée à un niveau + for dec_rcue in deca.get_decisions_rcues_annee(): + rcue = dec_rcue.rcue + if rcue.complete: # n'exporte que les RCUEs complets dec_ue1 = deca.decisions_ues[rcue.ue_1.id] dec_ue2 = deca.decisions_ues[rcue.ue_2.id] rcue_dict = { diff --git a/app/but/jury_but_view.py b/app/but/jury_but_view.py index 4af88cae57..cfb778152b 100644 --- a/app/but/jury_but_view.py +++ b/app/but/jury_but_view.py @@ -93,35 +93,25 @@ def show_etud(deca: DecisionsProposeesAnnee, read_only: bool = True) -> str:
    RCUE
    """ ) - for niveau in deca.niveaux_competences: + for dec_rcue in deca.get_decisions_rcues_annee(): + rcue = dec_rcue.rcue + niveau = rcue.niveau H.append( f"""
    {niveau.competence.titre}
    """ ) - dec_rcue = deca.decisions_rcue_by_niveau.get(niveau.id) # peut être None - ues = [ - ue - for ue in deca.ues_impair - if ue.niveau_competence and ue.niveau_competence.id == niveau.id - ] - ue_impair = ues[0] if ues else None - ues = [ - ue - for ue in deca.ues_pair - if ue.niveau_competence and ue.niveau_competence.id == niveau.id - ] - ue_pair = ues[0] if ues else None + ue_impair, ue_pair = rcue.ue_1, rcue.ue_2 # Les UEs à afficher, - # qui seront toujours en readonly sur le formsemestre de l'année précédente du redoublant + # qui ues_ro = [ ( ue_impair, - (deca.a_cheval and deca.formsemestre_id != deca.formsemestre_impair.id), + rcue.ue_cur_impair is None, ), ( ue_pair, - deca.a_cheval and deca.formsemestre_id != deca.formsemestre_pair.id, + rcue.ue_cur_pair is None, ), ] # Ordonne selon les dates des 2 semestres considérés: @@ -155,12 +145,13 @@ def _gen_but_select( code_valide: str, disabled: bool = False, klass: str = "", - data: dict = {}, + data: dict = None, code_valide_label: str = "", ) -> str: "Le menu html select avec les codes" # if disabled: # mauvaise idée car le disabled est traité en JS # return f"""
    {code_valide}
    """ + data = data or {} options_htm = "\n".join( [ f"""