1
0
forked from ScoDoc/ScoDoc

Merge branch 'table' of https://scodoc.org/git/viennet/ScoDoc into upgrading_pip

This commit is contained in:
Emmanuel Viennet 2023-04-04 10:01:44 +02:00
commit 0b4004ed93
42 changed files with 816 additions and 235 deletions

View File

@ -8,17 +8,17 @@
ScoDoc 9 API : accès aux formations
"""
from flask import g, jsonify
from flask import g, jsonify, request
from flask_login import login_required
import app
from app import log
from app.api import api_bp as bp, api_web_bp
from app.scodoc.sco_utils import json_error
from app.decorators import scodoc, permission_required
from app.models.formations import Formation
from app.models.formsemestre import FormSemestre
from app.models.moduleimpls import ModuleImpl
from app.models import ApcParcours, Formation, FormSemestre, ModuleImpl, UniteEns
from app.scodoc import sco_formations
from app.scodoc.sco_exceptions import ScoFormationConflict
from app.scodoc.sco_permissions import Permission
@ -174,7 +174,7 @@ def formation_export_by_formation_id(formation_id: int, export_ids=False):
]
},
{
"titre": "Se sensibiliser \u00e0 l'hygi\u00e8ne informatique et \u00e0 la cybers\u00e9curit\u00e9",
"titre": "Se sensibiliser \u00e0 l'hygi\u00e8ne informatique...",
"abbrev": "Hygi\u00e8ne informatique",
"code": "SAE11",
"heures_cours": 0.0,
@ -282,3 +282,29 @@ def moduleimpl(moduleimpl_id: int):
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
modimpl: ModuleImpl = query.first_or_404()
return jsonify(modimpl.to_dict(convert_objects=True))
@bp.route("/set_ue_parcours/<int:ue_id>", methods=["POST"])
@api_web_bp.route("/set_ue_parcours/<int:ue_id>", methods=["POST"])
@login_required
@scodoc
@permission_required(Permission.ScoChangeFormation)
def set_ue_parcours(ue_id: int):
"""Associe UE et parcours BUT.
La liste des ids de parcours est passée en argument JSON.
JSON arg: [parcour_id1, parcour_id2, ...]
"""
query = UniteEns.query.filter_by(id=ue_id)
if g.scodoc_dept:
query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id)
ue: UniteEns = query.first_or_404()
parcours_ids = request.get_json(force=True) or [] # may raise 400 Bad Request
if parcours_ids == [""]:
parcours = []
else:
parcours = [
ApcParcours.query.get_or_404(int(parcour_id)) for parcour_id in parcours_ids
]
log(f"set_ue_parcours: ue_id={ue.id} parcours_ids={parcours_ids}")
ok, error_message = ue.set_parcours(parcours)
return jsonify({"status": ok, "message": error_message})

View File

@ -7,6 +7,8 @@
"""
ScoDoc 9 API : accès aux formsemestres
"""
from operator import attrgetter, itemgetter
from flask import g, jsonify, request
from flask_login import login_required
@ -254,7 +256,7 @@ def formsemestre_programme(formsemestre_id: int):
if g.scodoc_dept:
query = query.filter_by(dept_id=g.scodoc_dept_id)
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
ues = formsemestre.query_ues()
ues = formsemestre.get_ues()
m_list = {
ModuleType.RESSOURCE: [],
ModuleType.SAE: [],
@ -345,7 +347,7 @@ def formsemestre_etudiants(
etud["id"], formsemestre_id, exclude_default=True
)
return jsonify(sorted(etuds, key=lambda e: e["sort_key"]))
return jsonify(sorted(etuds, key=itemgetter("sort_key")))
@bp.route("/formsemestre/<int:formsemestre_id>/etat_evals")
@ -432,7 +434,7 @@ def etat_evals(formsemestre_id: int):
# Si il y a plus d'une note saisie pour l'évaluation
if len(notes) >= 1:
# Tri des notes en fonction de leurs dates
notes_sorted = sorted(notes, key=lambda note: note.date)
notes_sorted = sorted(notes, key=attrgetter("date"))
date_debut = notes_sorted[0].date
date_fin = notes_sorted[-1].date

View File

@ -7,6 +7,8 @@
"""
ScoDoc 9 API : partitions
"""
from operator import attrgetter
from flask import g, jsonify, request
from flask_login import login_required
@ -85,7 +87,7 @@ def formsemestre_partitions(formsemestre_id: int):
if g.scodoc_dept:
query = query.filter_by(dept_id=g.scodoc_dept_id)
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
partitions = sorted(formsemestre.partitions, key=lambda p: p.numero or 0)
partitions = sorted(formsemestre.partitions, key=attrgetter("numero"))
return jsonify(
{
partition.id: partition.to_dict(with_groups=True)
@ -441,9 +443,9 @@ def formsemestre_order_partitions(formsemestre_id: int):
message="paramètre liste des partitions invalide",
)
for p_id, numero in zip(partition_ids, range(len(partition_ids))):
p = Partition.query.get_or_404(p_id)
p.numero = numero
db.session.add(p)
partition = Partition.query.get_or_404(p_id)
partition.numero = numero
db.session.add(partition)
db.session.commit()
app.set_sco_dept(formsemestre.departement.acronym)
sco_cache.invalidate_formsemestre(formsemestre_id)

View File

@ -7,9 +7,10 @@
"""
Edition associations UE <-> Ref. Compétence
"""
from flask import g, url_for
from flask import g, render_template, url_for
from app.models import ApcReferentielCompetences, UniteEns
from app.scodoc import codes_cursus
from app.forms.formation.ue_parcours_niveau import UEParcoursNiveauForm
def form_ue_choix_niveau(ue: UniteEns) -> str:
@ -32,7 +33,7 @@ def form_ue_choix_niveau(ue: UniteEns) -> str:
for parcour in ref_comp.parcours:
parcours_options.append(
f"""<option value="{parcour.id}" {
'selected' if ue.parcour == parcour else ''}
'selected' if parcour in ue.parcours else ''}
>{parcour.libelle} ({parcour.code})
</option>"""
)
@ -44,14 +45,14 @@ def form_ue_choix_niveau(ue: UniteEns) -> str:
<div class="cont_ue_choix_niveau">
<div>
<b>Parcours&nbsp;:</b>
<select class="select_parcour"
<select class="select_parcour multiselect"
onchange="set_ue_parcour(this);"
data-ue_id="{ue.id}"
data-setter="{
url_for( "notes.set_ue_parcours", scodoc_dept=g.scodoc_dept)
}">
<option value="" {
'selected' if ue.parcour is None else ''
'selected' if not ue.parcours else ''
}>Tous</option>
{newline.join(parcours_options)}
</select>
@ -72,6 +73,28 @@ def form_ue_choix_niveau(ue: UniteEns) -> str:
"""
# Nouvelle version XXX WIP
def form_ue_choix_parcours_niveau(ue: UniteEns):
"""formulaire (div) pour choix association des parcours et du niveau de compétence d'une UE"""
if ue.type != codes_cursus.UE_STANDARD:
return ""
ref_comp = ue.formation.referentiel_competence
if ref_comp is None:
return f"""<div class="ue_choix_niveau">
<div class="warning">Pas de référentiel de compétence associé à cette formation !</div>
<div><a class="stdlink" href="{ url_for('notes.refcomp_assoc_formation',
scodoc_dept=g.scodoc_dept, formation_id=ue.formation.id)
}">associer un référentiel de compétence</a>
</div>
</div>"""
parcours = ue.formation.referentiel_competence.parcours
form = UEParcoursNiveauForm(ue, parcours)
return f"""<div class="ue_choix_niveau">
{ render_template( "pn/ue_choix_parcours_niveau.j2", form_ue_parcours_niveau=form ) }
</div>
"""
def get_ue_niveaux_options_html(ue: UniteEns) -> str:
"""fragment html avec les options du menu de sélection du
niveau de compétences associé à une UE.
@ -85,9 +108,7 @@ def get_ue_niveaux_options_html(ue: UniteEns) -> str:
return ""
# Les niveaux:
annee = ue.annee() # 1, 2, 3
parcours, niveaux_by_parcours = ref_comp.get_niveaux_by_parcours(
annee, parcour=ue.parcour
)
parcours, niveaux_by_parcours = ref_comp.get_niveaux_by_parcours(annee, ue.parcours)
# Les niveaux déjà associés à d'autres UE du même semestre
autres_ues = ue.formation.ues.filter_by(semestre_idx=ue.semestre_idx)

View File

@ -24,7 +24,6 @@ from app.comp.res_but import ResultatsSemestreBUT
from app.comp.res_compat import NotesTableCompat
from app.comp import res_sem
from app.models import formsemestre
from app.models.but_refcomp import (
ApcAnneeParcours,
@ -32,6 +31,7 @@ from app.models.but_refcomp import (
ApcNiveau,
ApcParcours,
ApcParcoursNiveauCompetence,
ApcReferentielCompetences,
)
from app.models import Scolog, ScolarAutorisationInscription
from app.models.but_validations import (
@ -109,7 +109,7 @@ class EtudCursusBUT:
"cache les niveaux"
for annee in (1, 2, 3):
niveaux_d = formation.referentiel_competence.get_niveaux_by_parcours(
annee, self.parcour
annee, [self.parcour]
)[1]
# groupe les niveaux de tronc commun et ceux spécifiques au parcour
self.niveaux_by_annee[annee] = niveaux_d["TC"] + (
@ -170,6 +170,7 @@ class EtudCursusBUT:
}
}
"""
# XXX lent, provisoirement utilisé par TableJury.add_but_competences()
return {
competence.id: {
annee: self.validation_par_competence_et_annee.get(
@ -204,3 +205,210 @@ class EtudCursusBUT:
validation_rcue.to_dict_codes() if validation_rcue else None
)
return d
class FormSemestreCursusBUT:
"""L'état des étudiants d'un formsemestre dans leur cursus BUT
Permet d'obtenir pour chacun liste des niveaux validés/à valider
"""
def __init__(self, res: ResultatsSemestreBUT):
"""res indique le formsemestre de référence,
qui donne la liste des étudiants et le référentiel de compétence.
"""
self.res = res
self.formsemestre = res.formsemestre
if not res.formsemestre.formation.referentiel_competence:
raise ScoNoReferentielCompetences(formation=res.formsemestre.formation)
# Données cachées pour accélerer les accès:
self.referentiel_competences_id: int = (
self.res.formsemestre.formation.referentiel_competence_id
)
self.ue_ids: set[int] = set()
"set of ue_ids known to belong to our cursus"
self.parcours_by_id: dict[int, ApcParcours] = {}
"cache des parcours"
self.niveaux_by_parcour_by_annee: dict[int, dict[int, list[ApcNiveau]]] = {}
"cache { parcour_id : { annee : [ parcour] } }"
self.niveaux_by_id: dict[int, ApcNiveau] = {}
"cache niveaux"
def get_niveaux_parcours_etud(self, etud: Identite) -> dict[int, list[ApcNiveau]]:
"""Les niveaux compétences que doit valider cet étudiant.
Le parcour considéré est celui de l'inscription dans le semestre courant.
Si on est en début de cursus, on peut être en tronc commun sans avoir choisi
de parcours. Dans ce cas, on n'aura que les compétences de tronc commun.
Il faudra donc, avant de diplômer, s'assurer que les compétences du parcours
du dernier semestre (S6) sont validées (avec parcour non NULL).
"""
parcour_id = self.res.etuds_parcour_id.get(etud.id)
if parcour_id is None:
parcour = None
else:
if parcour_id not in self.parcours_by_id:
self.parcours_by_id[parcour_id] = ApcParcours.query.get(parcour_id)
parcour = self.parcours_by_id[parcour_id]
return self.get_niveaux_parcours_by_annee(parcour)
def get_niveaux_parcours_by_annee(
self, parcour: ApcParcours
) -> dict[int, list[ApcNiveau]]:
"""La liste des niveaux de compétences du parcours, par année BUT.
{ 1 : [ niveau, ... ] }
Si parcour est None, donne uniquement les niveaux tronc commun
(cas utile par exemple en 1ere année, mais surtout pas pour donner un diplôme!)
"""
parcour_id = None if parcour is None else parcour.id
if parcour_id in self.niveaux_by_parcour_by_annee:
return self.niveaux_by_parcour_by_annee[parcour_id]
ref_comp: ApcReferentielCompetences = (
self.res.formsemestre.formation.referentiel_competence
)
niveaux_by_annee = {}
for annee in (1, 2, 3):
niveaux_d = ref_comp.get_niveaux_by_parcours(annee, [parcour])[1]
# groupe les niveaux de tronc commun et ceux spécifiques au parcour
niveaux_by_annee[annee] = niveaux_d["TC"] + (
niveaux_d[parcour.id] if parcour else []
)
self.niveaux_by_parcour_by_annee[parcour_id] = niveaux_by_annee
self.niveaux_by_id.update(
{niveau.id: niveau for niveau in niveaux_by_annee[annee]}
)
return niveaux_by_annee
def get_etud_validation_par_competence_et_annee(self, etud: Identite):
"""{ competence_id : { 'BUT1' : validation_rcue (la "meilleure"), ... } }"""
validation_par_competence_et_annee = {}
for validation_rcue in ApcValidationRCUE.query.filter_by(etud=etud):
# On s'assurer qu'elle concerne notre cursus !
ue = validation_rcue.ue2
if ue.id not in self.ue_ids:
if (
ue.formation.referentiel_competences_id
== self.referentiel_competences_id
):
self.ue_ids = ue.id
else:
continue # skip this validation
niveau = validation_rcue.niveau()
if not niveau.competence.id in validation_par_competence_et_annee:
validation_par_competence_et_annee[niveau.competence.id] = {}
previous_validation = validation_par_competence_et_annee.get(
niveau.competence.id
).get(validation_rcue.annee())
# prend la "meilleure" validation
if (not previous_validation) or (
sco_codes.BUT_CODES_ORDERED[validation_rcue.code]
> sco_codes.BUT_CODES_ORDERED[previous_validation["code"]]
):
self.validation_par_competence_et_annee[niveau.competence.id][
niveau.annee
] = validation_rcue
return validation_par_competence_et_annee
def list_etud_inscriptions(self, etud: Identite):
"Le parcours à valider: celui du DERNIER semestre suivi (peut être None)"
self.niveaux_by_annee = {}
"{ annee : liste des niveaux à valider }"
self.niveaux: dict[int, ApcNiveau] = {}
"cache les niveaux"
for annee in (1, 2, 3):
niveaux_d = formation.referentiel_competence.get_niveaux_by_parcours(
annee, [self.parcour]
)[1]
# groupe les niveaux de tronc commun et ceux spécifiques au parcour
self.niveaux_by_annee[annee] = niveaux_d["TC"] + (
niveaux_d[self.parcour.id] if self.parcour else []
)
self.niveaux.update(
{niveau.id: niveau for niveau in self.niveaux_by_annee[annee]}
)
self.validation_par_competence_et_annee = {}
"""{ competence_id : { 'BUT1' : validation_rcue (la "meilleure"), ... } }"""
for validation_rcue in ApcValidationRCUE.query.filter_by(etud=etud):
niveau = validation_rcue.niveau()
if not niveau.competence.id in self.validation_par_competence_et_annee:
self.validation_par_competence_et_annee[niveau.competence.id] = {}
previous_validation = self.validation_par_competence_et_annee.get(
niveau.competence.id
).get(validation_rcue.annee())
# prend la "meilleure" validation
if (not previous_validation) or (
sco_codes.BUT_CODES_ORDERED[validation_rcue.code]
> sco_codes.BUT_CODES_ORDERED[previous_validation["code"]]
):
self.validation_par_competence_et_annee[niveau.competence.id][
niveau.annee
] = validation_rcue
self.competences = {
competence.id: competence
for competence in (
self.parcour.query_competences()
if self.parcour
else self.formation.referentiel_competence.get_competences_tronc_commun()
)
}
"cache { competence_id : competence }"
def formsemestre_warning_apc_setup(
formsemestre: FormSemestre, res: ResultatsSemestreBUT
) -> str:
"""Vérifie que la formation est OK pour un BUT:
- ref. compétence associé
- tous les niveaux des parcours du semestre associés à des UEs du formsemestre
- pas d'UE non associée à un niveau
Renvoie fragment de HTML.
"""
if not formsemestre.formation.is_apc():
return ""
if formsemestre.formation.referentiel_competence is None:
return f"""<div class="formsemestre_status_warning">
La <a class=stdlink" href="{
url_for("notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=formsemestre.formation.id)
}">formation n'est pas associée à un référentiel de compétence.</a>
</div>
"""
# Vérifie les niveaux de chaque parcours
H = []
for parcour in formsemestre.parcours or [None]:
annee = (formsemestre.semestre_id + 1) // 2
niveaux_ids = {
niveau.id
for niveau in ApcNiveau.niveaux_annee_de_parcours(
parcour, annee, formsemestre.formation.referentiel_competence
)
}
ues_parcour = formsemestre.formation.query_ues_parcour(parcour).filter(
UniteEns.semestre_idx == formsemestre.semestre_id
)
ues_niveaux_ids = {
ue.niveau_competence.id for ue in ues_parcour if ue.niveau_competence
}
if niveaux_ids != ues_niveaux_ids:
H.append(
f"""Parcours {parcour.code if parcour else "Tronc commun"} :
{len(ues_niveaux_ids)} UE avec niveaux
mais {len(niveaux_ids)} niveaux à valider !
"""
)
if not H:
return ""
return f"""<div class="formsemestre_status_warning">
Problème dans la configuration de la formation:
<ul>
<li>{ '<li></li>'.join(H) }</li>
</ul>
<p class="help">Vérifiez les parcours cochés pour ce semestre,
et les associations entre UE et niveaux <a class=stdlink" href="{
url_for("notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=formsemestre.formation.id)
}">dans la formation.
</p>
</div>
"""

View File

@ -324,7 +324,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
parcours,
niveaux_by_parcours,
) = formation.referentiel_competence.get_niveaux_by_parcours(
self.annee_but, self.parcour
self.annee_but, [self.parcour]
)
self.niveaux_competences = niveaux_by_parcours["TC"] + (
niveaux_by_parcours[self.parcour.id] if self.parcour else []
@ -1003,7 +1003,7 @@ def list_ue_parcour_etud(
parcour = ApcParcours.query.get(res.etuds_parcour_id[etud.id])
ues = (
formsemestre.formation.query_ues_parcour(parcour)
.filter_by(semestre_idx=formsemestre.semestre_id)
.filter(UniteEns.semestre_idx == formsemestre.semestre_id)
.order_by(UniteEns.numero)
.all()
)

View File

@ -228,14 +228,14 @@ class BonusSportAdditif(BonusSport):
# en APC, bonus_moy_arr est (nb_etuds, nb_ues_non_bonus)
if self.formsemestre.formation.is_apc():
# Bonus sur les UE et None sur moyenne générale
ues_idx = [ue.id for ue in self.formsemestre.query_ues(with_sport=False)]
ues_idx = [ue.id for ue in self.formsemestre.get_ues(with_sport=False)]
self.bonus_ues = pd.DataFrame(
bonus_moy_arr, index=self.etuds_idx, columns=ues_idx, dtype=float
)
elif self.classic_use_bonus_ues:
# Formations classiques apppliquant le bonus sur les UEs
# ici bonus_moy_arr = ndarray 1d nb_etuds
ues_idx = [ue.id for ue in self.formsemestre.query_ues(with_sport=False)]
ues_idx = [ue.id for ue in self.formsemestre.get_ues(with_sport=False)]
self.bonus_ues = pd.DataFrame(
np.stack([bonus_moy_arr] * len(ues_idx)).T,
index=self.etuds_idx,
@ -420,7 +420,7 @@ class BonusAmiens(BonusSportAdditif):
# # Bonus moyenne générale et sur les UE
# self.bonus_moy_gen = pd.Series(bonus, index=self.etuds_idx, dtype=float)
# ues_idx = [ue.id for ue in self.formsemestre.query_ues(with_sport=False)]
# ues_idx = [ue.id for ue in self.formsemestre.get_ues(with_sport=False)]
# nb_ues_no_bonus = len(ues_idx)
# self.bonus_ues = pd.DataFrame(
# np.stack([bonus] * nb_ues_no_bonus, axis=1),
@ -597,7 +597,7 @@ class BonusCachan1(BonusSportAdditif):
sem_modimpl_moys_inscrits = sem_modimpl_moys_inscrits[:, :, 0]
# ici sem_modimpl_moys_inscrits est nb_etuds x nb_mods_bonus, en APC et en classic
note_bonus_max = np.max(sem_modimpl_moys_inscrits, axis=1) # 1d, nb_etuds
ues = self.formsemestre.query_ues(with_sport=False).all()
ues = self.formsemestre.get_ues(with_sport=False)
ues_idx = [ue.id for ue in ues]
if self.formsemestre.formation.is_apc(): # --- BUT
@ -687,7 +687,7 @@ class BonusCalais(BonusSportAdditif):
else:
self.classic_use_bonus_ues = True # pour les LP
super().compute_bonus(sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan)
ues = self.formsemestre.query_ues(with_sport=False).all()
ues = self.formsemestre.get_ues(with_sport=False)
ues_sans_bs = [
ue for ue in ues if ue.acronyme[-2:].upper() != "BS"
] # les 2 derniers cars forcés en majus
@ -788,7 +788,7 @@ class BonusIUTRennes1(BonusSportAdditif):
sem_modimpl_moys_inscrits = sem_modimpl_moys_inscrits[:, :, 0]
# ici sem_modimpl_moys_inscrits est nb_etuds x nb_mods_bonus, en APC et en classic
note_bonus_max = np.max(sem_modimpl_moys_inscrits, axis=1) # 1d, nb_etuds
nb_ues = self.formsemestre.query_ues(with_sport=False).count()
nb_ues = len(self.formsemestre.get_ues(with_sport=False))
bonus_moy_arr = np.where(
note_bonus_max > self.seuil_moy_gen,

View File

@ -415,7 +415,7 @@ def load_evaluations_poids(moduleimpl_id: int) -> tuple[pd.DataFrame, list]:
"""
modimpl: ModuleImpl = ModuleImpl.query.get(moduleimpl_id)
evaluations = Evaluation.query.filter_by(moduleimpl_id=moduleimpl_id).all()
ues = modimpl.formsemestre.query_ues(with_sport=False).all()
ues = modimpl.formsemestre.get_ues(with_sport=False)
ue_ids = [ue.id for ue in ues]
evaluation_ids = [evaluation.id for evaluation in evaluations]
evals_poids = pd.DataFrame(columns=ue_ids, index=evaluation_ids, dtype=float)

View File

@ -121,7 +121,7 @@ def df_load_modimpl_coefs(
DataFrame rows = UEs (sans bonus), columns = modimpl, value = coef.
"""
if ues is None:
ues = formsemestre.query_ues().all()
ues = formsemestre.get_ues()
ue_ids = [x.id for x in ues]
if modimpls is None:
modimpls = formsemestre.modimpls_sorted

View File

@ -247,9 +247,9 @@ class ResultatsSemestreBUT(NotesTableCompat):
) in self.formsemestre.formation.referentiel_competence.parcours.all() + [None]:
ue_by_parcours[None if parcour is None else parcour.id] = {
ue.id: 1.0
for ue in self.formsemestre.formation.query_ues_parcour(
parcour
).filter_by(semestre_idx=self.formsemestre.semestre_id)
for ue in self.formsemestre.formation.query_ues_parcour(parcour).filter(
UniteEns.semestre_idx == self.formsemestre.semestre_id
)
}
#
for etudid in etuds_parcour_id:
@ -290,7 +290,7 @@ class ResultatsSemestreBUT(NotesTableCompat):
ues_parcour = self.formsemestre.formation.query_ues_parcour(parcour)
ues_ids = set()
for niveau in niveaux:
ue = ues_parcour.filter_by(niveau_competence=niveau).first()
ue = ues_parcour.filter_by(UniteEns.niveau_competence == niveau).first()
if ue:
ues_ids.add(ue.id)

View File

@ -10,6 +10,8 @@
from collections import Counter, defaultdict
from collections.abc import Generator
from functools import cached_property
from operator import attrgetter
import numpy as np
import pandas as pd
@ -162,7 +164,7 @@ class ResultatsSemestre(ResultatsCache):
(indices des DataFrames).
Note: un étudiant n'est pas nécessairement inscrit dans toutes ces UEs.
"""
return self.formsemestre.query_ues(with_sport=True).all()
return self.formsemestre.get_ues(with_sport=True)
@cached_property
def ressources(self):
@ -233,7 +235,7 @@ class ResultatsSemestre(ResultatsCache):
for modimpl in self.formsemestre.modimpls_sorted
if self.modimpl_inscr_df[modimpl.id][etudid]
}
ues = sorted(list(ues), key=lambda x: x.numero or 0)
ues = sorted(list(ues), key=attrgetter("numero"))
return ues
def modimpls_in_ue(self, ue: UniteEns, etudid, with_bonus=True) -> list[ModuleImpl]:
@ -283,7 +285,7 @@ class ResultatsSemestre(ResultatsCache):
# Quand il y a une capitalisation, vérifie toutes les UEs
sum_notes_ue = 0.0
sum_coefs_ue = 0.0
for ue in self.formsemestre.query_ues():
for ue in self.formsemestre.get_ues():
ue_cap = self.get_etud_ue_status(etudid, ue.id)
if ue_cap is None:
continue

View File

@ -108,7 +108,7 @@ class NotesTableCompat(ResultatsSemestre):
Si filter_sport, retire les UE de type SPORT.
Résultat: liste de dicts { champs UE U stats moyenne UE }
"""
ues = self.formsemestre.query_ues(with_sport=not filter_sport)
ues = self.formsemestre.get_ues(with_sport=not filter_sport)
ues_dict = []
for ue in ues:
d = ue.to_dict()
@ -178,7 +178,7 @@ class NotesTableCompat(ResultatsSemestre):
self.etud_moy_gen_ranks,
self.etud_moy_gen_ranks_int,
) = moy_sem.comp_ranks_series(etud_moy_gen_dem_zero)
ues = self.formsemestre.query_ues()
ues = self.formsemestre.get_ues()
for ue in ues:
moy_ue = self.etud_moy_ue[ue.id]
self.ue_rangs[ue.id] = (
@ -260,7 +260,7 @@ class NotesTableCompat(ResultatsSemestre):
Return: True|False, message explicatif
"""
ue_status_list = []
for ue in self.formsemestre.query_ues():
for ue in self.formsemestre.get_ues():
ue_status = self.get_etud_ue_status(etudid, ue.id)
if ue_status:
ue_status_list.append(ue_status)
@ -477,7 +477,7 @@ class NotesTableCompat(ResultatsSemestre):
"""
table_moyennes = []
etuds_inscriptions = self.formsemestre.etuds_inscriptions
ues = self.formsemestre.query_ues(with_sport=True) # avec bonus
ues = self.formsemestre.get_ues(with_sport=True) # avec bonus
for etudid in etuds_inscriptions:
moy_gen = self.etud_moy_gen.get(etudid, False)
if moy_gen is False:

View File

@ -0,0 +1,39 @@
from flask import g, url_for
from flask_wtf import FlaskForm
from wtforms.fields import SelectField, SelectMultipleField
from app.models import ApcParcours, ApcReferentielCompetences, UniteEns
class UEParcoursNiveauForm(FlaskForm):
"Formulaire association parcours et niveau de compétence à une UE"
niveau_select = SelectField(
"Niveau de compétence:", render_kw={"class": "niveau_select"}
)
parcours_multiselect = SelectMultipleField(
"Parcours :",
coerce=int,
option_widget={"class": "form-check-input"},
# widget_attrs={"class": "form-check"},
render_kw={"class": "multiselect select_ue_parcours", "multiple": "multiple"},
)
def __init__(self, ue: UniteEns, parcours: list[ApcParcours], *args, **kwargs):
super().__init__(*args, **kwargs)
# Initialise le menu des niveaux:
self.niveau_select.render_kw["data-ue_id"] = ue.id
self.niveau_select.choices = [
(r.id, f"{r.type_titre} {r.specialite_long} ({r.get_version()})")
for r in ApcReferentielCompetences.query.filter_by(dept_id=g.scodoc_dept_id)
]
# Initialise le menu des parcours
self.parcours_multiselect.render_kw["data-set_ue_parcours"] = url_for(
"apiweb.set_ue_parcours", ue_id=ue.id, scodoc_dept=g.scodoc_dept
)
parcours_options = [(str(p.id), f"{p.libelle} ({p.code})") for p in parcours]
self.parcours_multiselect.choices = parcours_options
# initialize checked items based on u instance
parcours_selected = [str(p.id) for p in ue.parcours]
self.parcours_multiselect.process_data(parcours_selected)

View File

@ -29,14 +29,14 @@ Formulaire changement formation
"""
from flask_wtf import FlaskForm
from wtforms import RadioField, SubmitField, validators
from wtforms import RadioField, SubmitField
from app.models import Formation
class FormSemestreChangeFormationForm(FlaskForm):
"Formulaire changement formation d'un formsemestre"
# consrtuit dynamiquement ci-dessous
# construit dynamiquement ci-dessous
def gen_formsemestre_change_formation_form(

View File

@ -6,6 +6,7 @@
"""ScoDoc 9 models : Référentiel Compétence BUT 2021
"""
from datetime import datetime
from operator import attrgetter
from flask_sqlalchemy.query import Query
from sqlalchemy.orm import class_mapper
@ -129,11 +130,11 @@ class ApcReferentielCompetences(db.Model, XMLModel):
}
def get_niveaux_by_parcours(
self, annee: int, parcour: "ApcParcours" = None
self, annee: int, parcours: list["ApcParcours"] = None
) -> tuple[list["ApcParcours"], dict]:
"""
Construit la liste des niveaux de compétences pour chaque parcours
de ce référentiel, ou seulement pour le parcours donné.
de ce référentiel, ou seulement pour les parcours donnés.
Les niveaux sont groupés par parcours, en isolant les niveaux de tronc commun.
@ -150,10 +151,8 @@ class ApcReferentielCompetences(db.Model, XMLModel):
)
"""
parcours_ref = self.parcours.order_by(ApcParcours.numero).all()
if parcour is None:
if parcours is None:
parcours = parcours_ref
else:
parcours = [parcour]
niveaux_by_parcours = {
parcour.id: ApcNiveau.niveaux_annee_de_parcours(parcour, annee, self)
for parcour in parcours_ref
@ -205,7 +204,7 @@ class ApcReferentielCompetences(db.Model, XMLModel):
for competence in parcours[0].query_competences()
if competence.id in ids
],
key=lambda c: c.numero or 0,
key=attrgetter("numero"),
)
def table_niveaux_parcours(self) -> dict:
@ -241,7 +240,7 @@ class ApcCompetence(db.Model, XMLModel):
titre = db.Column(db.Text(), nullable=False, index=True)
titre_long = db.Column(db.Text())
couleur = db.Column(db.Text())
numero = db.Column(db.Integer) # ordre de présentation
numero = db.Column(db.Integer, nullable=False, default=0) # ordre de présentation
_xml_attribs = { # xml_attrib : attribute
"id": "id_orebut",
"nom_court": "titre", # was name
@ -524,7 +523,7 @@ class ApcParcours(db.Model, XMLModel):
db.ForeignKey("apc_referentiel_competences.id", ondelete="CASCADE"),
nullable=False,
)
numero = db.Column(db.Integer) # ordre de présentation
numero = db.Column(db.Integer, nullable=False, default=0) # ordre de présentation
code = db.Column(db.Text(), nullable=False)
libelle = db.Column(db.Text(), nullable=False)
annees = db.relationship(
@ -533,7 +532,6 @@ class ApcParcours(db.Model, XMLModel):
lazy="dynamic",
cascade="all, delete-orphan",
)
ues = db.relationship("UniteEns", back_populates="parcour")
def __repr__(self) -> str:
return f"<{self.__class__.__name__} {self.id} {self.code!r} ref={self.referentiel}>"

View File

@ -3,6 +3,7 @@
"""ScoDoc models: evaluations
"""
import datetime
from operator import attrgetter
from app import db
from app.models.etudiants import Identite
@ -44,7 +45,7 @@ class Evaluation(db.Model):
)
# ordre de presentation (par défaut, le plus petit numero
# est la plus ancienne eval):
numero = db.Column(db.Integer)
numero = db.Column(db.Integer, nullable=False, default=0)
ues = db.relationship("UniteEns", secondary="evaluation_ue_poids", viewonly=True)
def __repr__(self):
@ -151,7 +152,7 @@ class Evaluation(db.Model):
Return True if (uncommited) modification, False otherwise.
"""
ue_coef_dict = self.moduleimpl.module.get_ue_coef_dict()
sem_ues = self.moduleimpl.formsemestre.query_ues(with_sport=False).all()
sem_ues = self.moduleimpl.formsemestre.get_ues(with_sport=False)
modified = False
for ue in sem_ues:
existing_poids = EvaluationUEPoids.query.filter_by(
@ -196,7 +197,7 @@ class Evaluation(db.Model):
return {
p.ue.id: p.poids
for p in sorted(
self.ue_poids, key=lambda p: (p.ue.numero or 0, p.ue.acronyme)
self.ue_poids, key=lambda p: attrgetter("ue.numero", "ue.acronyme")
)
}

View File

@ -9,13 +9,12 @@ from app.models import SHORT_STR_LEN
from app.models.but_refcomp import (
ApcAnneeParcours,
ApcCompetence,
ApcNiveau,
ApcParcours,
ApcParcoursNiveauCompetence,
)
from app.models.modules import Module
from app.models.moduleimpls import ModuleImpl
from app.models.ues import UniteEns
from app.models.ues import UniteEns, UEParcours
from app.scodoc import sco_cache
from app.scodoc import codes_cursus
from app.scodoc import sco_utils as scu
@ -213,23 +212,36 @@ class Formation(db.Model):
if change:
app.clear_scodoc_cache()
def query_ues_parcour(self, parcour: ApcParcours) -> Query:
"""Les UEs d'un parcours de la formation.
def query_ues_parcour(
self, parcour: ApcParcours, with_sport: bool = False
) -> Query:
"""Les UEs (non bonus) d'un parcours de la formation
(déclarée comme faisant partie du parcours ou du tronc commun, sans aucun parcours)
Si parcour est None, les UE sans parcours.
Exemple: pour avoir les UE du semestre 3, faire
`formation.query_ues_parcour(parcour).filter_by(semestre_idx=3)`
`formation.query_ues_parcour(parcour).filter(UniteEns.semestre_idx == 3)`
"""
if with_sport:
query_f = UniteEns.query.filter_by(formation=self)
else:
query_f = UniteEns.query.filter_by(formation=self, type=UE_STANDARD)
# Les UE sans parcours:
query_no_parcours = query_f.outerjoin(UEParcours).filter(
UEParcours.parcours_id == None
)
if parcour is None:
return UniteEns.query.filter_by(
formation=self, type=UE_STANDARD, parcour_id=None
)
return UniteEns.query.filter_by(formation=self, type=UE_STANDARD).filter(
UniteEns.niveau_competence_id == ApcNiveau.id,
(UniteEns.parcour_id == parcour.id) | (UniteEns.parcour_id == None),
ApcParcoursNiveauCompetence.competence_id == ApcNiveau.competence_id,
ApcParcoursNiveauCompetence.annee_parcours_id == ApcAnneeParcours.id,
ApcAnneeParcours.parcours_id == parcour.id,
)
return query_no_parcours.order_by(UniteEns.numero)
# Ajoute les UE du parcours sélectionné:
return query_no_parcours.union(
query_f.join(UEParcours).filter_by(parcours_id=parcour.id)
).order_by(UniteEns.numero)
# return UniteEns.query.filter_by(formation=self, type=UE_STANDARD).filter(
# UniteEns.niveau_competence_id == ApcNiveau.id,
# (UniteEns.parcour_id == parcour.id) | (UniteEns.parcour_id == None),
# ApcParcoursNiveauCompetence.competence_id == ApcNiveau.competence_id,
# ApcParcoursNiveauCompetence.annee_parcours_id == ApcAnneeParcours.id,
# ApcAnneeParcours.parcours_id == parcour.id,
# )
def query_competences_parcour(self, parcour: ApcParcours) -> Query:
"""Les ApcCompetences d'un parcours de la formation.
@ -279,7 +291,7 @@ class Matiere(db.Model):
matiere_id = db.synonym("id")
ue_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id"))
titre = db.Column(db.Text())
numero = db.Column(db.Integer) # ordre de présentation
numero = db.Column(db.Integer, nullable=False, default=0) # ordre de présentation
modules = db.relationship("Module", lazy="dynamic", backref="matiere")

View File

@ -12,6 +12,7 @@
"""
import datetime
from functools import cached_property
from operator import attrgetter
from flask_login import current_user
from flask_sqlalchemy.query import Query
@ -282,26 +283,34 @@ class FormSemestre(db.Model):
)
return r or []
def query_ues(self, with_sport=False) -> Query:
def get_ues(self, with_sport=False) -> list[UniteEns]:
"""UE des modules de ce semestre, triées par numéro.
- Formations classiques: les UEs auxquelles appartiennent
les modules mis en place dans ce semestre.
- Formations APC / BUT: les UEs de la formation qui
- ont le même numéro de semestre que ce formsemestre
- sont associées à l'un des parcours de ce formsemestre (ou à aucun)
- ont le même numéro de semestre que ce formsemestre;
- et sont associées à l'un des parcours de ce formsemestre
(ou à aucun, donc tronc commun).
"""
if self.formation.get_cursus().APC_SAE:
sem_ues = UniteEns.query.filter_by(
formation=self.formation, semestre_idx=self.semestre_id
formation: Formation = self.formation
if formation.is_apc():
sem_ues = {
ue.id: ue
for ue in formation.query_ues_parcour(
None, with_sport=with_sport
).filter(UniteEns.semestre_idx == self.semestre_id)
}
for parcour in self.parcours:
sem_ues.update(
{
ue.id: ue
for ue in formation.query_ues_parcour(
parcour, with_sport=with_sport
).filter(UniteEns.semestre_idx == self.semestre_id)
}
)
if self.parcours:
# Prend toutes les UE de l'un des parcours du sem., ou déclarées sans parcours
sem_ues = sem_ues.filter(
(UniteEns.parcour == None)
| (UniteEns.parcour_id.in_([p.id for p in self.parcours]))
)
# si le sem. ne coche aucun parcours, prend toutes les UE
ues = sem_ues.values()
return sorted(ues, key=attrgetter("numero"))
else:
sem_ues = db.session.query(UniteEns).filter(
ModuleImpl.formsemestre_id == self.id,
@ -310,32 +319,7 @@ class FormSemestre(db.Model):
)
if not with_sport:
sem_ues = sem_ues.filter(UniteEns.type != codes_cursus.UE_SPORT)
return sem_ues.order_by(UniteEns.numero)
def query_ues_parcours_etud(self, etudid: int) -> Query:
"""XXX inutilisé à part pour un test unitaire => supprimer ?
UEs que suit l'étudiant dans ce semestre BUT
en fonction du parcours dans lequel il est inscrit.
Si l'étudiant n'est inscrit à aucun parcours,
renvoie uniquement les UEs de tronc commun (sans parcours).
Si voulez les UE d'un parcours, il est plus efficace de passer par
`formation.query_ues_parcour(parcour)`.
"""
return self.query_ues().filter(
FormSemestreInscription.etudid == etudid,
FormSemestreInscription.formsemestre == self,
UniteEns.niveau_competence_id == ApcNiveau.id,
ApcParcoursNiveauCompetence.competence_id == ApcNiveau.competence_id,
ApcParcoursNiveauCompetence.annee_parcours_id == ApcAnneeParcours.id,
or_(
ApcAnneeParcours.parcours_id == FormSemestreInscription.parcour_id,
and_(
FormSemestreInscription.parcour_id.is_(None),
UniteEns.parcour_id.is_(None),
),
),
)
return sem_ues.order_by(UniteEns.numero).all()
@cached_property
def modimpls_sorted(self) -> list[ModuleImpl]:
@ -961,7 +945,7 @@ class FormationModalite(db.Model):
) # code
titre = db.Column(db.Text()) # texte explicatif
# numero = ordre de presentation)
numero = db.Column(db.Integer)
numero = db.Column(db.Integer, nullable=False, default=0)
@staticmethod
def insert_modalites():

View File

@ -7,6 +7,7 @@
"""ScoDoc models: Groups & partitions
"""
from operator import attrgetter
from app import db
from app.models import SHORT_STR_LEN
@ -29,7 +30,7 @@ class Partition(db.Model):
# "TD", "TP", ... (NULL for 'all')
partition_name = db.Column(db.String(SHORT_STR_LEN))
# Numero = ordre de presentation)
numero = db.Column(db.Integer)
numero = db.Column(db.Integer, nullable=False, default=0)
# Calculer le rang ?
bul_show_rank = db.Column(
db.Boolean(), nullable=False, default=False, server_default="false"
@ -92,7 +93,7 @@ class Partition(db.Model):
d.pop("formsemestre", None)
if with_groups:
groups = sorted(self.groups, key=lambda g: (g.numero or 0, g.group_name))
groups = sorted(self.groups, key=attrgetter("numero", "group_name"))
# un dict et non plus une liste, pour JSON
d["groups"] = {
group.id: group.to_dict(with_partition=False) for group in groups
@ -121,7 +122,7 @@ class GroupDescr(db.Model):
# "A", "C2", ... (NULL for 'all'):
group_name = db.Column(db.String(GROUPNAME_STR_LEN))
# Numero = ordre de presentation
numero = db.Column(db.Integer)
numero = db.Column(db.Integer, nullable=False, default=0)
etuds = db.relationship(
"Identite",

View File

@ -33,7 +33,7 @@ class Module(db.Model):
# pas un id mais le numéro du semestre: 1, 2, ...
# note: en APC, le semestre qui fait autorité est celui de l'UE
semestre_id = db.Column(db.Integer, nullable=False, default=1, server_default="1")
numero = db.Column(db.Integer) # ordre de présentation
numero = db.Column(db.Integer, nullable=False, default=0) # ordre de présentation
# id de l'element pedagogique Apogee correspondant:
code_apogee = db.Column(db.String(APO_CODE_STR_LEN))
# Type: ModuleType.STANDARD, MALUS, RESSOURCE, SAE (enum)

View File

@ -21,7 +21,7 @@ class UniteEns(db.Model):
ue_id = db.synonym("id")
formation_id = db.Column(db.Integer, db.ForeignKey("notes_formations.id"))
acronyme = db.Column(db.Text(), nullable=False)
numero = db.Column(db.Integer) # ordre de présentation
numero = db.Column(db.Integer, nullable=False, default=0) # ordre de présentation
titre = db.Column(db.Text())
# Le semestre_idx n'est pas un id mais le numéro du semestre: 1, 2, ...
# En ScoDoc7 et pour les formations classiques, il est NULL
@ -56,11 +56,10 @@ class UniteEns(db.Model):
)
niveau_competence = db.relationship("ApcNiveau", back_populates="ues")
# Une ue appartient soit à tous les parcours (tronc commun), soit à un seul:
parcour_id = db.Column(
db.Integer, db.ForeignKey("apc_parcours.id", ondelete="SET NULL"), index=True
# Une UE appartient soit à tous les parcours (tronc commun), soit à un sous-ensemble
parcours = db.relationship(
ApcParcours, secondary="ue_parcours", backref=db.backref("ues", lazy=True)
)
parcour = db.relationship("ApcParcours", back_populates="ues")
# relations
matieres = db.relationship("Matiere", lazy="dynamic", backref="ue")
@ -115,7 +114,9 @@ class UniteEns(db.Model):
e["ects"] = e["ects"]
e["coefficient"] = e["coefficient"] if e["coefficient"] else 0.0
e["code_apogee"] = e["code_apogee"] or "" # pas de None
e["parcour"] = self.parcour.to_dict(with_annees=False) if self.parcour else None
e["parcours"] = [
parcour.to_dict(with_annees=False) for parcour in self.parcours
]
if with_module_ue_coefs:
if convert_objects:
e["module_ue_coefs"] = [
@ -184,27 +185,86 @@ class UniteEns(db.Model):
return {x.strip() for x in self.code_apogee.split(",") if x}
return set()
def _check_apc_conflict(self, new_niveau_id: int, new_parcour_id: int):
"raises ScoFormationConflict si (niveau, parcours) pas unique dans ce semestre"
# Les UE du même semestre que nous:
ues_sem = self.formation.ues.filter_by(semestre_idx=self.semestre_idx)
if (new_niveau_id, new_parcour_id) in (
(oue.niveau_competence_id, oue.parcour_id)
for oue in ues_sem
if oue.id != self.id
):
log(
f"set_ue_niveau_competence: {self}: ({new_niveau_id}, {new_parcour_id}) déjà associé"
def _parcours_niveaux_ids(self, parcours=list[ApcParcours]) -> set[int]:
"""set des ids de niveaux dans les parcours listés"""
return set.union(
*[
{
n.id
for n in self.niveau_competence.niveaux_annee_de_parcours(
parcour, self.annee(), self.formation.referentiel_competence
)
}
for parcour in parcours
]
)
raise ScoFormationConflict()
def set_niveau_competence(self, niveau: ApcNiveau):
def check_niveau_unique_dans_parcours(
self, niveau: ApcNiveau, parcours=list[ApcParcours]
) -> tuple[bool, str]:
"""Vérifie que
- le niveau est dans au moins l'un des parcours listés;
- et que l'un des parcours associé à cette UE ne contient pas
déjà une UE associée au niveau donné dans une autre année.
Renvoie: (True, "") si ok, sinon (False, message).
"""
# Le niveau est-il dans l'un des parcours listés ?
if parcours:
if niveau.id not in self._parcours_niveaux_ids(parcours):
log(
f"Le niveau {niveau} ne fait pas partie des parcours de l'UE {self}."
)
return (
False,
f"""Le niveau {
niveau.libelle} ne fait pas partie des parcours de l'UE {self.acronyme}.""",
)
for parcour in parcours or [None]:
if parcour is None:
code_parcour = "TC"
ues_meme_niveau = [
ue
for ue in self.formation.query_ues_parcour(None).filter(
UniteEns.niveau_competence == niveau
)
]
else:
code_parcour = parcour.code
ues_meme_niveau = [
ue
for ue in parcour.ues
if ue.formation_id == self.formation_id
and ue.niveau_competence_id == niveau.id
]
if ues_meme_niveau:
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)
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
other_semestre_idx = self.semestre_idx + (
2 * (self.semestre_idx % 2) - 1
)
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)
return False, msg
return True, ""
def set_niveau_competence(self, niveau: ApcNiveau) -> tuple[bool, str]:
"""Associe cette UE au niveau de compétence indiqué.
Le niveau doit être dans le parcours de l'UE, s'il y en a un.
Le niveau doit être dans l'un des parcours de l'UE (si elle n'est pas
de tronc commun).
Assure que ce soit la seule dans son parcours.
Sinon, raises ScoFormationConflict.
Si niveau est None, désassocie.
Returns True if (de)association done, False on error.
"""
if niveau.id == self.niveau_competence_id:
return True # nothing to do
@ -215,41 +275,52 @@ class UniteEns(db.Model):
if not ok:
return ok, error_message
self.niveau_competence = niveau
db.session.add(self)
db.session.commit()
# Invalidation du cache
self.formation.invalidate_cached_sems()
log(f"ue.set_niveau_competence( {self}, {niveau} )")
return True, ""
def set_parcour(self, parcour: ApcParcours):
"""Associe cette UE au parcours indiqué.
Assure que ce soit la seule dans son parcours.
Sinon, raises ScoFormationConflict.
Si niveau est None, désassocie.
def set_parcours(self, parcours: list[ApcParcours]) -> tuple[bool, str]:
"""Associe cette UE aux parcours indiqués.
Si un niveau est déjà associé, vérifie sa cohérence.
Renvoie (True, "") si ok, sinon (False, error_message)
"""
if (parcour is not None) and self.niveau_competence is not None:
self._check_apc_conflict(self.niveau_competence.id, parcour.id)
self.parcour = parcour
# Le niveau est-il dans ce parcours ? Sinon, l'enlève
# breakpoint()
if (
parcour
parcours
and self.niveau_competence
and self.niveau_competence.id
not in (
n.id
for n in self.niveau_competence.niveaux_annee_de_parcours(
parcour, self.annee(), self.formation.referentiel_competence
)
)
and self.niveau_competence.id not in self._parcours_niveaux_ids(parcours)
):
self.niveau_competence = None
if parcours and self.niveau_competence:
ok, error_message = self.check_niveau_unique_dans_parcours(
self.niveau_competence, parcours
)
if not ok:
return False, error_message
self.parcours = parcours
db.session.add(self)
db.session.commit()
# Invalidation du cache
self.formation.invalidate_cached_sems()
log(f"ue.set_parcour( {self}, {parcour} )")
log(f"ue.set_parcours( {self}, {parcours} )")
return True, ""
class UEParcours(db.Model):
"""Association ue <-> parcours, indiquant les ECTS"""
__tablename__ = "ue_parcours"
ue_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id"), primary_key=True)
parcours_id = db.Column(
db.Integer, db.ForeignKey("apc_parcours.id"), primary_key=True
)
ects = db.Column(db.Float, nullable=True) # si NULL, on prendra les ECTS de l'UE
class DispenseUE(db.Model):

View File

@ -1009,10 +1009,7 @@ class ApoData(object):
]
)
codes_ues = set().union(
*[
ue.get_codes_apogee()
for ue in formsemestre.query_ues(with_sport=True)
]
*[ue.get_codes_apogee() for ue in formsemestre.get_ues(with_sport=True)]
)
s = set()
codes_by_sem[sem["formsemestre_id"]] = s

View File

@ -107,7 +107,7 @@ def html_edit_formation_apc(
icons=icons,
ues_by_sem=ues_by_sem,
ects_by_sem=ects_by_sem,
form_ue_choix_niveau=apc_edit_ue.form_ue_choix_niveau,
form_ue_choix_parcours_niveau=apc_edit_ue.form_ue_choix_parcours_niveau,
),
]
for semestre_idx in semestre_ids:

View File

@ -738,8 +738,10 @@ def ue_table(formation_id=None, semestre_idx=1, msg=""): # was ue_list
)
H = [
html_sco_header.sco_header(
cssstyles=["libjs/jQuery-tagEditor/jquery.tag-editor.css"],
javascripts=[
cssstyles=html_sco_header.BOOTSTRAP_MULTISELECT_CSS
+ ["libjs/jQuery-tagEditor/jquery.tag-editor.css"],
javascripts=html_sco_header.BOOTSTRAP_MULTISELECT_JS
+ [
"libjs/jinplace-1.2.1.min.js",
"js/ue_list.js",
"js/edit_ue.js",

View File

@ -79,7 +79,7 @@ def evaluation_create_form(
mod = modimpl_o["module"]
formsemestre_id = modimpl_o["formsemestre_id"]
formsemestre = modimpl.formsemestre
sem_ues = formsemestre.query_ues(with_sport=False).all()
sem_ues = formsemestre.get_ues(with_sport=False)
is_malus = mod["module_type"] == ModuleType.MALUS
is_apc = mod["module_type"] in (ModuleType.RESSOURCE, ModuleType.SAE)
preferences = sco_preferences.SemPreferences(formsemestre.id)

View File

@ -128,8 +128,10 @@ def formation_export_dict(
ue_dict["apc_niveau_annee"] = ue.niveau_competence.annee
ue_dict["apc_niveau_ordre"] = ue.niveau_competence.ordre
# Et le parcour:
if ue.parcour:
ue_dict["parcour"] = [ue.parcour.to_dict(with_annees=False)]
if ue.parcours:
ue_dict["parcours"] = [
parcour.to_dict(with_annees=False) for parcour in ue.parcours
]
# pour les coefficients:
ue_dict["reference"] = ue.id if ue_reference_style == "id" else ue.acronyme
if not export_ids:
@ -372,6 +374,7 @@ def formation_import_xml(doc: str, import_tags=True, use_local_refcomp=False):
# -- create matieres
for mat_info in ue_info[2]:
# Backward compat: un seul parcours par UE (ScoDoc < 9.4.71)
if mat_info[0] == "parcour":
# Parcours (BUT)
code_parcours = mat_info[1]["code"]
@ -380,11 +383,28 @@ def formation_import_xml(doc: str, import_tags=True, use_local_refcomp=False):
referentiel_id=referentiel_competence_id,
).first()
if parcour:
ue.parcour = parcour
ue.parcours = [parcour]
db.session.add(ue)
else:
flash(f"Attention: parcours {code_parcours} inexistant !")
log(f"Warning: parcours {code_parcours} inexistant !")
continue
elif mat_info[0] == "parcours":
# Parcours (BUT), liste (ScoDoc > 9.4.70)
codes_parcours = mat_info[1]["code"]
for code_parcours in codes_parcours:
parcour = ApcParcours.query.filter_by(
code=code_parcours,
referentiel_id=referentiel_competence_id,
).first()
if parcour:
ue.parcours.append(parcour)
else:
flash(f"Attention: parcours {code_parcours} inexistant !")
log(f"Warning: parcours {code_parcours} inexistant !")
db.session.add(ue)
continue
assert mat_info[0] == "matiere"
mat_info[1]["ue_id"] = ue_id
mat_id = sco_edit_matiere.do_matiere_create(mat_info[1])

View File

@ -37,6 +37,7 @@ from flask import flash, redirect, render_template, url_for
from flask_login import current_user
from app import log
from app.but.cursus_but import formsemestre_warning_apc_setup
from app.comp import res_sem
from app.comp.res_common import ResultatsSemestre
from app.comp.res_compat import NotesTableCompat
@ -604,7 +605,7 @@ def formsemestre_description_table(
columns_ids += ["Coef."]
ues = [] # liste des UE, seulement en APC pour les coefs
else:
ues = formsemestre.query_ues().all()
ues = formsemestre.get_ues()
columns_ids += [f"ue_{ue.id}" for ue in ues]
if sco_preferences.get_preference("bul_show_ects", formsemestre_id):
columns_ids += ["ects"]
@ -1057,6 +1058,7 @@ def formsemestre_status(formsemestre_id=None, check_parcours=True):
formsemestre_status_head(
formsemestre_id=formsemestre_id, page_title="Tableau de bord"
),
formsemestre_warning_apc_setup(formsemestre, nt),
formsemestre_warning_etuds_sans_note(formsemestre, nt)
if can_change_all_notes
else "",
@ -1282,7 +1284,7 @@ def formsemestre_tableau_modules(
"""
)
if mod.module_type in (ModuleType.RESSOURCE, ModuleType.SAE):
coefs = mod.ue_coefs_list(ues=formsemestre.query_ues().all())
coefs = mod.ue_coefs_list(ues=formsemestre.get_ues())
H.append(f'<a class="invisible_link" href="#" title="{mod_descr}">')
for coef in coefs:
if coef[1] > 0:

View File

@ -606,7 +606,9 @@ def formsemestre_recap_parcours_table(
else:
# si l'étudiant n'est pas inscrit à un parcours mais que le semestre a plus d'UE
# signale un éventuel problème:
if nt.formsemestre.query_ues().count() > len(nt.etud_ues_ids(etudid)):
if len(nt.formsemestre.get_ues()) > len(
nt.etud_ues_ids(etudid)
): # XXX sans dispenses
parcours_name = f"""
<span class="code_parcours no_parcours">{scu.EMO_WARNING}&nbsp;pas de parcours
</span>"""

View File

@ -982,8 +982,8 @@ def icontag(name, file_format="png", no_size=False, **attrs):
file_format,
),
)
im = PILImage.open(img_file)
width, height = im.size[0], im.size[1]
with PILImage.open(img_file) as image:
width, height = image.size[0], image.size[1]
ICONSIZES[name] = (width, height) # cache
else:
width, height = ICONSIZES[name]

View File

@ -89,7 +89,7 @@ function update_menus_niveau_competence() {
// );
// nouveau:
document.querySelectorAll("select.select_niveau_ue").forEach(
document.querySelectorAll("select.niveau_select").forEach(
elem => {
let ue_id = elem.dataset.ue_id;
$.get("get_ue_niveaux_options_html",
@ -103,3 +103,65 @@ function update_menus_niveau_competence() {
}
);
}
// ---- Nouveau formulaire choix parcours et niveau -----
//document.querySelectorAll("select.select_ue_parcours").forEach(
// elem => { elem.addEventListener('change', change_ue_parcours); }
//);
$().ready(function () {
$('select.select_ue_parcours').multiselect(
{
includeSelectAllOption: false,
nonSelectedText: 'choisir...',
// buttonContainer: '<div id="group_ids_sel_container"/>',
onChange: function (element, checked) {
var parent = element.parent();
var selectedOptions = parent.getValue().split(",");
let set_ue_parcours = element.context.dataset.set_ue_parcours;
fetch(set_ue_parcours, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(selectedOptions)
})
.then(response => response.json())
.then(data => {
if (!data.status) {
sco_message(data.message);
// get the option element corresponding to the selected value
var option = parent.find('option[value="' + element.val() + '"]');
// uncheck the option
option.prop('selected', false);
// refresh the multiselect to reflect the change
parent.multiselect('refresh');
}
})
.catch(error => console.error('Error: ' + error));
// // referme le menu apres chaque choix:
// $("#group_selector .btn-group").removeClass('open');
// if ($("#group_ids_sel").hasClass("submit_on_change")) {
// submit_group_selector();
// }
}
}
);
});
function change_ue_parcours(event) {
const multiselect = event.target;
const selectedOptions = Array.from(this.selectedOptions).map(option => option.value);
fetch('/set_option/', { // XXX TODO
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(selectedOptions)
})
.then(response => response.json())
.then(data => console.log('Success!'))
.catch(error => console.error('Error: ' + error));
};

View File

@ -74,7 +74,7 @@ class TableRecap(tb.Table):
# couples (modimpl, ue) effectivement présents dans la table:
self.modimpl_ue_ids = set()
ues = res.formsemestre.query_ues(with_sport=True) # avec bonus
ues = res.formsemestre.get_ues(with_sport=True) # avec bonus
ues_sans_bonus = [ue for ue in ues if ue.type != UE_SPORT]
if res.formsemestre.etuds_inscriptions: # table non vide

View File

@ -65,7 +65,7 @@
}}">modifier</a>
{% endif %}
{{ form_ue_choix_niveau(ue)|safe }}
{{ form_ue_choix_parcours_niveau(ue)|safe }}
{% if ue.type == 1 and ue.modules.count() == 0 %}

View File

@ -0,0 +1,13 @@
{# inclu par form_ues.j2 #}
<form method="POST" action="">
{{ form_ue_parcours_niveau.csrf_token }}
<div class="form-group">
{{ form_ue_parcours_niveau.niveau_select.label }}
{{ form_ue_parcours_niveau.niveau_select }}
{{ form_ue_parcours_niveau.parcours_multiselect.label }}
{{ form_ue_parcours_niveau.parcours_multiselect }}
</div>
</form>

View File

@ -421,25 +421,6 @@ def set_ue_niveau_competence():
return "", 204
@bp.route("/set_ue_parcours", methods=["POST"])
@scodoc
@permission_required(Permission.ScoChangeFormation)
def set_ue_parcours():
"""Associe UE et parcours BUT.
Si le parcour_id est "", désassocie."""
ue_id = request.form.get("ue_id")
parcour_id = request.form.get("parcour_id")
if parcour_id == "":
parcour_id = None
ue: UniteEns = UniteEns.query.get_or_404(ue_id)
parcour = None if parcour_id is None else ApcParcours.query.get_or_404(parcour_id)
try:
ue.set_parcour(parcour)
except ScoFormationConflict:
return "", 409 # conflict
return "", 204
@bp.route("/get_ue_niveaux_options_html")
@scodoc
@permission_required(Permission.ScoView)
@ -448,6 +429,9 @@ def get_ue_niveaux_options_html():
niveau de compétences associé à une UE
"""
ue_id = request.args.get("ue_id")
if ue_id is None:
log("WARNING: get_ue_niveaux_options_html missing ue_id arg")
return "???"
ue: UniteEns = UniteEns.query.get_or_404(ue_id)
return apc_edit_ue.get_ue_niveaux_options_html(ue)

View File

@ -79,7 +79,7 @@ def table_modules_ue_coefs(formation_id, semestre_idx=None, parcours_id: int = N
ues = [
ue
for ue in ues
if (parcours_id == ue.parcour_id) or (ue.parcour_id is None)
if (parcours_id in (p.id for p in ue.parcours)) or (not ue.parcours)
]
modules = [
mod
@ -113,13 +113,14 @@ def table_modules_ue_coefs(formation_id, semestre_idx=None, parcours_id: int = N
cells = []
for (row, mod) in enumerate(modules, start=2):
style = "champs champs_" + scu.ModuleType(mod.module_type).name
mod_parcours_ids = {p.id for p in mod.parcours}
for (col, ue) in enumerate(ues, start=2):
# met en gris les coefs qui devraient être nuls
# car le module n'est pas dans le parcours de l'UE:
if (
(mod.parcours is not None)
and (ue.parcour_id is not None)
and ue.parcour_id not in (p.id for p in mod.parcours)
and (ue.parcours)
and not {p.id for p in ue.parcours}.intersection(mod_parcours_ids)
):
cell_style = style + " champs_coef_hors_parcours"
else:

View File

@ -0,0 +1,98 @@
"""Association UEs <-> parcours
Revision ID: 054dd6133b9c
Revises: 6520faf67508
Create Date: 2023-03-30 19:40:50.575293
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.orm import sessionmaker # added by ev
# revision identifiers, used by Alembic.
revision = "054dd6133b9c"
down_revision = "6520faf67508"
branch_labels = None
depends_on = None
Session = sessionmaker()
def upgrade():
"""Passe d'une relation UE - Parcours one-to-many à une relation many-to-many
crée la table d'association, copie l'éventuelle relation existante
puis supprime la clé étrangère parcour_id
"""
op.create_table(
"ue_parcours",
sa.Column("ue_id", sa.Integer(), nullable=False),
sa.Column("parcours_id", sa.Integer(), nullable=False),
sa.Column("ects", sa.Float(), nullable=True),
sa.ForeignKeyConstraint(
["parcours_id"],
["apc_parcours.id"],
),
sa.ForeignKeyConstraint(
["ue_id"],
["notes_ue.id"],
),
sa.PrimaryKeyConstraint("ue_id", "parcours_id"),
)
#
bind = op.get_bind()
session = Session(bind=bind)
session.execute(
sa.text(
"""
INSERT INTO ue_parcours
SELECT id as ue_id, parcour_id as parcours_id
FROM notes_ue
WHERE parcour_id is not NULL;
"""
)
)
session.commit()
op.drop_column("notes_ue", "parcour_id")
# Numeros non nullables
for table in (
"apc_competence",
"apc_parcours",
"notes_form_modalites",
"notes_ue",
"notes_matieres",
"notes_modules",
"notes_evaluation",
"partition",
"group_descr",
):
session.execute(
sa.text(
f"""UPDATE {table} SET numero=0 WHERE numero is NULL;
"""
)
)
session.commit()
op.alter_column(table, "numero", existing_type=sa.INTEGER(), nullable=False)
def downgrade():
#
op.add_column(
"notes_ue",
sa.Column("parcour_id", sa.INTEGER(), autoincrement=False, nullable=True),
)
op.drop_table("ue_parcours")
for table in (
"apc_competence",
"apc_parcours",
"notes_form_modalites",
"notes_ue",
"notes_matieres",
"notes_modules",
"notes_evaluation",
"partition",
"group_descr",
):
op.alter_column(table, "numero", existing_type=sa.INTEGER(), nullable=True)

View File

@ -87,15 +87,15 @@ Formation:
competence: "Solutions TP"
'UE5.3':
annee: BUT3
parcours: RAPEB # + BEC
parcours: [RAPEB, BEC]
competence: "Dimensionner"
'UE5.4':
annee: BUT3
parcours: BAT # + TP
parcours: [BAT, TP]
competence: Organiser
'UE5.5':
annee: BUT3
parcours: BAT # + TP
parcours: [BAT, TP]
competence: Piloter
# S6 Parcours BAT + TP
'UE6.1': # Parcours BAT seulement
@ -104,19 +104,19 @@ Formation:
competence: "Solutions Bâtiment"
'UE6.2': # Parcours TP seulement
annee: BUT3
parcours: TP # + BEC
parcours: [TP,BEC]
competence: "Solutions TP"
'UE6.3':
annee: BUT3
parcours: RAPEB # + BEC
parcours: [RAPEB,BEC]
competence: "Dimensionner"
'UE6.4':
annee: BUT3
parcours: BAT # + TP
parcours: [BAT, TP]
competence: Organiser
'UE6.5':
annee: BUT3
parcours: BAT # + TP
parcours: [BAT,TP]
competence: Piloter
modules_parcours:

View File

@ -111,7 +111,7 @@ def build_modules_with_evaluations(
modimpl = models.ModuleImpl.query.get(moduleimpl_id)
assert modimpl.formsemestre.formation.get_cursus().APC_SAE # BUT
# Check ModuleImpl
ues = modimpl.formsemestre.query_ues().all()
ues = modimpl.formsemestre.get_ues()
assert len(ues) == 3
#
for _ in range(nb_evals_per_modimpl):

View File

@ -24,7 +24,7 @@ from tests.unit import yaml_setup, yaml_setup_but
import app
from app.but.jury_but_validation_auto import formsemestre_validation_auto_but
from app.models import Formation, FormSemestre
from app.models import Formation, FormSemestre, UniteEns
from config import TestConfig
DEPT = TestConfig.DEPT_TEST
@ -133,7 +133,11 @@ def test_but_jury_GCCD_CY(test_client):
assert parcour_BAT
# check le nombre d'UE dans chaque semestre BUT:
assert [
len(formation.query_ues_parcour(parcour_BAT).filter_by(semestre_idx=i).all())
len(
formation.query_ues_parcour(parcour_BAT)
.filter(UniteEns.semestre_idx == i)
.all()
)
for i in range(1, 7)
] == [5, 5, 5, 5, 3, 3]
# Vérifie les UEs du parcours TP
@ -141,6 +145,10 @@ def test_but_jury_GCCD_CY(test_client):
assert parcour_TP
# check le nombre d'UE dans chaque semestre BUT:
assert [
len(formation.query_ues_parcour(parcour_TP).filter_by(semestre_idx=i).all())
len(
formation.query_ues_parcour(parcour_TP)
.filter(UniteEns.semestre_idx == i)
.all()
)
for i in range(1, 7)
] == [5, 5, 5, 5, 3, 3]

View File

@ -30,6 +30,9 @@ REF_MLT_XML = open(
REF_GCCD_XML = open(
"ressources/referentiels/but2022/competences/but-GCCD-05012022-081630.xml"
).read()
REF_INFO_XML = open(
"ressources/referentiels/but2022/competences/but-INFO-05012022-081701.xml"
).read()
def test_but_refcomp(test_client):
@ -125,20 +128,20 @@ def test_refcomp_niveaux_mlt(test_client):
# Vérifier les niveaux_by_parcours
parcour = ref_comp.parcours.first()
# BUT 1
parcours, niveaux_by_parcours = ref_comp.get_niveaux_by_parcours(1, parcour)
parcours, niveaux_by_parcours = ref_comp.get_niveaux_by_parcours(1, [parcour])
assert parcours == [parcour] # le parcours indiqué
assert (tuple(niveaux_by_parcours.keys())) == (parcour.id, "TC")
assert niveaux_by_parcours[parcour.id] == [] # tout en tronc commun en BUT1 MLT
assert niveaux_by_parcours["TC"][0].competence.titre == "Transporter"
assert len(niveaux_by_parcours["TC"]) == 3
# BUT 2
parcours, niveaux_by_parcours = ref_comp.get_niveaux_by_parcours(2, parcour)
parcours, niveaux_by_parcours = ref_comp.get_niveaux_by_parcours(2, [parcour])
assert parcours == [parcour] # le parcours indiqué
assert (tuple(niveaux_by_parcours.keys())) == (parcour.id, "TC")
assert len(niveaux_by_parcours[parcour.id]) == 1
assert len(niveaux_by_parcours["TC"]) == 3
# BUT 3
parcours, niveaux_by_parcours = ref_comp.get_niveaux_by_parcours(3, parcour)
parcours, niveaux_by_parcours = ref_comp.get_niveaux_by_parcours(3, [parcour])
assert parcours == [parcour] # le parcours indiqué
assert (tuple(niveaux_by_parcours.keys())) == (parcour.id, "TC")
assert len(niveaux_by_parcours[parcour.id]) == 1
@ -182,13 +185,13 @@ def test_refcomp_niveaux_gccd(test_client):
# Vérifier les niveaux_by_parcours
parcour = ref_comp.parcours.first()
# BUT 1
parcours, niveaux_by_parcours = ref_comp.get_niveaux_by_parcours(1, parcour)
parcours, niveaux_by_parcours = ref_comp.get_niveaux_by_parcours(1, [parcour])
assert parcours == [parcour] # le parcours indiqué
assert (tuple(niveaux_by_parcours.keys())) == (parcour.id, "TC")
assert len(niveaux_by_parcours[parcour.id]) == 0
assert len(niveaux_by_parcours["TC"]) == 5
# BUT 3
parcours, niveaux_by_parcours = ref_comp.get_niveaux_by_parcours(3, parcour)
parcours, niveaux_by_parcours = ref_comp.get_niveaux_by_parcours(3, [parcour])
assert parcours == [parcour] # le parcours indiqué
assert (tuple(niveaux_by_parcours.keys())) == (parcour.id, "TC")
assert len(niveaux_by_parcours[parcour.id]) == 3

View File

@ -81,11 +81,23 @@ def associe_ues_et_parcours(formation: Formation, formation_infos: dict):
assert ue is not None # l'UE doit exister dans la formation avec cet acronyme
# Parcours:
if ue_infos.get("parcours", False):
parcour = referentiel_competence.parcours.filter_by(
# On peut spécifier un seul parcours (cas le plus fréquent) ou une liste
if isinstance(ue_infos["parcours"], list):
parcours = [
referentiel_competence.parcours.filter_by(code=code_parcour).first()
for code_parcour in ue_infos["parcours"]
]
assert (
None not in parcours
) # les parcours indiqués pour cette UE doivent exister
else:
parcours = referentiel_competence.parcours.filter_by(
code=ue_infos["parcours"]
).first()
assert parcour is not None # le parcours indiqué pour cette UE doit exister
ue.set_parcour(parcour)
).all()
assert (
len(parcours) == 1
) # le parcours indiqué pour cette UE doit exister
ue.set_parcours(parcours)
# Niveaux compétences:
competence = referentiel_competence.competences.filter_by(
@ -258,12 +270,19 @@ def check_deca_fields(formsemestre: FormSemestre, etud: Identite = None):
assert deca.validation is None # pas encore de validation enregistrée
assert False is deca.recorded
assert deca.code_valide is None
parcour = deca.parcour
formation: Formation = formsemestre.formation
ues = (
formation.query_ues_parcour(parcour)
.filter(UniteEns.semestre_idx == formsemestre.semestre_id)
.all()
)
if formsemestre.semestre_id % 2:
assert deca.formsemestre_impair == formsemestre
assert formsemestre.query_ues_parcours_etud(etud.id).all() == deca.ues_impair
assert ues == deca.ues_impair
else:
assert deca.formsemestre_pair == formsemestre
assert formsemestre.query_ues_parcours_etud(etud.id).all() == deca.ues_pair
assert ues == deca.ues_pair
assert deca.inscription_etat == scu.INSCRIT
assert deca.inscription_etat_impair == scu.INSCRIT
assert (deca.parcour is None) or (
@ -271,24 +290,27 @@ def check_deca_fields(formsemestre: FormSemestre, etud: Identite = None):
)
nb_ues = (
len(deca.formsemestre_pair.query_ues_parcours_etud(etud.id).all())
len(
formation.query_ues_parcour(parcour)
.filter(UniteEns.semestre_idx == deca.formsemestre_pair.semestre_id)
.all()
)
if deca.formsemestre_pair
else 0
)
nb_ues += (
len(deca.formsemestre_impair.query_ues_parcours_etud(etud.id).all())
len(
formation.query_ues_parcour(parcour)
.filter(UniteEns.semestre_idx == deca.formsemestre_impair.semestre_id)
.all()
)
if deca.formsemestre_impair
else 0
)
assert len(deca.decisions_ues) == nb_ues
nb_ues_un_sem = (
len(deca.formsemestre_impair.query_ues_parcours_etud(etud.id).all())
if deca.formsemestre_impair
else len(deca.formsemestre_pair.query_ues_parcours_etud(etud.id).all())
)
assert len(deca.niveaux_competences) == nb_ues_un_sem
assert deca.nb_competences == nb_ues_un_sem
assert len(deca.niveaux_competences) == len(ues)
assert deca.nb_competences == len(ues)
def but_test_jury(formsemestre: FormSemestre, doc: dict):

View File

@ -262,7 +262,7 @@ def saisie_notes_evaluations(formsemestre: FormSemestre, user: User):
date_debut = formsemestre.date_debut
date_fin = formsemestre.date_fin
list_ues = formsemestre.query_ues()
list_ues = formsemestre.get_ues()
def saisir_notes(evaluation_id: int, condition: int):
"""