From 495aac82aea54b28ae29845a1cf5abf0ef9bd6b7 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sat, 7 Sep 2024 23:55:27 +0200 Subject: [PATCH] =?UTF-8?q?Jury=20BUT:=20cas=20avec=20redoublement=20en=20?= =?UTF-8?q?changeant=20de=20parcours.=20Fix=20#988=20:=20d=C3=A9terminatio?= =?UTF-8?q?n=20parcours,=20compte=20ECTS,=20DUT120.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/but/cursus_but.py | 74 +++++++++++++++++++-------- app/but/jury_but_pv.py | 12 +++-- app/but/jury_dut120.py | 23 ++++++--- app/but/validations_view.py | 44 +++++++++++----- app/tables/jury_recap.py | 13 +++-- app/templates/but/validate_dut120.j2 | 19 ++++--- app/templates/but/validation_rcues.j2 | 4 +- app/views/jury_validations.py | 2 +- 8 files changed, 129 insertions(+), 62 deletions(-) diff --git a/app/but/cursus_but.py b/app/but/cursus_but.py index 0abaf31aa..89bcb9f96 100644 --- a/app/but/cursus_but.py +++ b/app/but/cursus_but.py @@ -64,7 +64,7 @@ class SituationEtudCursusBUT(sco_cursus_dut.SituationEtudCursusClassic): Considère le parcours du semestre en cours (res). """ parcour_id = self.nt.etuds_parcour_id.get(self.etud.id) - return but_parcours_validated(self.etud.id, parcour_id) + return but_parcours_validated(self.etud, parcour_id) def but_annee_validated( @@ -81,14 +81,14 @@ def but_annee_validated( ) -def but_parcours_validated(etudid: int, parcour_id: int | None) -> bool: +def but_parcours_validated(etud: Identite, parcour_id: int | None) -> bool: """Détermine si le parcours BUT est validé. = 180 ECTS acquis dans les UEs du parcours. """ if parcour_id is None: return False # étudiant non inscrit à un parcours # Les ECTS - validations = but_validations_ues_parcours(etudid, parcour_id) + validations = but_validations_ues_parcours(etud, parcour_id) ects_acquis = validations_count_ects(validations) return ects_acquis >= CursusBUT.ECTS_DIPLOME @@ -113,20 +113,10 @@ class EtudCursusBUT: # self.etud = etud self.formation = formation - self.inscriptions = sorted( - [ - ins - for ins in etud.formsemestre_inscriptions - if ins.formsemestre.formation.referentiel_competence - and ( - ins.formsemestre.formation.referentiel_competence.id - == formation.referentiel_competence.id - ) - ], - key=lambda s: (s.formsemestre.semestre_id, s.formsemestre.date_debut), - ) "Liste des inscriptions aux sem. de la formation, triées par indice et chronologie" - self.parcour: ApcParcours = self.inscriptions[-1].parcour + self.parcour: ApcParcours = get_etud_parcours( + etud, formation.referentiel_competence_id + ) "Le parcours à valider: celui du DERNIER semestre suivi (peut être None)" self.niveaux_by_annee: dict[int, list[ApcNiveau]] = {} "{ annee:int : liste des niveaux à valider }" @@ -229,8 +219,12 @@ class EtudCursusBUT: return annee in [n.annee for n in self.competences[competence_id].niveaux] def get_ects_acquis(self) -> int: - "Nombre d'ECTS validés par etud dans le BUT de ce référentiel" - return but_ects_valides(self.etud, self.formation.referentiel_competence.id) + "Nombre d'ECTS validés par etud dans le parcours BUT de ce référentiel" + return but_ects_valides( + self.etud, + self.formation.referentiel_competence.id, + parcour_id=self.parcour.id if self.parcour is not None else None, + ) def load_validation_by_niveau(self) -> dict[int, list[ApcValidationRCUE]]: """Cherche les validations de jury enregistrées pour chaque niveau @@ -403,17 +397,48 @@ class FormSemestreCursusBUT: # "cache { competence_id : competence }" +def get_etud_parcours( + etud: Identite, referentiel_competence_id: int | None +) -> ApcParcours | None: + """Le parcours de l'étudiant dans ce réf. de compétence, ou None. + = celui du DERNIER semestre suivi dans le référentiel de compétence + (peut être None si l'incription n'a pas de parcours) + """ + inscriptions = sorted( + [ + ins + for ins in etud.formsemestre_inscriptions + if ins.formsemestre.formation.referentiel_competence + and ( + ins.formsemestre.formation.referentiel_competence.id + == referentiel_competence_id + ) + ], + key=lambda s: (s.formsemestre.date_debut, s.formsemestre.semestre_id), + ) + return inscriptions[-1].parcour if inscriptions else None + + def but_ects_valides( etud: Identite, - referentiel_competence_id: int, + referentiel_competence_id: int | None = None, annees_but: None | Iterable[str] = None, + parcour_id: int | None = None, ) -> int: """Nombre d'ECTS validés par etud dans le BUT de référentiel indiqué. Ne prend que les UE associées à des niveaux de compétences, et ne les compte qu'une fois même en cas de redoublement avec re-validation. + + On peut spécifier soit le referentiel_competence_id, soit le parcour. + Si parcour est spécifié, ne prend que les UEs de ce parcours et du tronc commun. + Si annees_but est spécifié, un iterable "BUT1, "BUT2" par exemple, ne prend que ces années. """ - validations = but_validations_ues(etud, referentiel_competence_id, annees_but) + validations = ( + but_validations_ues_parcours(etud, parcour_id, annees_but) + if parcour_id is not None + else but_validations_ues(etud, referentiel_competence_id, annees_but) + ) return validations_count_ects(validations) @@ -472,7 +497,7 @@ def sorted_validations(validations) -> list[ScolarFormSemestreValidation]: def but_validations_ues_parcours( - etudid: int, parcour_id: int + etud: Identite, parcour_id: int, annees_but: None | Iterable[str] = None ) -> list[ScolarFormSemestreValidation]: """Query les validations d'UEs pour cet étudiant dans des UEs appartenant à ce parcours ou à son tronc commun. @@ -485,7 +510,7 @@ def but_validations_ues_parcours( # Les validations d'UE de ce parcours ou du tronc commun pour cet étudiant: validations = ( - ScolarFormSemestreValidation.query.filter_by(etudid=etudid) + ScolarFormSemestreValidation.query.filter_by(etudid=etud.id) .filter(ScolarFormSemestreValidation.ue_id != None) .join(UniteEns) .filter( @@ -499,6 +524,11 @@ def but_validations_ues_parcours( ) ) ) + # restreint à certaines années (utile pour les ECTS du DUT120) + if annees_but: + validations = validations.join(ApcNiveau).filter( + ApcNiveau.annee.in_(annees_but) + ) return sorted_validations(validations) diff --git a/app/but/jury_but_pv.py b/app/but/jury_but_pv.py index cdb46acd7..80f1b1131 100644 --- a/app/but/jury_but_pv.py +++ b/app/but/jury_but_pv.py @@ -11,8 +11,7 @@ from flask import g, request, url_for from openpyxl.styles import Alignment from app import log -from app.but import jury_but -from app.but.cursus_but import but_ects_valides +from app.but import cursus_but, jury_but from app.models.but_validations import ValidationDUT120 from app.models.etudiants import Identite from app.models.formsemestre import FormSemestre @@ -155,7 +154,14 @@ def pvjury_table_but( except ScoValueError: deca = None - ects_but_valides = but_ects_valides(etud, referentiel_competence_id) + parcour = cursus_but.get_etud_parcours( + etud, formsemestre.formation.referentiel_competence_id + ) + ects_but_valides = ( + cursus_but.but_ects_valides(etud, parcour_id=parcour.id) + if parcour + else cursus_but.but_ects_valides(etud, referentiel_competence_id) + ) has_diplome = deca.valide_diplome() if deca else False diplome_lst = ["ADM"] if has_diplome else [] validation_dut120 = ValidationDUT120.query.filter_by( diff --git a/app/but/jury_dut120.py b/app/but/jury_dut120.py index de272d8c0..ffe4bdc63 100644 --- a/app/but/jury_dut120.py +++ b/app/but/jury_dut120.py @@ -27,12 +27,22 @@ from app.views import ScoData def etud_valide_dut120(etud: Identite, referentiel_competence_id: int) -> bool: """Vrai si l'étudiant satisfait les conditions pour valider le DUT120""" - ects_but1_but2 = cursus_but.but_ects_valides( - etud, referentiel_competence_id, annees_but=("BUT1", "BUT2") - ) + ects_but1_but2 = etud_ects_but1_but2(etud, referentiel_competence_id) return ects_but1_but2 >= 120 +def etud_ects_but1_but2(etud, referentiel_competence_id: int) -> float: + """Les ECTS enregistré en BUT1 et BUT2 de ce ref. et du parcours""" + # parcours de la dernière inscription + parcour = cursus_but.get_etud_parcours(etud, referentiel_competence_id) + return cursus_but.but_ects_valides( + etud, + referentiel_competence_id, + parcour_id=parcour.id if parcour else None, + annees_but=("BUT1", "BUT2"), + ) + + class ValidationDUT120Form(FlaskForm): "Formulaire validation DUT120" submit = SubmitField("Enregistrer le diplôme DUT 120") @@ -61,16 +71,13 @@ def validate_dut120_etud(etudid: int, formsemestre_id: int): formsemestre_id=formsemestre_id, ) ) - - ects_but1_but2 = cursus_but.but_ects_valides( - etud, refcomp.id, annees_but=("BUT1", "BUT2") - ) - + ects_but1_but2 = etud_ects_but1_but2(etud, refcomp.id) form = ValidationDUT120Form() # Check if ValidationDUT120 instance already exists existing_validation = ValidationDUT120.query.filter_by( etudid=etud.id, referentiel_competence_id=refcomp.id ).first() + if existing_validation: flash("DUT120 déjà validé", "info") etud_can_validate_dut = False diff --git a/app/but/validations_view.py b/app/but/validations_view.py index d05857c2a..3a8c01ff6 100644 --- a/app/but/validations_view.py +++ b/app/but/validations_view.py @@ -9,7 +9,7 @@ from flask import render_template -from app import log +from app import db, log from app.but import cursus_but from app.models import ( ApcCompetence, @@ -20,10 +20,11 @@ from app.models import ( Formation, FormSemestre, Identite, - UniteEns, # ScolarAutorisationInscription, ScolarFormSemestreValidation, + UniteEns, ) +from app.models.ues import UEParcours from app.scodoc import codes_cursus from app.scodoc.sco_exceptions import ScoNoReferentielCompetences, ScoValueError from app.views import ScoData @@ -49,7 +50,9 @@ def validation_rcues(etud: Identite, formsemestre: FormSemestre, edit: bool = Fa ) ue_validation_by_niveau = get_ue_validation_by_niveau(refcomp, etud) - rcue_validation_by_niveau = get_rcue_validation_by_niveau(refcomp, etud) + rcue_validation_by_niveau = get_rcue_validation_by_niveau( + refcomp, etud, None if parcour is None else parcour.id + ) ects_acquis = sum((v.ects() for v in ue_validation_by_niveau.values())) return render_template( @@ -71,17 +74,18 @@ def validation_rcues(etud: Identite, formsemestre: FormSemestre, edit: bool = Fa def get_ue_validation_by_niveau( refcomp: ApcReferentielCompetences, etud: Identite ) -> dict[tuple[int, str], ScolarFormSemestreValidation]: - """Les validations d'UEs de cet étudiant liées à ce référentiel de compétences. - Pour chaque niveau / pair ou impair, choisi la "meilleure" validation + """Les validations d'UEs de cet étudiant liées à ce référentiel de compétences + dans le parcours suivi par l'étudiant (celui de son semestre le plus récent + dans un semestre de ce référentiel). + Pour chaque niveau / pair ou impair, choisi la "meilleure" validation. """ - validations: list[ScolarFormSemestreValidation] = ( - ScolarFormSemestreValidation.query.filter_by(etudid=etud.id) - .join(UniteEns) - .join(ApcNiveau) - .join(ApcCompetence) - .filter_by(referentiel_id=refcomp.id) - .all() + parcour = cursus_but.get_etud_parcours(etud, refcomp.id) + validations = ( + cursus_but.but_validations_ues_parcours(etud, parcour.id) + if parcour is not None + else cursus_but.but_validations_ues(etud, refcomp.id) ) + # La meilleure validation pour chaque UE ue_validation_by_niveau = {} # { (niveau_id, pair|impair) : validation } for validation in validations: @@ -104,10 +108,12 @@ def get_ue_validation_by_niveau( def get_rcue_validation_by_niveau( - refcomp: ApcReferentielCompetences, etud: Identite + refcomp: ApcReferentielCompetences, etud: Identite, parcour_id: int | None ) -> dict[int, ApcValidationRCUE]: """Les validations d'UEs de cet étudiant liées à ce référentiel de compétences. - Pour chaque niveau / pair ou impair, choisi la "meilleure" validation + Si parcour_id n'est pas None, restreint aux niveaux de ce parcours + et du tronc commun. + Pour chaque niveau / pair ou impair, choisi la "meilleure" validation. """ validations: list[ApcValidationRCUE] = ( ApcValidationRCUE.query.filter_by(etudid=etud.id) @@ -115,6 +121,16 @@ def get_rcue_validation_by_niveau( .join(ApcNiveau, UniteEns.niveau_competence_id == ApcNiveau.id) .join(ApcCompetence) .filter_by(referentiel_id=refcomp.id) + .filter( + db.or_( + UniteEns.id.in_( + UEParcours.query.with_entities(UEParcours.ue_id).filter( + UEParcours.parcours_id == parcour_id + ) + ), + ~UniteEns.id.in_(UEParcours.query.with_entities(UEParcours.ue_id)), + ) + ) .all() ) return { diff --git a/app/tables/jury_recap.py b/app/tables/jury_recap.py index 56d27e4e3..8189f978c 100644 --- a/app/tables/jury_recap.py +++ b/app/tables/jury_recap.py @@ -113,7 +113,7 @@ class TableJury(TableRecap): if res.is_apc and res.formsemestre.semestre_id == 6: # on ne vérifie le diplôme que dans ce cas pour ne pas ralentir if cursus_but.but_parcours_validated( - etud.id, res.etuds_parcour_id.get(etud.id) + etud, res.etuds_parcour_id.get(etud.id) ): row.add_cell( "autorisations_inscription", @@ -143,15 +143,20 @@ class TableJury(TableRecap): group="jury_code_sem", classes=["recorded_code"], ) - # ECTS acquis en BUT + # ECTS acquis en BUT (dans le parcours du semestre affiché) if res.formsemestre.formation.referentiel_competence_id: + parcour_id = res.etuds_parcour_id.get(etud.id) row.add_cell( "ects_acquis", "ECTS", # res.get_etud_ects_valides(etud.id), # cette recherche augmente de 10% le temps de construction de la table - cursus_but.but_ects_valides( - etud, res.formsemestre.formation.referentiel_competence_id + ( + cursus_but.but_ects_valides(etud, parcour_id=parcour_id) + if parcour_id is not None + else cursus_but.but_ects_valides( + etud, res.formsemestre.formation.referentiel_competence_id + ) ), group="jury_code_sem", classes=["recorded_code"], diff --git a/app/templates/but/validate_dut120.j2 b/app/templates/but/validate_dut120.j2 index e06f04faa..9bc48a869 100644 --- a/app/templates/but/validate_dut120.j2 +++ b/app/templates/but/validate_dut120.j2 @@ -10,7 +10,7 @@
-
Validation du DUT en 120 ECTS dans un parcours BUT
+

Validation du DUT en 120 ECTS dans un parcours BUT

{{etud.html_link_fiche()|safe}}
@@ -20,7 +20,7 @@
-
+

Les étudiants de BUT peuvent demander l’attribution du diplôme universitaire de technologie (DUT) au terme de l’acquisition des 120 premiers crédits européens du cursus.

@@ -29,12 +29,13 @@ une formation utilisant une autre version de référentiel, pensez à revalider

-
- {{etud.html_link_fiche()|safe}} a acquis {{ects_but1_but2}} ECTS en BUT1 et BUT2. - {% if not validation %} - Son DUT n'est pas encore enregistré dans cette spécialité. - {% endif %} -
+
+
+ {{etud.html_link_fiche()|safe}} a acquis {{ects_but1_but2}} ECTS en BUT1 et BUT2. + {% if not validation %} + Son DUT n'est pas encore enregistré dans cette spécialité. + {% endif %} +
{% if etud_can_validate_dut %}
@@ -65,4 +66,6 @@ une formation utilisant une autre version de référentiel, pensez à revalider {% endif %} {% endif %} +
+ {% endblock %} diff --git a/app/templates/but/validation_rcues.j2 b/app/templates/but/validation_rcues.j2 index 6dc4b39f0..21097b169 100644 --- a/app/templates/but/validation_rcues.j2 +++ b/app/templates/but/validation_rcues.j2 @@ -52,7 +52,7 @@ {% if parcour %} parcours {{parcour.code}} « {{parcour.libelle}} » {% else %} - non inscrit{{sco.etud.e}} à un parcours de la spécialité + non inscrit{{sco.etud.e}} à un parcours de la spécialité {% endif %}
@@ -164,7 +164,7 @@ {% endif %} -
+

Cette page montre les validations d'UEs et de niveaux de compétences (RCUEs) de {{sco.etud.html_link_fiche()|safe}} diff --git a/app/views/jury_validations.py b/app/views/jury_validations.py index 1bc8fd0fd..4ecdb443e 100644 --- a/app/views/jury_validations.py +++ b/app/views/jury_validations.py @@ -920,7 +920,7 @@ def jury_delete_manual(etudid: int): @scodoc @permission_required(Permission.ScoView) def etud_bilan_ects(etudid: int): - """Page bilan de tous els ECTS acquis par un étudiant. + """Page bilan de tous les ECTS acquis par un étudiant. Plusieurs formations (eg DUT, LP) peuvent être concernées. """ etud = Identite.get_etud(etudid)