Association parcours/UE: amélioration formulaire. Messages erreurs. Logique association UE/niveaux. test unitaire partiel. WIP.

This commit is contained in:
Emmanuel Viennet 2023-04-13 09:58:38 +02:00
parent 3baae95baf
commit da2f0ac2f9
6 changed files with 187 additions and 56 deletions

View File

@ -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}

View File

@ -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()

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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 = [

View File

@ -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)