316 lines
11 KiB
Python
316 lines
11 KiB
Python
##############################################################################
|
|
# ScoDoc
|
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
|
# See LICENSE
|
|
##############################################################################
|
|
|
|
"""
|
|
Met en place une base pour les tests unitaires, à partir d'une description
|
|
YAML qui peut donner la formation, son ref. compétences, les formsemestres,
|
|
les étudiants et leurs notes et décisions de jury.
|
|
|
|
Le traitement est effectué dans l'ordre suivant:
|
|
|
|
setup_from_yaml()
|
|
|
|
- setup_formation():
|
|
- import de la formation (le test utilise une seule formation)
|
|
- associe_ues_et_parcours():
|
|
- crée les associations formation <-> référentiel de compétence
|
|
- setup_formsemestres()
|
|
- crée les formsemestres décrits dans le YAML
|
|
avec tous les modules du semestre ou des parcours si indiqués
|
|
et une évaluation dans chaque moduleimpl.
|
|
- inscrit_les_etudiants()
|
|
- inscrit et place dans les groupes de parcours
|
|
- note_les_modules()
|
|
- saisie de toutes les notes indiquées dans le YAML
|
|
|
|
check_deca_fields()
|
|
- vérifie les champs du deca (nb UE, compétences, ...) mais pas les décisions de jury.
|
|
|
|
formsemestre_validation_auto_but(only_adm=False)
|
|
- enregistre toutes les décisions "par défaut" proposées (pas seulement les ADM)
|
|
|
|
test_but_jury()
|
|
- compare décisions attendues indiquées dans le YAML avec celles de ScoDoc
|
|
et enregistre immédiatement APRES la décision manuelle indiquée par `decision_jury`
|
|
dans le YAML.
|
|
|
|
|
|
Les tests unitaires associés sont généralement lents (construction de la base),
|
|
et donc marqués par `@pytest.mark.slow`.
|
|
"""
|
|
|
|
import os
|
|
|
|
import yaml
|
|
from flask import g
|
|
|
|
from app import db
|
|
|
|
from app.auth.models import User
|
|
|
|
from app.models import (
|
|
ApcParcours,
|
|
DispenseUE,
|
|
Evaluation,
|
|
Formation,
|
|
FormSemestre,
|
|
Identite,
|
|
Module,
|
|
ModuleImpl,
|
|
UniteEns,
|
|
)
|
|
|
|
from app.scodoc import sco_formations
|
|
from app.scodoc import sco_formsemestre_inscriptions
|
|
from app.scodoc import sco_groups
|
|
from app.scodoc import sco_saisie_notes
|
|
from app.scodoc import sco_utils as scu
|
|
|
|
from tests.conftest import RESOURCES_DIR
|
|
from tests.unit import yaml_setup_but
|
|
|
|
|
|
def setup_formation(formation_infos: dict) -> Formation:
|
|
"""Importe la formation, qui est lue à partir du fichier XML
|
|
formation_infos["filename"].
|
|
La formation peut être quelconque, on vérifie juste qu'elle est bien créée.
|
|
"""
|
|
# Lecture fichier XML local:
|
|
with open(
|
|
os.path.join(RESOURCES_DIR, "formations", formation_infos["filename"]),
|
|
encoding="utf-8",
|
|
) as f:
|
|
doc = f.read()
|
|
|
|
# --- Création de la formation
|
|
formation_id, _, _ = sco_formations.formation_import_xml(doc)
|
|
formation: Formation = Formation.query.get(formation_id)
|
|
assert formation
|
|
return formation
|
|
|
|
|
|
def create_formsemestre(
|
|
formation: Formation,
|
|
parcours: list[ApcParcours],
|
|
semestre_id: int,
|
|
titre: str,
|
|
date_debut: str,
|
|
date_fin: str,
|
|
) -> FormSemestre:
|
|
"Création d'un formsemestre, avec ses modimpls et évaluations"
|
|
assert formation.is_apc() or not parcours # parcours seulement si APC
|
|
formsemestre = FormSemestre(
|
|
formation=formation,
|
|
parcours=parcours,
|
|
dept_id=g.scodoc_dept_id,
|
|
titre=titre,
|
|
semestre_id=semestre_id,
|
|
date_debut=date_debut,
|
|
date_fin=date_fin,
|
|
)
|
|
# set responsable (list)
|
|
a_user = User.query.first()
|
|
formsemestre.responsables = [a_user]
|
|
db.session.add(formsemestre)
|
|
# Ajoute tous les modules du semestre sans parcours OU avec l'un des parcours indiqués
|
|
sem_parcours_ids = {p.id for p in parcours}
|
|
modules = [
|
|
m
|
|
for m in formsemestre.formation.modules.filter_by(semestre_id=semestre_id)
|
|
if (not m.parcours) # module de tronc commun
|
|
or (not sem_parcours_ids) # semestre sans parcours => tous
|
|
or ({p.id for p in m.parcours} & sem_parcours_ids)
|
|
]
|
|
for module in modules:
|
|
modimpl = ModuleImpl(module=module, responsable_id=a_user.id)
|
|
db.session.add(modimpl)
|
|
formsemestre.modimpls.append(modimpl)
|
|
|
|
# Crée une évaluation dans chaque module
|
|
create_evaluations(formsemestre)
|
|
|
|
# Partition par défaut:
|
|
db.session.commit()
|
|
partition_id = sco_groups.partition_create(
|
|
formsemestre.id, default=True, redirect=False
|
|
)
|
|
_ = sco_groups.create_group(partition_id, default=True)
|
|
|
|
# Partition de parcours:
|
|
if formsemestre.formation.is_apc():
|
|
formsemestre.setup_parcours_groups()
|
|
|
|
return 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(
|
|
moduleimpl=modimpl,
|
|
jour=formsemestre.date_debut,
|
|
description=f"Exam {modimpl.module.titre}",
|
|
coefficient=1.0,
|
|
note_max=20.0,
|
|
numero=1,
|
|
publish_incomplete=publish_incomplete,
|
|
)
|
|
db.session.add(evaluation)
|
|
|
|
if formsemestre.formation.is_apc():
|
|
# Affecte les mêmes poids que les coefs APC:
|
|
ue_coef_dict = modimpl.module.get_ue_coef_dict() # { ue_id : coef }
|
|
evaluation.set_ue_poids_dict(ue_coef_dict)
|
|
|
|
|
|
def note_les_modules(doc: dict):
|
|
"""Saisie les notes des étudiants
|
|
doc : données YAML
|
|
"""
|
|
a_user = User.query.first()
|
|
for nom, infos in doc["Etudiants"].items():
|
|
etud: Identite = Identite.query.filter_by(nom=nom).first()
|
|
assert etud is not None
|
|
for titre, sem_infos in infos["formsemestres"].items():
|
|
formsemestre: FormSemestre = FormSemestre.query.filter_by(
|
|
titre=titre
|
|
).first()
|
|
assert formsemestre is not None
|
|
for code_module, note in sem_infos.get("notes_modules", {}).items():
|
|
modimpl = (
|
|
formsemestre.modimpls.join(Module)
|
|
.filter_by(code=code_module)
|
|
.first()
|
|
)
|
|
# le sem. doit avoir un module du code indiqué:
|
|
if modimpl is None:
|
|
raise ValueError(f"module de code '{code_module}' introuvable")
|
|
for evaluation in modimpl.evaluations:
|
|
# s'il y a plusieurs evals, affecte la même note à chacune
|
|
note_value, invalid = sco_saisie_notes.convert_note_from_string(
|
|
str(note),
|
|
20.0,
|
|
note_min=scu.NOTES_MIN,
|
|
etudid=etud.id,
|
|
absents=[],
|
|
tosuppress=[],
|
|
invalids=[],
|
|
)
|
|
assert not invalid # valeur note invalide
|
|
assert isinstance(note_value, float)
|
|
sco_saisie_notes.notes_add(
|
|
a_user,
|
|
evaluation.id,
|
|
[(etud.id, note_value)],
|
|
comment="note_les_modules",
|
|
)
|
|
|
|
|
|
def setup_formsemestres(formation: Formation, doc: str):
|
|
"""Création des formsemestres
|
|
Le cas échéant associés à leur(s) parcours.
|
|
"""
|
|
for titre, infos in doc["FormSemestres"].items():
|
|
codes_parcours = infos.get("codes_parcours", [])
|
|
assert formation.is_apc() or not codes_parcours # parcours seulement en APC
|
|
parcours = []
|
|
for code_parcour in codes_parcours:
|
|
parcour = formation.referentiel_competence.parcours.filter_by(
|
|
code=code_parcour
|
|
).first()
|
|
assert parcour is not None
|
|
parcours.append(parcour)
|
|
|
|
_ = create_formsemestre(
|
|
formation,
|
|
parcours,
|
|
infos["idx"],
|
|
titre,
|
|
infos["date_debut"],
|
|
infos["date_fin"],
|
|
)
|
|
|
|
db.session.flush()
|
|
assert FormSemestre.query.count() == len(doc["FormSemestres"])
|
|
|
|
|
|
def inscrit_les_etudiants(formation: Formation, doc: dict):
|
|
"""Inscrit les étudiants dans chacun de leurs formsemestres"""
|
|
etudiants = doc.get("Etudiants")
|
|
if not etudiants:
|
|
return
|
|
for nom, infos in etudiants.items():
|
|
etud = Identite.create_etud(
|
|
dept_id=g.scodoc_dept_id,
|
|
nom=nom,
|
|
prenom=infos.get("prenom", "prénom"),
|
|
civilite=infos.get("civilite", "X"),
|
|
)
|
|
db.session.add(etud)
|
|
db.session.commit()
|
|
# L'inscrire à ses formsemestres
|
|
for titre, sem_infos in infos["formsemestres"].items():
|
|
formsemestre: FormSemestre = FormSemestre.query.filter_by(
|
|
titre=titre
|
|
).first()
|
|
assert formsemestre is not None
|
|
partition_parcours = formsemestre.partitions.filter_by(
|
|
partition_name=scu.PARTITION_PARCOURS
|
|
).first()
|
|
if partition_parcours is not None and "parcours" in sem_infos:
|
|
group = partition_parcours.groups.filter_by(
|
|
group_name=sem_infos["parcours"]
|
|
).first()
|
|
assert group is not None # le groupe de parcours doit exister
|
|
group_ids = [group.id]
|
|
else:
|
|
group_ids = []
|
|
# Génère des dispenses d'UEs
|
|
if "dispense_ues" in sem_infos:
|
|
etud_dispense_ues(formsemestre, etud, sem_infos["dispense_ues"])
|
|
sco_formsemestre_inscriptions.do_formsemestre_inscription_with_modules(
|
|
formsemestre.id,
|
|
etud.id,
|
|
group_ids=group_ids,
|
|
etat=scu.INSCRIT,
|
|
method="tests/unit/inscrit_les_etudiants",
|
|
)
|
|
# Met à jour les inscriptions:
|
|
for formsemestre in formation.formsemestres:
|
|
formsemestre.update_inscriptions_parcours_from_groups()
|
|
|
|
|
|
def etud_dispense_ues(
|
|
formsemestre: FormSemestre, etud: Identite, ue_acronymes: list[str]
|
|
):
|
|
"""Génère des dispenses d'UE"""
|
|
for ue_acronyme in set(ue_acronymes):
|
|
ue: UniteEns = formsemestre.formation.ues.filter_by(
|
|
acronyme=ue_acronyme
|
|
).first()
|
|
assert ue
|
|
disp = DispenseUE(formsemestre_id=formsemestre.id, ue_id=ue.id, etudid=etud.id)
|
|
db.session.add(disp)
|
|
|
|
|
|
def setup_from_yaml(filename: str) -> dict:
|
|
"""Lit le fichier yaml et construit l'ensemble des objets"""
|
|
with open(filename, encoding="utf-8") as f:
|
|
doc = yaml.safe_load(f.read())
|
|
|
|
# Charge de ref. comp. avant la formation, de façon à pouvoir
|
|
# re-créer les associations UE/Niveaux
|
|
yaml_setup_but.setup_formation_referentiel(doc.get("ReferentielCompetences", {}))
|
|
formation = setup_formation(doc["Formation"])
|
|
|
|
yaml_setup_but.associe_ues_et_parcours(formation, doc["Formation"])
|
|
setup_formsemestres(formation, doc)
|
|
etudiants = doc.get("Etudiants")
|
|
if etudiants:
|
|
inscrit_les_etudiants(formation, doc)
|
|
note_les_modules(doc)
|
|
return doc
|