diff --git a/app/but/cursus_but.py b/app/but/cursus_but.py index 5b5b6f29c26a1f3eacef8ea0914d8df2f1e883e8..5eda000dcaad1143da74a5cc7ca292ab88e36984 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 bb73af4311b9854965384c611121393b35e1661f..d4e7dd8cf2c133167e618f4602ecbe1e21211bee 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 + # ---- 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 + ) + # ---- 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 None ) - self.inscription_etat_impair = ( - etud.inscription_etat(formsemestre_impair.id) - if formsemestre_impair is not None + self.res_pair: ResultatsSemestreBUT = ( + res_sem.load_formsemestre_results(self.formsemestre_pair) + if self.formsemestre_pair 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() + self.cur_ues_impair = ( + list_ue_parcour_etud( + self.formsemestre_impair, self.etud, self.parcour, self.res_impair ) - 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 + if self.formsemestre_impair + else [] + ) + self.cur_ues_pair = ( + list_ue_parcour_etud( + self.formsemestre_pair, self.etud, self.parcour, self.res_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 + 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 [] + ) + """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.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, ) - else: - self.res_impair = None + .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" - self.ues_impair, self.ues_pair = self.compute_ues_annee() # pylint: disable=all + # ---- Décisions d'UEs self.decisions_ues = { - ue.id: DecisionsProposeesUE( - etud, formsemestre_impair, ue, self.inscription_etat_impair + rcue.ue_1.id: DecisionsProposeesUE( + etud, self.formsemestre_impair, rcue, False, self.inscription_etat ) - for ue in self.ues_impair + for rcue in self.rcue_by_niveau.values() + if rcue.ue_1 } - "{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 + rcue.ue_2.id: DecisionsProposeesUE( + etud, self.formsemestre_pair, rcue, True, self.inscription_etat ) - for ue in self.ues_pair + for rcue in self.rcue_by_niveau.values() + if rcue.ue_2 } ) - self.rcues_annee = self.compute_rcues_annee() - - formation = ( - self.formsemestre_impair.formation - if self.formsemestre_impair - else self.formsemestre_pair.formation - ) - ( - parcours, - niveaux_by_parcours, - ) = formation.referentiel_competence.get_niveaux_by_parcours( - self.annee_but, [self.parcour] if self.parcour else None - ) - 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. - = niveaux du tronc commun + niveau du parcours de l'étudiant. - """ - self.decisions_rcue_by_niveau = self.compute_decisions_niveaux() + 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 += "</li>" text += f""" - <li>RCUEs: {html.escape(str(self.rcues_annee))}</li> + <li>RCUEs: {html.escape(str(self.rcue_by_niveau))}</li> <li>nb_competences: {getattr(self, "nb_competences", "-")}</li> <li>nb_validables: {getattr(self, "nb_validables", "-")}</li> <li>codes: {self.codes}</li> @@ -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 + + if formsemestre.semestre_id % 2: + idx_autre = formsemestre.semestre_id + 1 else: - other_semestre_id = formsemestre.semestre_id + 1 + idx_autre = formsemestre.semestre_id - 1 - other_formsemestre = None + # 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 - - 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 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 + 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 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 _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""" - - 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_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 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_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 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 +# 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 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_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 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. + +# 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 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 e3119b276467c4f7e2ddcd4437089291b4103bb6..29630af045c4ff719c5bc4d52fb2a12167009f0c 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 00aa649ad819fdd9022138fad29a42fed1f001fe..089b2e772a040477fcb3b0b1963a19ed6d02f02f 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 4af88cae5738e0e384168915bffe7774268d419f..cfb778152b2794f6e8ec94684f6d46ab1ec18370 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: <div class="titre">RCUE</div> """ ) - 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"""<div class="but_niveau_titre"> <div title="{niveau.competence.titre_long}">{niveau.competence.titre}</div> </div>""" ) - 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"""<div class="but_code {klass}">{code_valide}</div>""" + data = data or {} options_htm = "\n".join( [ f"""<option value="{code}" @@ -220,8 +211,14 @@ def _gen_but_niveau_ue( else: scoplement = "" - return f"""<div class="but_niveau_ue { - 'recorded' if dec_ue.code_valide is not None else ''} + ue_class = "" # 'recorded' if dec_ue.code_valide is not None else '' + if dec_ue.code_valide is not None and dec_ue.codes: + if dec_ue.code_valide == dec_ue.codes[0]: + ue_class = "recorded" + else: + ue_class = "recorded_different" + + return f"""<div class="but_niveau_ue {ue_class} {'annee_prec' if annee_prec else ''} "> <div title="{ue.titre}">{ue.acronyme}</div> @@ -242,7 +239,7 @@ def _gen_but_niveau_ue( def _gen_but_rcue(dec_rcue: DecisionsProposeesRCUE, niveau: ApcNiveau) -> str: - if dec_rcue is None: + if dec_rcue is None or not dec_rcue.rcue.complete: return """ <div class="but_niveau_rcue niveau_vide with_scoplement"> <div></div> diff --git a/app/models/but_validations.py b/app/models/but_validations.py index 4054c9f43b751baadb73cb7192a19366594b7c34..00dc61952fa7f698df72d343f76414876aeccc4d 100644 --- a/app/models/but_validations.py +++ b/app/models/but_validations.py @@ -4,7 +4,6 @@ """ from typing import Union -from flask_sqlalchemy.query import Query from app import db from app.models import CODE_STR_LEN @@ -13,8 +12,6 @@ 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 codes_cursus as sco_codes -from app.scodoc import sco_utils as scu class ApcValidationRCUE(db.Model): @@ -22,7 +19,7 @@ class ApcValidationRCUE(db.Model): aka "regroupements cohérents d'UE" dans le jargon BUT. - Le formsemestre est celui du semestre PAIR du niveau de compétence + Le formsemestre est l'origine, utilisé pour effacer """ __tablename__ = "apc_validation_rcue" @@ -109,139 +106,6 @@ 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*. - - La moyenne (10/20) au RCUE déclenche la compensation des UE. - """ - - def __init__( - self, - etud: Identite, - formsemestre_1: FormSemestre, - dec_ue_1: "DecisionsProposeesUE", - formsemestre_2: FormSemestre, - dec_ue_2: "DecisionsProposeesUE", - inscription_etat: str, - ): - ue_1 = dec_ue_1.ue - ue_2 = dec_ue_2.ue - # Ordonne les UE dans le sens croissant (S1,S2) ou (S3,S4)... - if formsemestre_1.semestre_id > formsemestre_2.semestre_id: - (ue_1, formsemestre_1), (ue_2, formsemestre_2) = ( - (ue_2, formsemestre_2), - (ue_1, formsemestre_1), - ) - assert formsemestre_1.semestre_id % 2 == 1 - assert formsemestre_2.semestre_id % 2 == 0 - assert abs(formsemestre_1.semestre_id - formsemestre_2.semestre_id) == 1 - assert ue_1.niveau_competence_id == ue_2.niveau_competence_id - self.etud = etud - self.formsemestre_1 = formsemestre_1 - "semestre impair" - self.ue_1 = ue_1 - self.formsemestre_2 = formsemestre_2 - "semestre pair" - self.ue_2 = ue_2 - # Stocke les moyennes d'UE - if inscription_etat != scu.INSCRIT: - self.moy_rcue = None - self.moy_ue_1 = self.moy_ue_2 = "-" - self.moy_ue_1_val = self.moy_ue_2_val = 0.0 - return - self.moy_ue_1 = dec_ue_1.moy_ue_with_cap - self.moy_ue_1_val = self.moy_ue_1 if self.moy_ue_1 is not None else 0.0 - self.moy_ue_2 = dec_ue_2.moy_ue_with_cap - 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) - 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 * ue_1.coef_rcue + self.moy_ue_2 * ue_2.coef_rcue - ) / (ue_1.coef_rcue + ue_2.coef_rcue) - else: - self.moy_rcue = None - - def __repr__(self) -> str: - return f"""<{self.__class__.__name__} { - self.ue_1.acronyme}({self.moy_ue_1}) { - self.ue_2.acronyme}({self.moy_ue_2})>""" - - def __str__(self) -> str: - return f"""RCUE { - self.ue_1.acronyme}({self.moy_ue_1}) + { - self.ue_2.acronyme}({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 > sco_codes.BUT_BARRE_RCUE) - and ( - (self.moy_ue_1_val < sco_codes.NOTES_BARRE_GEN) - or (self.moy_ue_2_val < sco_codes.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 > sco_codes.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 > sco_codes.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 sco_codes.CODES_RCUE_VALIDES - ): - return validation - return None - - # unused # def find_rcues( # formsemestre: FormSemestre, ue: UniteEns, etud: Identite, inscription_etat: str @@ -319,7 +183,7 @@ class ApcValidationAnnee(db.Model): formsemestre_id = db.Column( db.Integer, db.ForeignKey("notes_formsemestre.id"), nullable=True ) - "le semestre IMPAIR (le 1er) de l'année" + "le semestre origine, normalement l'IMPAIR (le 1er) de l'année" formation_id = db.Column( db.Integer, db.ForeignKey("notes_formations.id"), diff --git a/app/models/events.py b/app/models/events.py index d3e7709761262dc6db73f1465eec99862d3fed5b..de93a24c0f608ad1a6f642c1c1a3a220822298ad 100644 --- a/app/models/events.py +++ b/app/models/events.py @@ -265,11 +265,8 @@ class ScolarNews(db.Model): # Informations générales H.append( - f"""<div> - Pour être informé des évolutions de ScoDoc, - vous pouvez vous - <a class="stdlink" href="{scu.SCO_ANNONCES_WEBSITE}"> - abonner à la liste de diffusion</a>. + f"""<div><a class="discretelink" href="{scu.SCO_ANNONCES_WEBSITE}"> + Pour en savoir plus sur ScoDoc voir le site scodoc.org</a>. </div> """ ) diff --git a/app/models/validations.py b/app/models/validations.py index 10791758a0d68e525f7694e29537abdf7fea8f22..9e2cf5e27d58bc96e73d42800cc8c57cac261aaa 100644 --- a/app/models/validations.py +++ b/app/models/validations.py @@ -68,7 +68,7 @@ class ScolarFormSemestreValidation(db.Model): if self.ue_id: # Note: si l'objet vient d'être créé, ue_id peut exister mais pas ue ! return f"""décision sur UE {self.ue.acronyme if self.ue else self.ue_id - }: {self.code}""" + } ({self.ue_id}): {self.code}""" return f"""décision sur semestre {self.formsemestre.titre_mois()} du { self.event_date.strftime("%d/%m/%Y")}""" diff --git a/app/scodoc/sco_formsemestre_edit.py b/app/scodoc/sco_formsemestre_edit.py index 4a94f0bde8ff9877d287ee681ff6df46e03473fd..67410b5c145f9c923672a28f6f42669909a92e3b 100644 --- a/app/scodoc/sco_formsemestre_edit.py +++ b/app/scodoc/sco_formsemestre_edit.py @@ -793,7 +793,13 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N {tf[1]} """ elif tf[0] == -1: - return "<h4>annulation</h4>" + return redirect( + url_for( + "notes.formsemestre_status", + scodoc_dept=g.scodoc_dept, + formsemestre_id=formsemestre.id, + ) + ) else: if tf[2]["gestion_compensation_lst"]: tf[2]["gestion_compensation"] = True diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index c163806eebc2d0fdbd110537db3e54649286cd15..17dd909f8fc46c50eb8f9876407b9fad2703ab0d 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -520,7 +520,7 @@ SCO_DEFAULT_SQL_USERS_CNX = "dbname=SCOUSERS port=%s" % SCO_DEFAULT_SQL_PORT # Valeurs utilisées pour affichage seulement, pas de requetes ni de mails envoyés: SCO_WEBSITE = "https://scodoc.org" SCO_USER_MANUAL = "https://scodoc.org/GuideUtilisateur" -SCO_ANNONCES_WEBSITE = "https://listes.univ-paris13.fr/mailman/listinfo/scodoc-annonces" +SCO_ANNONCES_WEBSITE = "https://scodoc.org/Contact" SCO_DEVEL_LIST = "scodoc-devel@listes.univ-paris13.fr" SCO_USERS_LIST = "notes@listes.univ-paris13.fr" SCO_LISTS_URL = "https://scodoc.org/Contact" diff --git a/app/static/js/scodoc.js b/app/static/js/scodoc.js index 44b0352be303b045ac583f206111a4874ae3f93b..b7de540872f8a4741d23f6eb01a5fefaa88e73f4 100644 --- a/app/static/js/scodoc.js +++ b/app/static/js/scodoc.js @@ -1,291 +1,316 @@ // JS for all ScoDoc pages (using jQuery UI) - $(function () { - // Autocomplete recherche etudiants par nom - $(".in-expnom").autocomplete( - { - delay: 300, // wait 300ms before suggestions - minLength: 2, // min nb of chars before suggest - position: { collision: 'flip' }, // automatic menu position up/down - source: SCO_URL + "/search_etud_by_name", - select: function (event, ui) { - $(".in-expnom").val(ui.item.value); - $("#form-chercheetud").submit(); - } - }); + // Autocomplete recherche etudiants par nom + $(".in-expnom").autocomplete({ + delay: 300, // wait 300ms before suggestions + minLength: 2, // min nb of chars before suggest + position: { collision: "flip" }, // automatic menu position up/down + source: SCO_URL + "/search_etud_by_name", + select: function (event, ui) { + $(".in-expnom").val(ui.item.value); + $("#form-chercheetud").submit(); + }, + }); - // Date picker - $(".datepicker").datepicker({ - showOn: 'button', - buttonImage: '/ScoDoc/static/icons/calendar_img.png', - buttonImageOnly: true, - dateFormat: 'dd/mm/yy', - duration: 'fast', - }); - $('.datepicker').datepicker('option', $.extend({ showMonthAfterYear: false }, - $.datepicker.regional['fr'])); + // Date picker + $(".datepicker").datepicker({ + showOn: "button", + buttonImage: "/ScoDoc/static/icons/calendar_img.png", + buttonImageOnly: true, + dateFormat: "dd/mm/yy", + duration: "fast", + }); + $(".datepicker").datepicker( + "option", + $.extend({ showMonthAfterYear: false }, $.datepicker.regional["fr"]) + ); - /* Barre menu */ - var sco_menu_position = { my: "left top", at: "left bottom" }; - $("#sco_menu").menu({ - position: sco_menu_position, - blur: function () { - $(this).menu("option", "position", sco_menu_position); - }, - focus: function (e, ui) { - if ($("#sco_menu").get(0) !== $(ui).get(0).item.parent().get(0)) { - $(this).menu("option", "position", { my: "left top", at: "right top" }); - } + /* Barre menu */ + var sco_menu_position = { my: "left top", at: "left bottom" }; + $("#sco_menu") + .menu({ + position: sco_menu_position, + blur: function () { + $(this).menu("option", "position", sco_menu_position); + }, + focus: function (e, ui) { + if ($("#sco_menu").get(0) !== $(ui).get(0).item.parent().get(0)) { + $(this).menu("option", "position", { + my: "left top", + at: "right top", + }); } - }).mouseleave(function (x, y) { - $("#sco_menu").menu('collapseAll'); + }, + }) + .mouseleave(function (x, y) { + $("#sco_menu").menu("collapseAll"); }); - $("#sco_menu > li > a > span").switchClass("ui-icon-carat-1-e", "ui-icon-carat-1-s"); + $("#sco_menu > li > a > span").switchClass( + "ui-icon-carat-1-e", + "ui-icon-carat-1-s" + ); - /* Les menus isoles dropdown */ - $(".sco_dropdown_menu").menu({ - position: sco_menu_position - }).mouseleave(function (x, y) { - $(".sco_dropdown_menu").menu('collapseAll'); - } - ); - $(".sco_dropdown_menu > li > a > span").switchClass("ui-icon-carat-1-e", "ui-icon-carat-1-s"); + /* Les menus isoles dropdown */ + $(".sco_dropdown_menu") + .menu({ + position: sco_menu_position, + }) + .mouseleave(function (x, y) { + $(".sco_dropdown_menu").menu("collapseAll"); + }); + $(".sco_dropdown_menu > li > a > span").switchClass( + "ui-icon-carat-1-e", + "ui-icon-carat-1-s" + ); - /* up-to-date status */ - var update_div = document.getElementById("update_warning"); - if (update_div) { - fetch('install_info').then( - response => response.text() - ).then(text => { - update_div.innerHTML = text; - if (text) { - update_div.style.display = "block"; - } - }); - } + /* up-to-date status */ + var update_div = document.getElementById("update_warning"); + if (update_div) { + fetch("install_info") + .then((response) => response.text()) + .then((text) => { + update_div.innerHTML = text; + if (text) { + update_div.style.display = "block"; + } + }); + } }); function sco_capitalize(string) { - return string[0].toUpperCase() + string.slice(1).toLowerCase(); + return string[0].toUpperCase() + string.slice(1).toLowerCase(); } // Affiche un message transitoire (duration milliseconds, 0 means infinity) function sco_message(msg, className = "message_custom", duration = 0) { - var div = document.createElement("div"); - div.className = className; - div.innerHTML = msg; - document.querySelector("body").appendChild(div); - if (duration) { - setTimeout(() => { - div.remove(); - }, 3000); - } + var div = document.createElement("div"); + div.className = className; + div.innerHTML = msg; + document.querySelector("body").appendChild(div); + if (duration) { + setTimeout(() => { + div.remove(); + }, 8000); + } } function sco_error_message(msg) { - sco_message(msg, className = "message_error", duration = 0); + sco_message(msg, (className = "message_error"), (duration = 0)); } - function get_query_args() { - var s = window.location.search; // eg "?x=1&y=2" - var vars = {}; - s.replace( - /[?&]+([^=&]+)=?([^&]*)?/gi, // regexp - function (m, key, value) { // callback - vars[key] = value !== undefined ? value : ''; - } - ); - return vars; + var s = window.location.search; // eg "?x=1&y=2" + var vars = {}; + s.replace( + /[?&]+([^=&]+)=?([^&]*)?/gi, // regexp + function (m, key, value) { + // callback + vars[key] = value !== undefined ? value : ""; + } + ); + return vars; } - // Tables (gen_tables) $(function () { - if ($('table.gt_table').length > 0) { - var table_options = { - "paging": false, - "searching": false, - "info": false, - /* "autoWidth" : false, */ - "fixedHeader": { - "header": true, - "footer": true - }, - "orderCellsTop": true, // cellules ligne 1 pour tri - "aaSorting": [], // Prevent initial sorting - }; - $('table.gt_table').DataTable(table_options); - table_options["searching"] = true; - $('table.gt_table_searchable').DataTable(table_options); - } + if ($("table.gt_table").length > 0) { + var table_options = { + paging: false, + searching: false, + info: false, + /* "autoWidth" : false, */ + fixedHeader: { + header: true, + footer: true, + }, + orderCellsTop: true, // cellules ligne 1 pour tri + aaSorting: [], // Prevent initial sorting + }; + $("table.gt_table").DataTable(table_options); + table_options["searching"] = true; + $("table.gt_table_searchable").DataTable(table_options); + } }); - // Show tags (readonly) function readOnlyTags(nodes) { - // nodes are textareas, hide them and create a span showing tags - for (var i = 0; i < nodes.length; i++) { - var node = $(nodes[i]); - node.hide(); - var tags = nodes[i].value.split(','); - node.after('<span class="ro_tags"><span class="ro_tag">' + tags.join('</span><span class="ro_tag">') + '</span></span>'); - } + // nodes are textareas, hide them and create a span showing tags + for (var i = 0; i < nodes.length; i++) { + var node = $(nodes[i]); + node.hide(); + var tags = nodes[i].value.split(","); + node.after( + '<span class="ro_tags"><span class="ro_tag">' + + tags.join('</span><span class="ro_tag">') + + "</span></span>" + ); + } } /* Editeur pour champs * Usage: créer un élément avec data-oid (object id) - * La méthode d'URL save sera appelée en POST avec deux arguments: oid et value, + * La méthode d'URL save sera appelée en POST avec deux arguments: oid et value, * value contenant la valeur du champs. * Inspiré par les codes et conseils de Seb. L. */ class ScoFieldEditor { - constructor(selector, save_url, read_only) { - this.save_url = save_url; - this.read_only = read_only; - this.selector = selector; - this.installListeners(); + constructor(selector, save_url, read_only) { + this.save_url = save_url; + this.read_only = read_only; + this.selector = selector; + this.installListeners(); + } + // Enregistre l'élément obj + save(obj) { + var value = obj.innerText.trim(); + if (value.length == 0) { + value = ""; } - // Enregistre l'élément obj - save(obj) { - var value = obj.innerText.trim(); - if (value.length == 0) { - value = ""; - } - if (value == obj.dataset.value) { - return true; // Aucune modification, pas d'enregistrement mais on continue normalement - } - obj.classList.add("sco_wait"); - // DEBUG - // console.log(` - // data : ${value}, - // id: ${obj.dataset.oid} - // `); + if (value == obj.dataset.value) { + return true; // Aucune modification, pas d'enregistrement mais on continue normalement + } + obj.classList.add("sco_wait"); + // DEBUG + // console.log(` + // data : ${value}, + // id: ${obj.dataset.oid} + // `); - $.post(this.save_url, - { - oid: obj.dataset.oid, - value: value, - }, - function (result) { - obj.classList.remove("sco_wait"); - obj.classList.add("sco_modified"); - } - ); - return true; + $.post( + this.save_url, + { + oid: obj.dataset.oid, + value: value, + }, + function (result) { + obj.classList.remove("sco_wait"); + obj.classList.add("sco_modified"); + } + ); + return true; + } + /*****************************/ + /* Gestion des évènements */ + /*****************************/ + installListeners() { + if (this.read_only) { + return; } - /*****************************/ - /* Gestion des évènements */ - /*****************************/ - installListeners() { - if (this.read_only) { - return; - } - document.body.addEventListener("keydown", this.key); - let editor = this; - this.handleSelectCell = (event) => { editor.selectCell(event) }; - this.handleModifCell = (event) => { editor.modifCell(event) }; - this.handleBlur = (event) => { editor.blurCell(event) }; - this.handleKeyCell = (event) => { editor.keyCell(event) }; - document.querySelectorAll(this.selector).forEach(cellule => { - cellule.addEventListener("click", this.handleSelectCell); - cellule.addEventListener("dblclick", this.handleModifCell); - cellule.addEventListener("blur", this.handleBlur); - }); + document.body.addEventListener("keydown", this.key); + let editor = this; + this.handleSelectCell = (event) => { + editor.selectCell(event); + }; + this.handleModifCell = (event) => { + editor.modifCell(event); + }; + this.handleBlur = (event) => { + editor.blurCell(event); + }; + this.handleKeyCell = (event) => { + editor.keyCell(event); + }; + document.querySelectorAll(this.selector).forEach((cellule) => { + cellule.addEventListener("click", this.handleSelectCell); + cellule.addEventListener("dblclick", this.handleModifCell); + cellule.addEventListener("blur", this.handleBlur); + }); + } + /*********************************/ + /* Interaction avec les cellules */ + /*********************************/ + blurCell(event) { + let currentModif = document.querySelector(".sco_modifying"); + if (currentModif) { + if (!this.save(currentModif)) { + return; + } } - /*********************************/ - /* Interaction avec les cellules */ - /*********************************/ - blurCell(event) { - let currentModif = document.querySelector(".sco_modifying"); - if (currentModif) { - if (!this.save(currentModif)) { - return; - } + } + selectCell(event) { + let obj = event.currentTarget; + if (obj) { + if (obj.classList.contains("sco_modifying")) { + return; // Cellule en cours de modification, ne pas sélectionner. + } + let currentModif = document.querySelector(".sco_modifying"); + if (currentModif) { + if (!this.save(currentModif)) { + return; } - } - selectCell(event) { - let obj = event.currentTarget; - if (obj) { - if (obj.classList.contains("sco_modifying")) { - return; // Cellule en cours de modification, ne pas sélectionner. - } - let currentModif = document.querySelector(".sco_modifying"); - if (currentModif) { - if (!this.save(currentModif)) { - return; - } - } + } - this.unselectCell(); - obj.classList.add("sco_selected"); - } + this.unselectCell(); + obj.classList.add("sco_selected"); } - unselectCell() { - document.querySelectorAll(".sco_selected, .sco_modifying").forEach(cellule => { - cellule.classList.remove("sco_selected", "sco_modifying"); - cellule.removeAttribute("contentEditable"); - cellule.removeEventListener("keydown", this.handleKeyCell); - }); + } + unselectCell() { + document + .querySelectorAll(".sco_selected, .sco_modifying") + .forEach((cellule) => { + cellule.classList.remove("sco_selected", "sco_modifying"); + cellule.removeAttribute("contentEditable"); + cellule.removeEventListener("keydown", this.handleKeyCell); + }); + } + modifCell(event) { + let obj = event.currentTarget; + if (obj) { + obj.classList.add("sco_modifying"); + obj.contentEditable = true; + obj.addEventListener("keydown", this.handleKeyCell); + obj.focus(); } - modifCell(event) { - let obj = event.currentTarget; - if (obj) { - obj.classList.add("sco_modifying"); - obj.contentEditable = true; - obj.addEventListener("keydown", this.handleKeyCell); - obj.focus(); - } - } - key(event) { - switch (event.key) { - case "Enter": - this.modifCell(document.querySelector(".sco_selected")); - event.preventDefault(); - break; - } + } + key(event) { + switch (event.key) { + case "Enter": + this.modifCell(document.querySelector(".sco_selected")); + event.preventDefault(); + break; } - keyCell(event) { - let obj = event.currentTarget; - if (obj) { - if (event.key == "Enter") { - event.preventDefault(); - event.stopPropagation(); - if (!this.save(obj)) { - return - } - obj.classList.remove("sco_modifying"); - // ArrowMove(0, 1); - // modifCell(document.querySelector(".sco_selected")); - this.unselectCell(); - } + } + keyCell(event) { + let obj = event.currentTarget; + if (obj) { + if (event.key == "Enter") { + event.preventDefault(); + event.stopPropagation(); + if (!this.save(obj)) { + return; } + obj.classList.remove("sco_modifying"); + // ArrowMove(0, 1); + // modifCell(document.querySelector(".sco_selected")); + this.unselectCell(); + } } + } } function getCurrentScriptPath() { - // Get all the script elements on the page - var scripts = document.getElementsByTagName('script'); + // Get all the script elements on the page + var scripts = document.getElementsByTagName("script"); - // Find the last script element (which is the currently executing script) - var currentScript = scripts[scripts.length - 1]; + // Find the last script element (which is the currently executing script) + var currentScript = scripts[scripts.length - 1]; - // Retrieve the src attribute of the script element - var scriptPath = currentScript.src; + // Retrieve the src attribute of the script element + var scriptPath = currentScript.src; - return scriptPath; + return scriptPath; } function removeLastTwoComponents(path) { - // Split the path into individual components - var components = path.split('/'); + // Split the path into individual components + var components = path.split("/"); - // Remove the last two components (filename and enclosing directory) - components.splice(-2); + // Remove the last two components (filename and enclosing directory) + components.splice(-2); - // Join the remaining components back into a path - var newPath = components.join('/'); + // Join the remaining components back into a path + var newPath = components.join("/"); - return newPath; + return newPath; } diff --git a/app/tables/jury_recap.py b/app/tables/jury_recap.py index 3e6f5d24da1b26a81dc908b2c54528c77e524808..aca2491399ea67dd192e3ee29073c0ebdb5d2ecd 100644 --- a/app/tables/jury_recap.py +++ b/app/tables/jury_recap.py @@ -74,9 +74,8 @@ class TableJury(TableRecap): self.freq_codes_annuels[deca.code_valide] += 1 row.add_nb_rcues_cell() # --- Les RCUEs - 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(): + if dec_rcue.rcue.complete: row.add_rcue_cols(dec_rcue) self.freq_codes_annuels["total"] = len(self.rows) @@ -205,7 +204,7 @@ class RowJury(RowRecap): else: classes.append("moy_ue_valid") - if len(deca.rcues_annee) > 0: + if len(deca.get_decisions_rcues_annee()) > 0: # permet un tri par nb de niveaux validables + moyenne gen indicative S_pair if deca.res_pair and deca.etud.id in deca.res_pair.etud_moy_gen: moy = deca.res_pair.etud_moy_gen[deca.etud.id] @@ -260,9 +259,11 @@ class RowJury(RowRecap): def add_rcue_cols(self, dec_rcue: DecisionsProposeesRCUE): "2 cells: moyenne du RCUE, code enregistré" - self.table.group_titles["rcue"] = "RCUEs en cours" rcue = dec_rcue.rcue - col_id = f"moy_rcue_{rcue.ue_1.niveau_competence_id}" # le niveau_id + if not rcue.complete: + return + col_id = f"moy_rcue_{rcue.niveau.id}" # le niveau_id + self.table.group_titles["rcue"] = "RCUEs en cours" note_class = "" val = rcue.moy_rcue if isinstance(val, float): diff --git a/app/templates/but/documentation_codes_jury.j2 b/app/templates/but/documentation_codes_jury.j2 index 3ea9c80decaf7581d7567f56874d3d1a69bbc3f3..25d051e57becf2bea967297ebb59b328e2dcd859 100644 --- a/app/templates/but/documentation_codes_jury.j2 +++ b/app/templates/but/documentation_codes_jury.j2 @@ -130,6 +130,12 @@ <td class="amue">CODJ</td> <td>Acquis par décision du jury</td> </tr> + <tr> + <td>ADSUP</td> + <td>{{codes["ADSUP"]}}</td> + <td class="amue"></td> + <td>Acquis parce que le niveau de compétence supérieur est acquis</td> + </tr> <tr> <td>AJ</td> <td>{{codes["AJ"]}}</td> @@ -200,6 +206,12 @@ <td class="amue"></td> <td>Acquis par décision de jury sur le RCUE (ECTS acquis)</td> </tr> + <tr> + <td>ADSUP</td> + <td>{{codes["ADSUP"]}}</td> + <td class="amue"></td> + <td>Acquis parce que le niveau de compétence supérieur est acquis</td> + </tr> <tr> <td>AJ</td> <td>{{codes["AJ"]}}</td> diff --git a/app/templates/pn/ue_infos.j2 b/app/templates/pn/ue_infos.j2 index 071b1b8731e0c0f468e7645ca844291d8a1606d7..61ead8e01182579a70e8b3e9108e574b4f753848 100644 --- a/app/templates/pn/ue_infos.j2 +++ b/app/templates/pn/ue_infos.j2 @@ -13,6 +13,7 @@ <ul> <li>Semestre: {{ue.semestre_idx}}</li> <li>Code: <tt>{{ue.ue_code}}</tt></li> + <li>ECTS: <b>{{ue.ects or 0}}</b></li> <li>Type: {{ue.type}}</li> <li>Externe: {{ "oui" if ue.is_external else "non" }}</li> <li>Code Apogée: {{ue.code_apogee or "aucun"}}</li> diff --git a/app/views/notes.py b/app/views/notes.py index 3b1772982de5b56e833ad63d0ed3fce058aac10e..6483c510218b6bdde6807f111278b32dac5e2d03 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -2430,7 +2430,7 @@ def formsemestre_validation_but( ) deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre) - if len(deca.rcues_annee) == 0: + if len(deca.get_decisions_rcues_annee()) == 0: return jury_but_view.jury_but_semestriel( formsemestre, etud, read_only, navigation_div=navigation_div ) @@ -2459,22 +2459,25 @@ def formsemestre_validation_but( warning = "" if len(deca.niveaux_competences) != len(deca.decisions_rcue_by_niveau): - if deca.a_cheval: - warning += """<div class="warning">Attention: regroupements RCUE - entre années (redoublement).</div>""" - else: - warning += f"""<div class="warning">Attention: {len(deca.niveaux_competences)} + warning += f"""<div class="warning">Attention: {len(deca.niveaux_competences)} niveaux mais {len(deca.decisions_rcue_by_niveau)} regroupements RCUE.</div>""" if (deca.parcour is None) and len(formsemestre.parcours) > 0: warning += ( """<div class="warning">L'étudiant n'est pas inscrit à un parcours.</div>""" ) - if deca.formsemestre_impair and deca.inscription_etat_impair != scu.INSCRIT: - etat_ins = scu.ETATS_INSCRIPTION.get(deca.inscription_etat_impair, "inconnu?") - warning += f"""<div class="warning">{etat_ins} en S{deca.formsemestre_impair.semestre_id}</div>""" - if deca.formsemestre_pair and deca.inscription_etat_pair != scu.INSCRIT: - etat_ins = scu.ETATS_INSCRIPTION.get(deca.inscription_etat_pair, "inconnu?") - warning += f"""<div class="warning">{etat_ins} en S{deca.formsemestre_pair.semestre_id}</div>""" + + if deca.formsemestre_impair: + inscription = deca.formsemestre_impair.etuds_inscriptions.get(etud.id) + if (not inscription) or inscription.etat != scu.INSCRIT: + etat_ins = scu.ETATS_INSCRIPTION.get(inscription.etat, "inconnu?") + warning += f"""<div class="warning">{etat_ins} en S{deca.formsemestre_impair.semestre_id}</div>""" + + if deca.formsemestre_pair: + inscription = deca.formsemestre_pair.etuds_inscriptions.get(etud.id) + if (not inscription) or inscription.etat != scu.INSCRIT: + etat_ins = scu.ETATS_INSCRIPTION.get(inscription.etat, "inconnu?") + warning += f"""<div class="warning">{etat_ins} en S{deca.formsemestre_pair.semestre_id}</div>""" + if deca.has_notes_en_attente(): warning += f"""<div class="warning">{etud.nomprenom} a des notes en ATTente. Vous devriez régler cela avant de statuer en jury !</div>""" @@ -2531,7 +2534,7 @@ def formsemestre_validation_but( else: erase_span = f"""<a href="{ url_for("notes.formsemestre_jury_but_erase", - scodoc_dept=g.scodoc_dept, formsemestre_id=deca.formsemestre_id, + scodoc_dept=g.scodoc_dept, formsemestre_id=deca.formsemestre.id, etudid=deca.etud.id)}" class="stdlink" title="efface décisions issues des jurys de cette année" >effacer décisions de ce jury</a> @@ -2564,15 +2567,7 @@ def formsemestre_validation_but( ) H.append(navigation_div) H.append("</form>") - if deca.a_cheval: - H.append( - f"""<div class="but_doc_codes but_warning_rcue_cap"> - {scu.EMO_WARNING} Rappel: pour les redoublants, seules les UE <b>capitalisées</b> (note > 10) - lors d'une année précédente peuvent être prise en compte pour former - un RCUE (associé à un niveau de compétence du BUT). - </div> - """ - ) + # Affichage cursus BUT but_cursus = cursus_but.EtudCursusBUT(etud, deca.formsemestre.formation) H += [ @@ -2595,7 +2590,14 @@ def formsemestre_validation_but( codes=ScoDocSiteConfig.get_codes_apo_dict(), ) ) - + H.append( + f"""<div class="but_doc_codes but_warning_rcue_cap"> + {scu.EMO_WARNING} Rappel: pour les redoublants, seules les UE <b>capitalisées</b> (note > 10) + lors d'une année précédente peuvent être prise en compte pour former + un RCUE (associé à un niveau de compétence du BUT). + </div> + """ + ) return "\n".join(H) + html_sco_header.sco_footer() diff --git a/sco_version.py b/sco_version.py index 282f517e7f105f148f3d89b177fcdb04785748b5..aa68bec3df11ed603a081dd84c417c1654f74c6a 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.4.93" +SCOVERSION = "9.4.94" SCONAME = "ScoDoc"