diff --git a/app/but/cursus_but.py b/app/but/cursus_but.py index 89a7bd669e..d8ca17e154 100644 --- a/app/but/cursus_but.py +++ b/app/but/cursus_but.py @@ -14,6 +14,7 @@ Classe raccordant avec ScoDoc 7: """ import collections +from operator import attrgetter from typing import Union from flask import g, url_for @@ -23,8 +24,6 @@ from app import log from app.comp.res_but import ResultatsSemestreBUT from app.comp.res_compat import NotesTableCompat -from app.comp import res_sem - from app.models.but_refcomp import ( ApcAnneeParcours, ApcCompetence, @@ -45,7 +44,7 @@ from app.models.formsemestre import FormSemestre, FormSemestreInscription from app.models.ues import UniteEns from app.models.validations import ScolarFormSemestreValidation from app.scodoc import codes_cursus as sco_codes -from app.scodoc.codes_cursus import RED, UE_STANDARD +from app.scodoc.codes_cursus import code_ue_validant, RED, UE_STANDARD from app.scodoc import sco_utils as scu from app.scodoc.sco_exceptions import ScoNoReferentielCompetences, ScoValueError @@ -360,6 +359,40 @@ class FormSemestreCursusBUT: "cache { competence_id : competence }" +def etud_ues_de_but1_non_validees( + etud: Identite, formation: Formation, parcour: ApcParcours +) -> list[UniteEns]: + """Vrai si cet étudiant a validé toutes ses UEs de S1 et S2, dans son parcours""" + # Les UEs avec décisions, dans les S1 ou S2 d'une formation de même code: + validations = ( + ScolarFormSemestreValidation.query.filter_by(etudid=etud.id) + .filter(ScolarFormSemestreValidation.ue_id != None) + .join(UniteEns) + .filter(db.or_(UniteEns.semestre_idx == 1, UniteEns.semestre_idx == 2)) + .join(Formation) + .filter_by(formation_code=formation.formation_code) + ) + codes_validations_by_ue = collections.defaultdict(list) + for v in validations: + codes_validations_by_ue[v.ue_id].append(v.code) + + # Les UEs du parcours en S1 et S2: + ues = formation.query_ues_parcour(parcour).filter( + db.or_(UniteEns.semestre_idx == 1, UniteEns.semestre_idx == 2) + ) + # Liste triée des ues non validées + return sorted( + [ + ue + for ue in ues + if any( + (not code_ue_validant(code) for code in codes_validations_by_ue[ue.id]) + ) + ], + key=attrgetter("numero", "acronyme"), + ) + + def formsemestre_warning_apc_setup( formsemestre: FormSemestre, res: ResultatsSemestreBUT ) -> str: diff --git a/app/but/jury_but.py b/app/but/jury_but.py index 227ead81c4..c1ded7b883 100644 --- a/app/but/jury_but.py +++ b/app/but/jury_but.py @@ -69,6 +69,7 @@ from flask import flash, g, url_for from app import db from app import log +from app.but import cursus_but from app.but.cursus_but import EtudCursusBUT from app.comp.res_but import ResultatsSemestreBUT from app.comp import res_sem @@ -363,15 +364,33 @@ class DecisionsProposeesAnnee(DecisionsProposees): "Vrai si plus de la moitié des RCUE validables" 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" - # XXX TODO ajouter condition pour passage en S5 + explanation = "" + # 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 + ) + self.passage_de_droit = not ues_but1_non_validees + explanation += ( + f"""UEs de BUT1 non validées: { + ', '.join(ue.acronyme for ue in ues_but1_non_validees) + }. """ + if ues_but1_non_validees + else "" + ) + else: + # pas inscrit dans le semestre courant ??? + self.passage_de_droit = False - # Enfin calcule les codes des UE: + # Enfin calcule les codes des UEs: for dec_ue in self.decisions_ues.values(): dec_ue.compute_codes() # Reste à attribuer ADM, ADJ, PASD, PAS1NCI, RED, NAR plural = self.nb_validables > 1 - expl_rcues = f"""{self.nb_validables} niveau{"x" if plural else ""} validable{ + explanation += f"""{self.nb_validables} niveau{"x" if plural else ""} validable{ "s" if plural else ""} sur {self.nb_competences}""" if self.admis: self.codes = [sco_codes.ADM] + self.codes @@ -390,7 +409,7 @@ class DecisionsProposeesAnnee(DecisionsProposees): sco_codes.ABL, sco_codes.EXCLU, ] - expl_rcues = "" + explanation = "" elif self.passage_de_droit: self.codes = [sco_codes.PASD, sco_codes.ADJ] + self.codes elif self.valide_moitie_rcue: # mais au moins 1 rcue insuffisante @@ -400,7 +419,7 @@ class DecisionsProposeesAnnee(DecisionsProposees): sco_codes.PAS1NCI, sco_codes.ADJ, ] + self.codes - expl_rcues += f" et {self.nb_rcues_under_8} < 8" + explanation += f" et {self.nb_rcues_under_8} < 8" else: self.codes = [ sco_codes.RED, @@ -409,7 +428,7 @@ class DecisionsProposeesAnnee(DecisionsProposees): sco_codes.ADJ, sco_codes.PASD, # voir #488 (discutable, conventions locales) ] + self.codes - expl_rcues += 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 ( @@ -419,7 +438,7 @@ class DecisionsProposeesAnnee(DecisionsProposees): # Si validée par niveau supérieur: if self.code_valide == sco_codes.ADSUP: self.codes.insert(0, sco_codes.ADSUP) - self.explanation = f"
{expl_rcues}
" + self.explanation = f"
{explanation}
" messages = self.descr_pb_coherence() if messages: self.explanation += ( @@ -1261,6 +1280,8 @@ class DecisionsProposeesRCUE(DecisionsProposees): validation_rcue = validations_rcue[0] validation_rcue.code = sco_codes.ADSUP validation_rcue.date = datetime.now() + db.session.add(validation_rcue) + db.session.commit() log(f"updating {validation_rcue}") if validation_rcue.formsemestre_id is not None: sco_cache.invalidate_formsemestre( @@ -1271,9 +1292,9 @@ class DecisionsProposeesRCUE(DecisionsProposees): 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}") - db.session.add(validation_rcue) - db.session.commit() self.valide_annee_inferieure() def valide_annee_inferieure(self) -> None: diff --git a/app/models/validations.py b/app/models/validations.py index cc55651878..229d15ad54 100644 --- a/app/models/validations.py +++ b/app/models/validations.py @@ -11,7 +11,7 @@ from app.models.events import Scolog class ScolarFormSemestreValidation(db.Model): - """Décisions de jury""" + """Décisions de jury (sur semestre ou UEs)""" __tablename__ = "scolar_formsemestre_validation" # Assure unicité de la décision: diff --git a/tests/ressources/yaml/cursus_but_gccd_cy.yaml b/tests/ressources/yaml/cursus_but_gccd_cy.yaml index 668951d2ca..8852a36436 100644 --- a/tests/ressources/yaml/cursus_but_gccd_cy.yaml +++ b/tests/ressources/yaml/cursus_but_gccd_cy.yaml @@ -255,3 +255,26 @@ Etudiants: parcours: TP notes_modules: "R4.01": 14 # toutes UE + + D: # Etudiant arrive en S4 avec une UE manquante en S1 + prenom: Étudiant_TP_malaise + civilite: M + formsemestres: + S1: + parcours: TP + notes_modules: + "R1.01": 11 # toutes UEs + "SAÉ 1-2": 8 # plombe l'UE 2 + S2: + parcours: TP + notes_modules: + "R2.01": 11 # toutes UEs + S3: + parcours: TP + notes_modules: + "R3.01": 12 # toutes UEs + S4: + parcours: TP + notes_modules: + "R4.01": 14 # toutes UE + "R4.04": 6 # plombe l'UE1 diff --git a/tests/unit/yaml_setup.py b/tests/unit/yaml_setup.py index f24b92b79a..35b016a2f1 100644 --- a/tests/unit/yaml_setup.py +++ b/tests/unit/yaml_setup.py @@ -146,7 +146,7 @@ def create_formsemestre( return formsemestre -def create_evaluations(formsemestre: FormSemestre): +def create_evaluations(formsemestre: FormSemestre, publish_incomplete=True): """Crée une évaluation dans chaque module du semestre""" for modimpl in formsemestre.modimpls: evaluation = Evaluation( @@ -156,6 +156,7 @@ def create_evaluations(formsemestre: FormSemestre): coefficient=1.0, note_max=20.0, numero=1, + publish_incomplete=publish_incomplete, ) db.session.add(evaluation)