forked from ScoDoc/ScoDoc
WIP: associations UEs / Competences, ref. comp., tests, refactoring.
This commit is contained in:
parent
c6e1a16b99
commit
dfa453768d
@ -12,6 +12,7 @@ import traceback
|
||||
import logging
|
||||
from logging.handlers import SMTPHandler, WatchedFileHandler
|
||||
from threading import Thread
|
||||
import warnings
|
||||
|
||||
from flask import current_app, g, request
|
||||
from flask import Flask
|
||||
@ -254,6 +255,8 @@ def create_app(config_class=DevConfig):
|
||||
# Evite de logguer toutes les requetes dans notre log
|
||||
logging.getLogger("werkzeug").disabled = True
|
||||
app.logger.setLevel(app.config["LOG_LEVEL"])
|
||||
if app.config["TESTING"] or app.config["DEBUG"]:
|
||||
warnings.filterwarnings("error")
|
||||
|
||||
# Vérifie/crée lien sym pour les URL statiques
|
||||
link_filename = f"{app.root_path}/static/links/{sco_version.SCOVERSION}"
|
||||
|
@ -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})
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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 :</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)
|
||||
|
@ -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(
|
||||
@ -185,7 +186,7 @@ class EtudCursusBUT:
|
||||
"""
|
||||
{
|
||||
competence_id : {
|
||||
annee : { validation}
|
||||
annee : { validation }
|
||||
}
|
||||
}
|
||||
où validation est un petit dict avec niveau_id, etc.
|
||||
@ -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>
|
||||
"""
|
||||
|
@ -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()
|
||||
)
|
||||
|
@ -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,
|
||||
|
@ -409,7 +409,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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
39
app/forms/formation/ue_parcours_niveau.py
Normal file
39
app/forms/formation/ue_parcours_niveau.py
Normal 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)
|
@ -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(
|
||||
|
@ -6,6 +6,7 @@
|
||||
"""ScoDoc 9 models : Référentiel Compétence BUT 2021
|
||||
"""
|
||||
from datetime import datetime
|
||||
from operator import attrgetter
|
||||
|
||||
import flask_sqlalchemy
|
||||
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
|
||||
@ -523,7 +522,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(
|
||||
@ -532,7 +531,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}>"
|
||||
|
@ -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")
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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) -> flask_sqlalchemy.BaseQuery:
|
||||
"""Les UEs d'un parcours de la formation.
|
||||
def query_ues_parcour(
|
||||
self, parcour: ApcParcours, with_sport: bool = False
|
||||
) -> flask_sqlalchemy.BaseQuery:
|
||||
"""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 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,
|
||||
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 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
|
||||
@ -281,7 +293,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")
|
||||
|
||||
|
@ -12,6 +12,7 @@
|
||||
"""
|
||||
import datetime
|
||||
from functools import cached_property
|
||||
from operator import attrgetter
|
||||
|
||||
from flask_login import current_user
|
||||
import flask_sqlalchemy
|
||||
@ -281,60 +282,43 @@ class FormSemestre(db.Model):
|
||||
)
|
||||
return r or []
|
||||
|
||||
def query_ues(self, with_sport=False) -> flask_sqlalchemy.BaseQuery:
|
||||
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
|
||||
)
|
||||
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]))
|
||||
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)
|
||||
}
|
||||
)
|
||||
# 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,
|
||||
Module.id == ModuleImpl.module_id,
|
||||
UniteEns.id == Module.ue_id,
|
||||
)
|
||||
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) -> flask_sqlalchemy.BaseQuery:
|
||||
"""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),
|
||||
),
|
||||
),
|
||||
)
|
||||
if not with_sport:
|
||||
sem_ues = sem_ues.filter(UniteEns.type != codes_cursus.UE_SPORT)
|
||||
return sem_ues.order_by(UniteEns.numero).all()
|
||||
|
||||
@cached_property
|
||||
def modimpls_sorted(self) -> list[ModuleImpl]:
|
||||
@ -960,7 +944,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():
|
||||
|
@ -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",
|
||||
|
@ -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)
|
||||
|
@ -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,78 +185,142 @@ 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é"
|
||||
)
|
||||
raise ScoFormationConflict()
|
||||
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
|
||||
]
|
||||
)
|
||||
|
||||
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
|
||||
if niveau is not None:
|
||||
self._check_apc_conflict(niveau.id, self.parcour_id)
|
||||
# Le niveau est-il dans le parcours ? Sinon, erreur
|
||||
if self.parcour and niveau.id not in (
|
||||
n.id
|
||||
for n in niveau.niveaux_annee_de_parcours(
|
||||
self.parcour, self.annee(), self.formation.referentiel_competence
|
||||
)
|
||||
):
|
||||
log(
|
||||
f"set_niveau_competence: niveau {niveau} hors parcours {self.parcour}"
|
||||
)
|
||||
return
|
||||
|
||||
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()
|
||||
# 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):
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -737,8 +737,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",
|
||||
|
@ -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)
|
||||
|
@ -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])
|
||||
|
@ -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:
|
||||
|
@ -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} pas de parcours
|
||||
</span>"""
|
||||
|
@ -992,8 +992,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]
|
||||
|
@ -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));
|
||||
};
|
||||
|
@ -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
|
||||
|
@ -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 %}
|
||||
|
13
app/templates/pn/ue_choix_parcours_niveau.j2
Normal file
13
app/templates/pn/ue_choix_parcours_niveau.j2
Normal 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>
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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:
|
||||
|
98
migrations/versions/054dd6133b9c_association_ues_parcours.py
Normal file
98
migrations/versions/054dd6133b9c_association_ues_parcours.py
Normal 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)
|
@ -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:
|
||||
|
@ -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):
|
||||
|
@ -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]
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
code=ue_infos["parcours"]
|
||||
).first()
|
||||
assert parcour is not None # le parcours indiqué pour cette UE doit exister
|
||||
ue.set_parcour(parcour)
|
||||
# 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"]
|
||||
).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):
|
||||
|
@ -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):
|
||||
"""
|
||||
|
Loading…
Reference in New Issue
Block a user