From 6357dd999d1702a07310a858cc4cdac6e2b48aba Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 13 Apr 2023 09:58:38 +0200 Subject: [PATCH] =?UTF-8?q?Association=20parcours/UE:=20am=C3=A9lioration?= =?UTF-8?q?=20formulaire.=20Messages=20erreurs.=20Logique=20association=20?= =?UTF-8?q?UE/niveaux.=20test=20unitaire=20partiel.=20WIP.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/formations.py | 4 +- app/models/ues.py | 47 +++++++++---- app/static/css/scodoc.css | 22 ++++++- app/templates/but/parcour_formation.j2 | 2 +- app/views/but_formation.py | 91 +++++++++++++++----------- tests/unit/test_cursus_but.py | 77 ++++++++++++++++++++++ 6 files changed, 187 insertions(+), 56 deletions(-) create mode 100644 tests/unit/test_cursus_but.py diff --git a/app/api/formations.py b/app/api/formations.py index e9f200a9..3274cb22 100644 --- a/app/api/formations.py +++ b/app/api/formations.py @@ -346,10 +346,10 @@ def assoc_ue_niveau(ue_id: int, niveau_id: int): ok, error_message = ue.set_niveau_competence(niveau) if not ok: if g.scodoc_dept: # "usage web" - flash(error_message) + flash(error_message, "error") return json_error(404, error_message) if g.scodoc_dept: # "usage web" - flash(f"UE {ue.acronyme} associée au niveau {niveau.libelle}") + flash(f"""{ue.acronyme} associée au niveau "{niveau.libelle}" """) return {"status": 0} diff --git a/app/models/ues.py b/app/models/ues.py index a4385880..e301530b 100644 --- a/app/models/ues.py +++ b/app/models/ues.py @@ -280,10 +280,14 @@ class UniteEns(db.Model): and ue.niveau_competence_id == niveau.id ] if ues_meme_niveau: + msg_parc = f"parcours {code_parcour}" if parcour else "tronc commun" if len(ues_meme_niveau) > 1: # deja 2 UE sur ce niveau msg = f"""Niveau "{ - niveau.libelle}" déjà associé à deux UE du parcours {code_parcour}""" - log("check_niveau_unique_dans_parcours: " + msg) + niveau.libelle}" déjà associé à deux UE du {msg_parc}""" + log( + f"check_niveau_unique_dans_parcours(niveau_id={niveau.id}): " + + msg + ) return False, msg # s'il y a déjà une UE associée à ce niveau, elle doit être dans l'autre semestre # de la même année scolaire @@ -292,8 +296,11 @@ class UniteEns(db.Model): ) if ues_meme_niveau[0].semestre_idx != other_semestre_idx: msg = f"""Niveau "{ - niveau.libelle}" associé à une autre année du parcours {code_parcour}""" - log("check_niveau_unique_dans_parcours: " + msg) + niveau.libelle}" associé à une autre année du {msg_parc}""" + log( + f"check_niveau_unique_dans_parcours(niveau_id={niveau.id}): " + + msg + ) return False, msg return True, "" @@ -314,16 +321,30 @@ class UniteEns(db.Model): False, "La formation n'est pas associée à un référentiel de compétences", ) - if niveau.competence.referentiel.id != self.formation.referentiel_competence.id: - return False, "Le niveau n'appartient pas au référentiel de la formation" - if niveau.id == self.niveau_competence_id: + if niveau is not None: + if self.niveau_competence_id is not None: + return ( + False, + f"{self.acronyme} déjà associée à un niveau de compétences", + ) + if ( + niveau.competence.referentiel.id + != self.formation.referentiel_competence.id + ): + return ( + False, + "Le niveau n'appartient pas au référentiel de la formation", + ) + if niveau.id == self.niveau_competence_id: + return True, "" # nothing to do + if self.niveau_competence_id is not None: + ok, error_message = self.check_niveau_unique_dans_parcours( + niveau, self.parcours + ) + if not ok: + return ok, error_message + elif self.niveau_competence_id is None: return True, "" # nothing to do - if (niveau is not None) and (self.niveau_competence_id is not None): - ok, error_message = self.check_niveau_unique_dans_parcours( - niveau, self.parcours - ) - if not ok: - return ok, error_message self.niveau_competence = niveau db.session.add(self) db.session.commit() diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index c8f8843b..798b3178 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -64,6 +64,26 @@ div#gtrcontent { display: None; } +div.alert { + z-index: 9; + position: absolute; + top: 10px; + right: 10px; +} + +div.alert-info { + color: #0019d7; + background-color: #68f36d; + border-color: #0a8d0c; +} + +div.alert-error { + color: #ef0020; + background-color: #ffff00; + border-color: #8d0a17; +} + + div.tab-content { margin-top: 10px; margin-left: 15px; @@ -938,7 +958,7 @@ span.linktitresem a:visited { a.stdlink, a.stdlink:visited { - color: #0e0e9d; + color: blue; text-decoration: underline; } diff --git a/app/templates/but/parcour_formation.j2 b/app/templates/but/parcour_formation.j2 index 04ad6e70..e46bba2a 100644 --- a/app/templates/but/parcour_formation.j2 +++ b/app/templates/but/parcour_formation.j2 @@ -177,7 +177,7 @@ function assoc_ue_niveau(event, niveau_id) { .then(response => response.json()) .then(data => { if (data.status) { - sco_message(data.message); + /* sco_message(data.message); */ /* revert menu to initial state */ event.target.value = event.target.dataset.ue_id; } diff --git a/app/views/but_formation.py b/app/views/but_formation.py index 44b66d74..d15d90c8 100644 --- a/app/views/but_formation.py +++ b/app/views/but_formation.py @@ -87,6 +87,21 @@ def parcour_formation(formation_id: int, parcour_id: int = None) -> str: ) +def ue_associee_au_niveau_du_parcours( + ues_possibles: list[UniteEns], niveau: ApcNiveau, sem_name: str = "S" +) -> UniteEns: + "L'UE associée à ce niveau, ou None" + ues = [ue for ue in ues_possibles if ue.niveau_competence_id == niveau.id] + if len(ues) > 1: + # plusieurs UEs associées à ce niveau: élimine celles sans parcours + ues_pair_avec_parcours = [ue for ue in ues if ue.parcours] + if ues_pair_avec_parcours: + ues = ues_pair_avec_parcours + if len(ues) > 1: + log(f"_niveau_ues: {len(ues)} associées au niveau {niveau} / {sem_name}") + return ues[0] if ues else None + + def parcour_formation_competences(parcour: ApcParcours, formation: Formation) -> list: """ [ @@ -108,59 +123,57 @@ def parcour_formation_competences(parcour: ApcParcours, formation: Formation) -> """ def _niveau_ues(competence: ApcCompetence, annee: int) -> dict: - "niveau et ues pour l'année du parcours" + "niveau et ues pour cette compétence de cette année du parcours" niveaux = ApcNiveau.niveaux_annee_de_parcours( parcour, annee, competence=competence ) if len(niveaux) > 0: if len(niveaux) > 1: - log(f"_niveau_ues: plus d'un niveau pour {competence} annee {annee}") + log( + f"""_niveau_ues: plus d'un niveau pour {competence} + annee {annee} parcours {parcour.code}""" + ) niveau = niveaux[0] elif len(niveaux) == 0: - return {"niveau": None, "ue_pair": None, "ue_impair": None} - # toutes les UEs de la formation associées à ce niveau + return { + "niveau": None, + "ue_pair": None, + "ue_impair": None, + "ues_pair": [], + "ues_impair": [], + } + # Toutes les UEs de la formation dans ce parcours ou tronc commun ues = [ ue - for ue in niveau.ues - if ue.formation.id == formation.id - # and parcour.id in (p.id for p in ue.parcours) - ] - ues_pair = [ue for ue in ues if ue.semestre_idx == 2 * annee] - if len(ues_pair) > 0: - ue_pair = ues_pair[0] - if len(ues_pair) > 1: - log( - f"_niveau_ues: {len(ues)} associées au niveau {niveau} / S{2*annee}" - ) - else: - ue_pair = None - ues_pair_possibles = [ - ue - for ue in formation.ues.filter_by(semestre_idx=2 * annee, type=UE_STANDARD) - if (ue.niveau_competence is None) or (ue.niveau_competence_id == niveau.id) - ] - ues_impair = [ue for ue in ues if ue.semestre_idx == (2 * annee - 1)] - if len(ues_impair) > 0: - ue_impair = ues_impair[0] - if len(ues_impair) > 1: - log( - f"_niveau_ues: {len(ues)} associées au niveau {niveau} / S{2*annee-1}" - ) - else: - ue_impair = None - ues_impair_possibles = [ - ue - for ue in formation.ues.filter_by( - semestre_idx=2 * annee - 1, type=UE_STANDARD - ) - if (ue.niveau_competence is None) or (ue.niveau_competence_id == niveau.id) + for ue in formation.ues + if ((not ue.parcours) or (parcour.id in (p.id for p in ue.parcours))) + and ue.type == UE_STANDARD ] + ues_pair_possibles = [ue for ue in ues if ue.semestre_idx == (2 * annee)] + ues_impair_possibles = [ue for ue in ues if ue.semestre_idx == (2 * annee - 1)] + + # UE associée au niveau dans ce parcours + ue_pair = ue_associee_au_niveau_du_parcours( + ues_pair_possibles, niveau, f"S{2*annee}" + ) + ue_impair = ue_associee_au_niveau_du_parcours( + ues_impair_possibles, niveau, f"S{2*annee-1}" + ) + return { "niveau": niveau, "ue_pair": ue_pair, - "ues_pair": ues_pair_possibles, + "ues_pair": [ + ue + for ue in ues_pair_possibles + if (not ue.niveau_competence) or ue.niveau_competence.id == niveau.id + ], "ue_impair": ue_impair, - "ues_impair": ues_impair_possibles, + "ues_impair": [ + ue + for ue in ues_impair_possibles + if (not ue.niveau_competence) or ue.niveau_competence.id == niveau.id + ], } competences = [ diff --git a/tests/unit/test_cursus_but.py b/tests/unit/test_cursus_but.py new file mode 100644 index 00000000..f1e09df9 --- /dev/null +++ b/tests/unit/test_cursus_but.py @@ -0,0 +1,77 @@ +# -*- coding: UTF-8 -* + +"""Unit tests : cursus_but + + +Ce test suppose une base département existante. + +Usage: pytest tests/unit/test_cursus_but.py +""" + + +import pytest +from tests.unit import yaml_setup + +import app + +# XXX from app.but.cursus_but import FormSemestreCursusBUT +from app.comp import res_sem +from app.comp.res_but import ResultatsSemestreBUT +from app.models import ApcParcours, ApcReferentielCompetences, FormSemestre +from config import TestConfig + +DEPT = TestConfig.DEPT_TEST + + +@pytest.mark.skip # XXX WIP +@pytest.mark.slow +def test_cursus_but_jury_gb(test_client): + # Construit la base de test GB une seule fois + # puis lance les tests de jury + app.set_sco_dept(DEPT) + # login_user(User.query.filter_by(user_name="admin").first()) # XXX pour tests manuels + # ctx.push() # XXX + doc = yaml_setup.setup_from_yaml("tests/ressources/yaml/cursus_but_gb.yaml") + formsemestre: FormSemestre = FormSemestre.query.filter_by(titre="S3").first() + res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre) + cursus = FormSemestreCursusBUT(res) + ref_comp: ApcReferentielCompetences = formsemestre.formation.referentiel_competence + # Vérifie niveaux du tronc commun: + niveaux_parcours_by_annee_tc = cursus.get_niveaux_parcours_by_annee(None) + assert set(niveaux_parcours_by_annee_tc.keys()) == {1, 2, 3} + # structure particulière à GB: + assert [len(niveaux_parcours_by_annee_tc[annee]) for annee in (1, 2, 3)] == [ + 2, + 2, + 1, + ] + parcour: ApcParcours = ref_comp.parcours.filter_by(code="SEE").first() + assert parcour + niveaux_parcours_by_annee_see = cursus.get_niveaux_parcours_by_annee(parcour) + assert set(niveaux_parcours_by_annee_see.keys()) == {1, 2, 3} + # GB SEE: 4 niveaux en BU1, 5 en BUT2, 4 en BUT3 + assert [len(niveaux_parcours_by_annee_see[annee]) for annee in (1, 2, 3)] == [ + 4, + 5, + 4, + ] + # Un étudiant inscrit en SEE + inscription = formsemestre.etuds_inscriptions[1] + assert inscription.parcour.code == "SEE" + etud = inscription.etud + assert cursus.get_niveaux_parcours_etud(etud) == niveaux_parcours_by_annee_see + + +# @pytest.mark.skip # XXX WIP +def test_refcomp_niveaux_info(test_client): + """Test niveaux / parcours / UE pour un BUT INFO + avec parcours A et C, même compétences mais coefs différents + selon le parcours. + """ + # WIP + # pour le moment juste le chargement de la formation, du ref. comp, et des UE du S4. + app.set_sco_dept(DEPT) + doc = yaml_setup.setup_from_yaml("tests/ressources/yaml/cursus_but_info.yaml") + formsemestre: FormSemestre = FormSemestre.query.filter_by(titre="S4").first() + assert formsemestre + res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre)