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
|
import logging
|
||||||
from logging.handlers import SMTPHandler, WatchedFileHandler
|
from logging.handlers import SMTPHandler, WatchedFileHandler
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
|
import warnings
|
||||||
|
|
||||||
from flask import current_app, g, request
|
from flask import current_app, g, request
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
@ -254,6 +255,8 @@ def create_app(config_class=DevConfig):
|
|||||||
# Evite de logguer toutes les requetes dans notre log
|
# Evite de logguer toutes les requetes dans notre log
|
||||||
logging.getLogger("werkzeug").disabled = True
|
logging.getLogger("werkzeug").disabled = True
|
||||||
app.logger.setLevel(app.config["LOG_LEVEL"])
|
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
|
# Vérifie/crée lien sym pour les URL statiques
|
||||||
link_filename = f"{app.root_path}/static/links/{sco_version.SCOVERSION}"
|
link_filename = f"{app.root_path}/static/links/{sco_version.SCOVERSION}"
|
||||||
|
@ -8,17 +8,17 @@
|
|||||||
ScoDoc 9 API : accès aux formations
|
ScoDoc 9 API : accès aux formations
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from flask import g, jsonify
|
from flask import g, jsonify, request
|
||||||
from flask_login import login_required
|
from flask_login import login_required
|
||||||
|
|
||||||
import app
|
import app
|
||||||
|
from app import log
|
||||||
from app.api import api_bp as bp, api_web_bp
|
from app.api import api_bp as bp, api_web_bp
|
||||||
from app.scodoc.sco_utils import json_error
|
from app.scodoc.sco_utils import json_error
|
||||||
from app.decorators import scodoc, permission_required
|
from app.decorators import scodoc, permission_required
|
||||||
from app.models.formations import Formation
|
from app.models import ApcParcours, Formation, FormSemestre, ModuleImpl, UniteEns
|
||||||
from app.models.formsemestre import FormSemestre
|
|
||||||
from app.models.moduleimpls import ModuleImpl
|
|
||||||
from app.scodoc import sco_formations
|
from app.scodoc import sco_formations
|
||||||
|
from app.scodoc.sco_exceptions import ScoFormationConflict
|
||||||
from app.scodoc.sco_permissions import Permission
|
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",
|
"abbrev": "Hygi\u00e8ne informatique",
|
||||||
"code": "SAE11",
|
"code": "SAE11",
|
||||||
"heures_cours": 0.0,
|
"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)
|
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
||||||
modimpl: ModuleImpl = query.first_or_404()
|
modimpl: ModuleImpl = query.first_or_404()
|
||||||
return jsonify(modimpl.to_dict(convert_objects=True))
|
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
|
ScoDoc 9 API : accès aux formsemestres
|
||||||
"""
|
"""
|
||||||
|
from operator import attrgetter, itemgetter
|
||||||
|
|
||||||
from flask import g, jsonify, request
|
from flask import g, jsonify, request
|
||||||
from flask_login import login_required
|
from flask_login import login_required
|
||||||
|
|
||||||
@ -254,7 +256,7 @@ def formsemestre_programme(formsemestre_id: int):
|
|||||||
if g.scodoc_dept:
|
if g.scodoc_dept:
|
||||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||||
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
|
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
|
||||||
ues = formsemestre.query_ues()
|
ues = formsemestre.get_ues()
|
||||||
m_list = {
|
m_list = {
|
||||||
ModuleType.RESSOURCE: [],
|
ModuleType.RESSOURCE: [],
|
||||||
ModuleType.SAE: [],
|
ModuleType.SAE: [],
|
||||||
@ -345,7 +347,7 @@ def formsemestre_etudiants(
|
|||||||
etud["id"], formsemestre_id, exclude_default=True
|
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")
|
@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
|
# Si il y a plus d'une note saisie pour l'évaluation
|
||||||
if len(notes) >= 1:
|
if len(notes) >= 1:
|
||||||
# Tri des notes en fonction de leurs dates
|
# 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_debut = notes_sorted[0].date
|
||||||
date_fin = notes_sorted[-1].date
|
date_fin = notes_sorted[-1].date
|
||||||
|
@ -7,6 +7,8 @@
|
|||||||
"""
|
"""
|
||||||
ScoDoc 9 API : partitions
|
ScoDoc 9 API : partitions
|
||||||
"""
|
"""
|
||||||
|
from operator import attrgetter
|
||||||
|
|
||||||
from flask import g, jsonify, request
|
from flask import g, jsonify, request
|
||||||
from flask_login import login_required
|
from flask_login import login_required
|
||||||
|
|
||||||
@ -85,7 +87,7 @@ def formsemestre_partitions(formsemestre_id: int):
|
|||||||
if g.scodoc_dept:
|
if g.scodoc_dept:
|
||||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||||
formsemestre: FormSemestre = query.first_or_404(formsemestre_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(
|
return jsonify(
|
||||||
{
|
{
|
||||||
partition.id: partition.to_dict(with_groups=True)
|
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",
|
message="paramètre liste des partitions invalide",
|
||||||
)
|
)
|
||||||
for p_id, numero in zip(partition_ids, range(len(partition_ids))):
|
for p_id, numero in zip(partition_ids, range(len(partition_ids))):
|
||||||
p = Partition.query.get_or_404(p_id)
|
partition = Partition.query.get_or_404(p_id)
|
||||||
p.numero = numero
|
partition.numero = numero
|
||||||
db.session.add(p)
|
db.session.add(partition)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
app.set_sco_dept(formsemestre.departement.acronym)
|
app.set_sco_dept(formsemestre.departement.acronym)
|
||||||
sco_cache.invalidate_formsemestre(formsemestre_id)
|
sco_cache.invalidate_formsemestre(formsemestre_id)
|
||||||
|
@ -7,9 +7,10 @@
|
|||||||
"""
|
"""
|
||||||
Edition associations UE <-> Ref. Compétence
|
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.models import ApcReferentielCompetences, UniteEns
|
||||||
from app.scodoc import codes_cursus
|
from app.scodoc import codes_cursus
|
||||||
|
from app.forms.formation.ue_parcours_niveau import UEParcoursNiveauForm
|
||||||
|
|
||||||
|
|
||||||
def form_ue_choix_niveau(ue: UniteEns) -> str:
|
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:
|
for parcour in ref_comp.parcours:
|
||||||
parcours_options.append(
|
parcours_options.append(
|
||||||
f"""<option value="{parcour.id}" {
|
f"""<option value="{parcour.id}" {
|
||||||
'selected' if ue.parcour == parcour else ''}
|
'selected' if parcour in ue.parcours else ''}
|
||||||
>{parcour.libelle} ({parcour.code})
|
>{parcour.libelle} ({parcour.code})
|
||||||
</option>"""
|
</option>"""
|
||||||
)
|
)
|
||||||
@ -44,14 +45,14 @@ def form_ue_choix_niveau(ue: UniteEns) -> str:
|
|||||||
<div class="cont_ue_choix_niveau">
|
<div class="cont_ue_choix_niveau">
|
||||||
<div>
|
<div>
|
||||||
<b>Parcours :</b>
|
<b>Parcours :</b>
|
||||||
<select class="select_parcour"
|
<select class="select_parcour multiselect"
|
||||||
onchange="set_ue_parcour(this);"
|
onchange="set_ue_parcour(this);"
|
||||||
data-ue_id="{ue.id}"
|
data-ue_id="{ue.id}"
|
||||||
data-setter="{
|
data-setter="{
|
||||||
url_for( "notes.set_ue_parcours", scodoc_dept=g.scodoc_dept)
|
url_for( "notes.set_ue_parcours", scodoc_dept=g.scodoc_dept)
|
||||||
}">
|
}">
|
||||||
<option value="" {
|
<option value="" {
|
||||||
'selected' if ue.parcour is None else ''
|
'selected' if not ue.parcours else ''
|
||||||
}>Tous</option>
|
}>Tous</option>
|
||||||
{newline.join(parcours_options)}
|
{newline.join(parcours_options)}
|
||||||
</select>
|
</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:
|
def get_ue_niveaux_options_html(ue: UniteEns) -> str:
|
||||||
"""fragment html avec les options du menu de sélection du
|
"""fragment html avec les options du menu de sélection du
|
||||||
niveau de compétences associé à une UE.
|
niveau de compétences associé à une UE.
|
||||||
@ -85,9 +108,7 @@ def get_ue_niveaux_options_html(ue: UniteEns) -> str:
|
|||||||
return ""
|
return ""
|
||||||
# Les niveaux:
|
# Les niveaux:
|
||||||
annee = ue.annee() # 1, 2, 3
|
annee = ue.annee() # 1, 2, 3
|
||||||
parcours, niveaux_by_parcours = ref_comp.get_niveaux_by_parcours(
|
parcours, niveaux_by_parcours = ref_comp.get_niveaux_by_parcours(annee, ue.parcours)
|
||||||
annee, parcour=ue.parcour
|
|
||||||
)
|
|
||||||
|
|
||||||
# Les niveaux déjà associés à d'autres UE du même semestre
|
# 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)
|
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.res_compat import NotesTableCompat
|
||||||
|
|
||||||
from app.comp import res_sem
|
from app.comp import res_sem
|
||||||
from app.models import formsemestre
|
|
||||||
|
|
||||||
from app.models.but_refcomp import (
|
from app.models.but_refcomp import (
|
||||||
ApcAnneeParcours,
|
ApcAnneeParcours,
|
||||||
@ -32,6 +31,7 @@ from app.models.but_refcomp import (
|
|||||||
ApcNiveau,
|
ApcNiveau,
|
||||||
ApcParcours,
|
ApcParcours,
|
||||||
ApcParcoursNiveauCompetence,
|
ApcParcoursNiveauCompetence,
|
||||||
|
ApcReferentielCompetences,
|
||||||
)
|
)
|
||||||
from app.models import Scolog, ScolarAutorisationInscription
|
from app.models import Scolog, ScolarAutorisationInscription
|
||||||
from app.models.but_validations import (
|
from app.models.but_validations import (
|
||||||
@ -109,7 +109,7 @@ class EtudCursusBUT:
|
|||||||
"cache les niveaux"
|
"cache les niveaux"
|
||||||
for annee in (1, 2, 3):
|
for annee in (1, 2, 3):
|
||||||
niveaux_d = formation.referentiel_competence.get_niveaux_by_parcours(
|
niveaux_d = formation.referentiel_competence.get_niveaux_by_parcours(
|
||||||
annee, self.parcour
|
annee, [self.parcour]
|
||||||
)[1]
|
)[1]
|
||||||
# groupe les niveaux de tronc commun et ceux spécifiques au parcour
|
# groupe les niveaux de tronc commun et ceux spécifiques au parcour
|
||||||
self.niveaux_by_annee[annee] = niveaux_d["TC"] + (
|
self.niveaux_by_annee[annee] = niveaux_d["TC"] + (
|
||||||
@ -170,6 +170,7 @@ class EtudCursusBUT:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
# XXX lent, provisoirement utilisé par TableJury.add_but_competences()
|
||||||
return {
|
return {
|
||||||
competence.id: {
|
competence.id: {
|
||||||
annee: self.validation_par_competence_et_annee.get(
|
annee: self.validation_par_competence_et_annee.get(
|
||||||
@ -185,7 +186,7 @@ class EtudCursusBUT:
|
|||||||
"""
|
"""
|
||||||
{
|
{
|
||||||
competence_id : {
|
competence_id : {
|
||||||
annee : { validation}
|
annee : { validation }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
où validation est un petit dict avec niveau_id, etc.
|
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
|
validation_rcue.to_dict_codes() if validation_rcue else None
|
||||||
)
|
)
|
||||||
return d
|
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,
|
parcours,
|
||||||
niveaux_by_parcours,
|
niveaux_by_parcours,
|
||||||
) = formation.referentiel_competence.get_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"] + (
|
self.niveaux_competences = niveaux_by_parcours["TC"] + (
|
||||||
niveaux_by_parcours[self.parcour.id] if self.parcour else []
|
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])
|
parcour = ApcParcours.query.get(res.etuds_parcour_id[etud.id])
|
||||||
ues = (
|
ues = (
|
||||||
formsemestre.formation.query_ues_parcour(parcour)
|
formsemestre.formation.query_ues_parcour(parcour)
|
||||||
.filter_by(semestre_idx=formsemestre.semestre_id)
|
.filter(UniteEns.semestre_idx == formsemestre.semestre_id)
|
||||||
.order_by(UniteEns.numero)
|
.order_by(UniteEns.numero)
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
|
@ -228,14 +228,14 @@ class BonusSportAdditif(BonusSport):
|
|||||||
# en APC, bonus_moy_arr est (nb_etuds, nb_ues_non_bonus)
|
# en APC, bonus_moy_arr est (nb_etuds, nb_ues_non_bonus)
|
||||||
if self.formsemestre.formation.is_apc():
|
if self.formsemestre.formation.is_apc():
|
||||||
# Bonus sur les UE et None sur moyenne générale
|
# 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(
|
self.bonus_ues = pd.DataFrame(
|
||||||
bonus_moy_arr, index=self.etuds_idx, columns=ues_idx, dtype=float
|
bonus_moy_arr, index=self.etuds_idx, columns=ues_idx, dtype=float
|
||||||
)
|
)
|
||||||
elif self.classic_use_bonus_ues:
|
elif self.classic_use_bonus_ues:
|
||||||
# Formations classiques apppliquant le bonus sur les UEs
|
# Formations classiques apppliquant le bonus sur les UEs
|
||||||
# ici bonus_moy_arr = ndarray 1d nb_etuds
|
# 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(
|
self.bonus_ues = pd.DataFrame(
|
||||||
np.stack([bonus_moy_arr] * len(ues_idx)).T,
|
np.stack([bonus_moy_arr] * len(ues_idx)).T,
|
||||||
index=self.etuds_idx,
|
index=self.etuds_idx,
|
||||||
@ -420,7 +420,7 @@ class BonusAmiens(BonusSportAdditif):
|
|||||||
|
|
||||||
# # Bonus moyenne générale et sur les UE
|
# # Bonus moyenne générale et sur les UE
|
||||||
# self.bonus_moy_gen = pd.Series(bonus, index=self.etuds_idx, dtype=float)
|
# 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)
|
# nb_ues_no_bonus = len(ues_idx)
|
||||||
# self.bonus_ues = pd.DataFrame(
|
# self.bonus_ues = pd.DataFrame(
|
||||||
# np.stack([bonus] * nb_ues_no_bonus, axis=1),
|
# 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]
|
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
|
# 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
|
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]
|
ues_idx = [ue.id for ue in ues]
|
||||||
|
|
||||||
if self.formsemestre.formation.is_apc(): # --- BUT
|
if self.formsemestre.formation.is_apc(): # --- BUT
|
||||||
@ -687,7 +687,7 @@ class BonusCalais(BonusSportAdditif):
|
|||||||
else:
|
else:
|
||||||
self.classic_use_bonus_ues = True # pour les LP
|
self.classic_use_bonus_ues = True # pour les LP
|
||||||
super().compute_bonus(sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan)
|
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 = [
|
ues_sans_bs = [
|
||||||
ue for ue in ues if ue.acronyme[-2:].upper() != "BS"
|
ue for ue in ues if ue.acronyme[-2:].upper() != "BS"
|
||||||
] # les 2 derniers cars forcés en majus
|
] # les 2 derniers cars forcés en majus
|
||||||
@ -788,7 +788,7 @@ class BonusIUTRennes1(BonusSportAdditif):
|
|||||||
sem_modimpl_moys_inscrits = sem_modimpl_moys_inscrits[:, :, 0]
|
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
|
# 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
|
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(
|
bonus_moy_arr = np.where(
|
||||||
note_bonus_max > self.seuil_moy_gen,
|
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)
|
modimpl: ModuleImpl = ModuleImpl.query.get(moduleimpl_id)
|
||||||
evaluations = Evaluation.query.filter_by(moduleimpl_id=moduleimpl_id).all()
|
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]
|
ue_ids = [ue.id for ue in ues]
|
||||||
evaluation_ids = [evaluation.id for evaluation in evaluations]
|
evaluation_ids = [evaluation.id for evaluation in evaluations]
|
||||||
evals_poids = pd.DataFrame(columns=ue_ids, index=evaluation_ids, dtype=float)
|
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.
|
DataFrame rows = UEs (sans bonus), columns = modimpl, value = coef.
|
||||||
"""
|
"""
|
||||||
if ues is None:
|
if ues is None:
|
||||||
ues = formsemestre.query_ues().all()
|
ues = formsemestre.get_ues()
|
||||||
ue_ids = [x.id for x in ues]
|
ue_ids = [x.id for x in ues]
|
||||||
if modimpls is None:
|
if modimpls is None:
|
||||||
modimpls = formsemestre.modimpls_sorted
|
modimpls = formsemestre.modimpls_sorted
|
||||||
|
@ -247,9 +247,9 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
|||||||
) in self.formsemestre.formation.referentiel_competence.parcours.all() + [None]:
|
) in self.formsemestre.formation.referentiel_competence.parcours.all() + [None]:
|
||||||
ue_by_parcours[None if parcour is None else parcour.id] = {
|
ue_by_parcours[None if parcour is None else parcour.id] = {
|
||||||
ue.id: 1.0
|
ue.id: 1.0
|
||||||
for ue in self.formsemestre.formation.query_ues_parcour(
|
for ue in self.formsemestre.formation.query_ues_parcour(parcour).filter(
|
||||||
parcour
|
UniteEns.semestre_idx == self.formsemestre.semestre_id
|
||||||
).filter_by(semestre_idx=self.formsemestre.semestre_id)
|
)
|
||||||
}
|
}
|
||||||
#
|
#
|
||||||
for etudid in etuds_parcour_id:
|
for etudid in etuds_parcour_id:
|
||||||
@ -290,7 +290,7 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
|||||||
ues_parcour = self.formsemestre.formation.query_ues_parcour(parcour)
|
ues_parcour = self.formsemestre.formation.query_ues_parcour(parcour)
|
||||||
ues_ids = set()
|
ues_ids = set()
|
||||||
for niveau in niveaux:
|
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:
|
if ue:
|
||||||
ues_ids.add(ue.id)
|
ues_ids.add(ue.id)
|
||||||
|
|
||||||
|
@ -10,6 +10,8 @@
|
|||||||
from collections import Counter, defaultdict
|
from collections import Counter, defaultdict
|
||||||
from collections.abc import Generator
|
from collections.abc import Generator
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
|
from operator import attrgetter
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
@ -162,7 +164,7 @@ class ResultatsSemestre(ResultatsCache):
|
|||||||
(indices des DataFrames).
|
(indices des DataFrames).
|
||||||
Note: un étudiant n'est pas nécessairement inscrit dans toutes ces UEs.
|
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
|
@cached_property
|
||||||
def ressources(self):
|
def ressources(self):
|
||||||
@ -233,7 +235,7 @@ class ResultatsSemestre(ResultatsCache):
|
|||||||
for modimpl in self.formsemestre.modimpls_sorted
|
for modimpl in self.formsemestre.modimpls_sorted
|
||||||
if self.modimpl_inscr_df[modimpl.id][etudid]
|
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
|
return ues
|
||||||
|
|
||||||
def modimpls_in_ue(self, ue: UniteEns, etudid, with_bonus=True) -> list[ModuleImpl]:
|
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
|
# Quand il y a une capitalisation, vérifie toutes les UEs
|
||||||
sum_notes_ue = 0.0
|
sum_notes_ue = 0.0
|
||||||
sum_coefs_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)
|
ue_cap = self.get_etud_ue_status(etudid, ue.id)
|
||||||
if ue_cap is None:
|
if ue_cap is None:
|
||||||
continue
|
continue
|
||||||
|
@ -108,7 +108,7 @@ class NotesTableCompat(ResultatsSemestre):
|
|||||||
Si filter_sport, retire les UE de type SPORT.
|
Si filter_sport, retire les UE de type SPORT.
|
||||||
Résultat: liste de dicts { champs UE U stats moyenne UE }
|
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 = []
|
ues_dict = []
|
||||||
for ue in ues:
|
for ue in ues:
|
||||||
d = ue.to_dict()
|
d = ue.to_dict()
|
||||||
@ -178,7 +178,7 @@ class NotesTableCompat(ResultatsSemestre):
|
|||||||
self.etud_moy_gen_ranks,
|
self.etud_moy_gen_ranks,
|
||||||
self.etud_moy_gen_ranks_int,
|
self.etud_moy_gen_ranks_int,
|
||||||
) = moy_sem.comp_ranks_series(etud_moy_gen_dem_zero)
|
) = moy_sem.comp_ranks_series(etud_moy_gen_dem_zero)
|
||||||
ues = self.formsemestre.query_ues()
|
ues = self.formsemestre.get_ues()
|
||||||
for ue in ues:
|
for ue in ues:
|
||||||
moy_ue = self.etud_moy_ue[ue.id]
|
moy_ue = self.etud_moy_ue[ue.id]
|
||||||
self.ue_rangs[ue.id] = (
|
self.ue_rangs[ue.id] = (
|
||||||
@ -260,7 +260,7 @@ class NotesTableCompat(ResultatsSemestre):
|
|||||||
Return: True|False, message explicatif
|
Return: True|False, message explicatif
|
||||||
"""
|
"""
|
||||||
ue_status_list = []
|
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)
|
ue_status = self.get_etud_ue_status(etudid, ue.id)
|
||||||
if ue_status:
|
if ue_status:
|
||||||
ue_status_list.append(ue_status)
|
ue_status_list.append(ue_status)
|
||||||
@ -477,7 +477,7 @@ class NotesTableCompat(ResultatsSemestre):
|
|||||||
"""
|
"""
|
||||||
table_moyennes = []
|
table_moyennes = []
|
||||||
etuds_inscriptions = self.formsemestre.etuds_inscriptions
|
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:
|
for etudid in etuds_inscriptions:
|
||||||
moy_gen = self.etud_moy_gen.get(etudid, False)
|
moy_gen = self.etud_moy_gen.get(etudid, False)
|
||||||
if moy_gen is 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 flask_wtf import FlaskForm
|
||||||
from wtforms import RadioField, SubmitField, validators
|
from wtforms import RadioField, SubmitField
|
||||||
|
|
||||||
from app.models import Formation
|
from app.models import Formation
|
||||||
|
|
||||||
|
|
||||||
class FormSemestreChangeFormationForm(FlaskForm):
|
class FormSemestreChangeFormationForm(FlaskForm):
|
||||||
"Formulaire changement formation d'un formsemestre"
|
"Formulaire changement formation d'un formsemestre"
|
||||||
# consrtuit dynamiquement ci-dessous
|
# construit dynamiquement ci-dessous
|
||||||
|
|
||||||
|
|
||||||
def gen_formsemestre_change_formation_form(
|
def gen_formsemestre_change_formation_form(
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
"""ScoDoc 9 models : Référentiel Compétence BUT 2021
|
"""ScoDoc 9 models : Référentiel Compétence BUT 2021
|
||||||
"""
|
"""
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from operator import attrgetter
|
||||||
|
|
||||||
import flask_sqlalchemy
|
import flask_sqlalchemy
|
||||||
from sqlalchemy.orm import class_mapper
|
from sqlalchemy.orm import class_mapper
|
||||||
@ -129,11 +130,11 @@ class ApcReferentielCompetences(db.Model, XMLModel):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def get_niveaux_by_parcours(
|
def get_niveaux_by_parcours(
|
||||||
self, annee: int, parcour: "ApcParcours" = None
|
self, annee: int, parcours: list["ApcParcours"] = None
|
||||||
) -> tuple[list["ApcParcours"], dict]:
|
) -> tuple[list["ApcParcours"], dict]:
|
||||||
"""
|
"""
|
||||||
Construit la liste des niveaux de compétences pour chaque parcours
|
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.
|
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()
|
parcours_ref = self.parcours.order_by(ApcParcours.numero).all()
|
||||||
if parcour is None:
|
if parcours is None:
|
||||||
parcours = parcours_ref
|
parcours = parcours_ref
|
||||||
else:
|
|
||||||
parcours = [parcour]
|
|
||||||
niveaux_by_parcours = {
|
niveaux_by_parcours = {
|
||||||
parcour.id: ApcNiveau.niveaux_annee_de_parcours(parcour, annee, self)
|
parcour.id: ApcNiveau.niveaux_annee_de_parcours(parcour, annee, self)
|
||||||
for parcour in parcours_ref
|
for parcour in parcours_ref
|
||||||
@ -205,7 +204,7 @@ class ApcReferentielCompetences(db.Model, XMLModel):
|
|||||||
for competence in parcours[0].query_competences()
|
for competence in parcours[0].query_competences()
|
||||||
if competence.id in ids
|
if competence.id in ids
|
||||||
],
|
],
|
||||||
key=lambda c: c.numero or 0,
|
key=attrgetter("numero"),
|
||||||
)
|
)
|
||||||
|
|
||||||
def table_niveaux_parcours(self) -> dict:
|
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 = db.Column(db.Text(), nullable=False, index=True)
|
||||||
titre_long = db.Column(db.Text())
|
titre_long = db.Column(db.Text())
|
||||||
couleur = 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
|
_xml_attribs = { # xml_attrib : attribute
|
||||||
"id": "id_orebut",
|
"id": "id_orebut",
|
||||||
"nom_court": "titre", # was name
|
"nom_court": "titre", # was name
|
||||||
@ -523,7 +522,7 @@ class ApcParcours(db.Model, XMLModel):
|
|||||||
db.ForeignKey("apc_referentiel_competences.id", ondelete="CASCADE"),
|
db.ForeignKey("apc_referentiel_competences.id", ondelete="CASCADE"),
|
||||||
nullable=False,
|
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)
|
code = db.Column(db.Text(), nullable=False)
|
||||||
libelle = db.Column(db.Text(), nullable=False)
|
libelle = db.Column(db.Text(), nullable=False)
|
||||||
annees = db.relationship(
|
annees = db.relationship(
|
||||||
@ -532,7 +531,6 @@ class ApcParcours(db.Model, XMLModel):
|
|||||||
lazy="dynamic",
|
lazy="dynamic",
|
||||||
cascade="all, delete-orphan",
|
cascade="all, delete-orphan",
|
||||||
)
|
)
|
||||||
ues = db.relationship("UniteEns", back_populates="parcour")
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"<{self.__class__.__name__} {self.id} {self.code!r} ref={self.referentiel}>"
|
return f"<{self.__class__.__name__} {self.id} {self.code!r} ref={self.referentiel}>"
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
"""ScoDoc models: evaluations
|
"""ScoDoc models: evaluations
|
||||||
"""
|
"""
|
||||||
import datetime
|
import datetime
|
||||||
|
from operator import attrgetter
|
||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
from app.models.etudiants import Identite
|
from app.models.etudiants import Identite
|
||||||
@ -44,7 +45,7 @@ class Evaluation(db.Model):
|
|||||||
)
|
)
|
||||||
# ordre de presentation (par défaut, le plus petit numero
|
# ordre de presentation (par défaut, le plus petit numero
|
||||||
# est la plus ancienne eval):
|
# 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)
|
ues = db.relationship("UniteEns", secondary="evaluation_ue_poids", viewonly=True)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
@ -151,7 +152,7 @@ class Evaluation(db.Model):
|
|||||||
Return True if (uncommited) modification, False otherwise.
|
Return True if (uncommited) modification, False otherwise.
|
||||||
"""
|
"""
|
||||||
ue_coef_dict = self.moduleimpl.module.get_ue_coef_dict()
|
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
|
modified = False
|
||||||
for ue in sem_ues:
|
for ue in sem_ues:
|
||||||
existing_poids = EvaluationUEPoids.query.filter_by(
|
existing_poids = EvaluationUEPoids.query.filter_by(
|
||||||
@ -196,7 +197,7 @@ class Evaluation(db.Model):
|
|||||||
return {
|
return {
|
||||||
p.ue.id: p.poids
|
p.ue.id: p.poids
|
||||||
for p in sorted(
|
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 (
|
from app.models.but_refcomp import (
|
||||||
ApcAnneeParcours,
|
ApcAnneeParcours,
|
||||||
ApcCompetence,
|
ApcCompetence,
|
||||||
ApcNiveau,
|
|
||||||
ApcParcours,
|
ApcParcours,
|
||||||
ApcParcoursNiveauCompetence,
|
ApcParcoursNiveauCompetence,
|
||||||
)
|
)
|
||||||
from app.models.modules import Module
|
from app.models.modules import Module
|
||||||
from app.models.moduleimpls import ModuleImpl
|
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 sco_cache
|
||||||
from app.scodoc import codes_cursus
|
from app.scodoc import codes_cursus
|
||||||
from app.scodoc import sco_utils as scu
|
from app.scodoc import sco_utils as scu
|
||||||
@ -213,23 +212,36 @@ class Formation(db.Model):
|
|||||||
if change:
|
if change:
|
||||||
app.clear_scodoc_cache()
|
app.clear_scodoc_cache()
|
||||||
|
|
||||||
def query_ues_parcour(self, parcour: ApcParcours) -> flask_sqlalchemy.BaseQuery:
|
def query_ues_parcour(
|
||||||
"""Les UEs d'un parcours de la formation.
|
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.
|
Si parcour est None, les UE sans parcours.
|
||||||
Exemple: pour avoir les UE du semestre 3, faire
|
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:
|
if with_sport:
|
||||||
return UniteEns.query.filter_by(
|
query_f = UniteEns.query.filter_by(formation=self)
|
||||||
formation=self, type=UE_STANDARD, parcour_id=None
|
else:
|
||||||
)
|
query_f = UniteEns.query.filter_by(formation=self, type=UE_STANDARD)
|
||||||
return UniteEns.query.filter_by(formation=self, type=UE_STANDARD).filter(
|
# Les UE sans parcours:
|
||||||
UniteEns.niveau_competence_id == ApcNiveau.id,
|
query_no_parcours = query_f.outerjoin(UEParcours).filter(
|
||||||
(UniteEns.parcour_id == parcour.id) | (UniteEns.parcour_id == None),
|
UEParcours.parcours_id == None
|
||||||
ApcParcoursNiveauCompetence.competence_id == ApcNiveau.competence_id,
|
|
||||||
ApcParcoursNiveauCompetence.annee_parcours_id == ApcAnneeParcours.id,
|
|
||||||
ApcAnneeParcours.parcours_id == parcour.id,
|
|
||||||
)
|
)
|
||||||
|
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(
|
def query_competences_parcour(
|
||||||
self, parcour: ApcParcours
|
self, parcour: ApcParcours
|
||||||
@ -281,7 +293,7 @@ class Matiere(db.Model):
|
|||||||
matiere_id = db.synonym("id")
|
matiere_id = db.synonym("id")
|
||||||
ue_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id"))
|
ue_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id"))
|
||||||
titre = db.Column(db.Text())
|
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")
|
modules = db.relationship("Module", lazy="dynamic", backref="matiere")
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
"""
|
"""
|
||||||
import datetime
|
import datetime
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
|
from operator import attrgetter
|
||||||
|
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
import flask_sqlalchemy
|
import flask_sqlalchemy
|
||||||
@ -281,60 +282,43 @@ class FormSemestre(db.Model):
|
|||||||
)
|
)
|
||||||
return r or []
|
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.
|
"""UE des modules de ce semestre, triées par numéro.
|
||||||
- Formations classiques: les UEs auxquelles appartiennent
|
- Formations classiques: les UEs auxquelles appartiennent
|
||||||
les modules mis en place dans ce semestre.
|
les modules mis en place dans ce semestre.
|
||||||
- Formations APC / BUT: les UEs de la formation qui
|
- Formations APC / BUT: les UEs de la formation qui
|
||||||
- ont le même numéro de semestre que ce formsemestre
|
- ont le même numéro de semestre que ce formsemestre;
|
||||||
- sont associées à l'un des parcours de ce formsemestre (ou à aucun)
|
- et sont associées à l'un des parcours de ce formsemestre
|
||||||
|
(ou à aucun, donc tronc commun).
|
||||||
"""
|
"""
|
||||||
if self.formation.get_cursus().APC_SAE:
|
formation: Formation = self.formation
|
||||||
sem_ues = UniteEns.query.filter_by(
|
if formation.is_apc():
|
||||||
formation=self.formation, semestre_idx=self.semestre_id
|
sem_ues = {
|
||||||
)
|
ue.id: ue
|
||||||
if self.parcours:
|
for ue in formation.query_ues_parcour(
|
||||||
# Prend toutes les UE de l'un des parcours du sem., ou déclarées sans parcours
|
None, with_sport=with_sport
|
||||||
sem_ues = sem_ues.filter(
|
).filter(UniteEns.semestre_idx == self.semestre_id)
|
||||||
(UniteEns.parcour == None)
|
}
|
||||||
| (UniteEns.parcour_id.in_([p.id for p in self.parcours]))
|
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:
|
else:
|
||||||
sem_ues = db.session.query(UniteEns).filter(
|
sem_ues = db.session.query(UniteEns).filter(
|
||||||
ModuleImpl.formsemestre_id == self.id,
|
ModuleImpl.formsemestre_id == self.id,
|
||||||
Module.id == ModuleImpl.module_id,
|
Module.id == ModuleImpl.module_id,
|
||||||
UniteEns.id == Module.ue_id,
|
UniteEns.id == Module.ue_id,
|
||||||
)
|
)
|
||||||
if not with_sport:
|
if not with_sport:
|
||||||
sem_ues = sem_ues.filter(UniteEns.type != codes_cursus.UE_SPORT)
|
sem_ues = sem_ues.filter(UniteEns.type != codes_cursus.UE_SPORT)
|
||||||
return sem_ues.order_by(UniteEns.numero)
|
return sem_ues.order_by(UniteEns.numero).all()
|
||||||
|
|
||||||
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),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def modimpls_sorted(self) -> list[ModuleImpl]:
|
def modimpls_sorted(self) -> list[ModuleImpl]:
|
||||||
@ -960,7 +944,7 @@ class FormationModalite(db.Model):
|
|||||||
) # code
|
) # code
|
||||||
titre = db.Column(db.Text()) # texte explicatif
|
titre = db.Column(db.Text()) # texte explicatif
|
||||||
# numero = ordre de presentation)
|
# numero = ordre de presentation)
|
||||||
numero = db.Column(db.Integer)
|
numero = db.Column(db.Integer, nullable=False, default=0)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def insert_modalites():
|
def insert_modalites():
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
"""ScoDoc models: Groups & partitions
|
"""ScoDoc models: Groups & partitions
|
||||||
"""
|
"""
|
||||||
|
from operator import attrgetter
|
||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
from app.models import SHORT_STR_LEN
|
from app.models import SHORT_STR_LEN
|
||||||
@ -29,7 +30,7 @@ class Partition(db.Model):
|
|||||||
# "TD", "TP", ... (NULL for 'all')
|
# "TD", "TP", ... (NULL for 'all')
|
||||||
partition_name = db.Column(db.String(SHORT_STR_LEN))
|
partition_name = db.Column(db.String(SHORT_STR_LEN))
|
||||||
# Numero = ordre de presentation)
|
# Numero = ordre de presentation)
|
||||||
numero = db.Column(db.Integer)
|
numero = db.Column(db.Integer, nullable=False, default=0)
|
||||||
# Calculer le rang ?
|
# Calculer le rang ?
|
||||||
bul_show_rank = db.Column(
|
bul_show_rank = db.Column(
|
||||||
db.Boolean(), nullable=False, default=False, server_default="false"
|
db.Boolean(), nullable=False, default=False, server_default="false"
|
||||||
@ -92,7 +93,7 @@ class Partition(db.Model):
|
|||||||
d.pop("formsemestre", None)
|
d.pop("formsemestre", None)
|
||||||
|
|
||||||
if with_groups:
|
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
|
# un dict et non plus une liste, pour JSON
|
||||||
d["groups"] = {
|
d["groups"] = {
|
||||||
group.id: group.to_dict(with_partition=False) for group in 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'):
|
# "A", "C2", ... (NULL for 'all'):
|
||||||
group_name = db.Column(db.String(GROUPNAME_STR_LEN))
|
group_name = db.Column(db.String(GROUPNAME_STR_LEN))
|
||||||
# Numero = ordre de presentation
|
# Numero = ordre de presentation
|
||||||
numero = db.Column(db.Integer)
|
numero = db.Column(db.Integer, nullable=False, default=0)
|
||||||
|
|
||||||
etuds = db.relationship(
|
etuds = db.relationship(
|
||||||
"Identite",
|
"Identite",
|
||||||
|
@ -33,7 +33,7 @@ class Module(db.Model):
|
|||||||
# pas un id mais le numéro du semestre: 1, 2, ...
|
# pas un id mais le numéro du semestre: 1, 2, ...
|
||||||
# note: en APC, le semestre qui fait autorité est celui de l'UE
|
# 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")
|
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:
|
# id de l'element pedagogique Apogee correspondant:
|
||||||
code_apogee = db.Column(db.String(APO_CODE_STR_LEN))
|
code_apogee = db.Column(db.String(APO_CODE_STR_LEN))
|
||||||
# Type: ModuleType.STANDARD, MALUS, RESSOURCE, SAE (enum)
|
# Type: ModuleType.STANDARD, MALUS, RESSOURCE, SAE (enum)
|
||||||
|
@ -21,7 +21,7 @@ class UniteEns(db.Model):
|
|||||||
ue_id = db.synonym("id")
|
ue_id = db.synonym("id")
|
||||||
formation_id = db.Column(db.Integer, db.ForeignKey("notes_formations.id"))
|
formation_id = db.Column(db.Integer, db.ForeignKey("notes_formations.id"))
|
||||||
acronyme = db.Column(db.Text(), nullable=False)
|
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())
|
titre = db.Column(db.Text())
|
||||||
# Le semestre_idx n'est pas un id mais le numéro du semestre: 1, 2, ...
|
# 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
|
# 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")
|
niveau_competence = db.relationship("ApcNiveau", back_populates="ues")
|
||||||
|
|
||||||
# Une ue appartient soit à tous les parcours (tronc commun), soit à un seul:
|
# Une UE appartient soit à tous les parcours (tronc commun), soit à un sous-ensemble
|
||||||
parcour_id = db.Column(
|
parcours = db.relationship(
|
||||||
db.Integer, db.ForeignKey("apc_parcours.id", ondelete="SET NULL"), index=True
|
ApcParcours, secondary="ue_parcours", backref=db.backref("ues", lazy=True)
|
||||||
)
|
)
|
||||||
parcour = db.relationship("ApcParcours", back_populates="ues")
|
|
||||||
|
|
||||||
# relations
|
# relations
|
||||||
matieres = db.relationship("Matiere", lazy="dynamic", backref="ue")
|
matieres = db.relationship("Matiere", lazy="dynamic", backref="ue")
|
||||||
@ -115,7 +114,9 @@ class UniteEns(db.Model):
|
|||||||
e["ects"] = e["ects"]
|
e["ects"] = e["ects"]
|
||||||
e["coefficient"] = e["coefficient"] if e["coefficient"] else 0.0
|
e["coefficient"] = e["coefficient"] if e["coefficient"] else 0.0
|
||||||
e["code_apogee"] = e["code_apogee"] or "" # pas de None
|
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 with_module_ue_coefs:
|
||||||
if convert_objects:
|
if convert_objects:
|
||||||
e["module_ue_coefs"] = [
|
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 {x.strip() for x in self.code_apogee.split(",") if x}
|
||||||
return set()
|
return set()
|
||||||
|
|
||||||
def _check_apc_conflict(self, new_niveau_id: int, new_parcour_id: int):
|
def _parcours_niveaux_ids(self, parcours=list[ApcParcours]) -> set[int]:
|
||||||
"raises ScoFormationConflict si (niveau, parcours) pas unique dans ce semestre"
|
"""set des ids de niveaux dans les parcours listés"""
|
||||||
# Les UE du même semestre que nous:
|
return set.union(
|
||||||
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)
|
n.id
|
||||||
for oue in ues_sem
|
for n in self.niveau_competence.niveaux_annee_de_parcours(
|
||||||
if oue.id != self.id
|
parcour, self.annee(), self.formation.referentiel_competence
|
||||||
):
|
)
|
||||||
log(
|
}
|
||||||
f"set_ue_niveau_competence: {self}: ({new_niveau_id}, {new_parcour_id}) déjà associé"
|
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é.
|
"""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.
|
Assure que ce soit la seule dans son parcours.
|
||||||
Sinon, raises ScoFormationConflict.
|
Sinon, raises ScoFormationConflict.
|
||||||
|
|
||||||
Si niveau est None, désassocie.
|
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:
|
if niveau is not None:
|
||||||
self._check_apc_conflict(niveau.id, self.parcour_id)
|
ok, error_message = self.check_niveau_unique_dans_parcours(
|
||||||
# Le niveau est-il dans le parcours ? Sinon, erreur
|
niveau, self.parcours
|
||||||
if self.parcour and niveau.id not in (
|
)
|
||||||
n.id
|
if not ok:
|
||||||
for n in niveau.niveaux_annee_de_parcours(
|
return ok, error_message
|
||||||
self.parcour, self.annee(), self.formation.referentiel_competence
|
|
||||||
)
|
|
||||||
):
|
|
||||||
log(
|
|
||||||
f"set_niveau_competence: niveau {niveau} hors parcours {self.parcour}"
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
self.niveau_competence = niveau
|
self.niveau_competence = niveau
|
||||||
|
|
||||||
db.session.add(self)
|
db.session.add(self)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
# Invalidation du cache
|
# Invalidation du cache
|
||||||
self.formation.invalidate_cached_sems()
|
self.formation.invalidate_cached_sems()
|
||||||
log(f"ue.set_niveau_competence( {self}, {niveau} )")
|
log(f"ue.set_niveau_competence( {self}, {niveau} )")
|
||||||
|
return True, ""
|
||||||
|
|
||||||
def set_parcour(self, parcour: ApcParcours):
|
def set_parcours(self, parcours: list[ApcParcours]) -> tuple[bool, str]:
|
||||||
"""Associe cette UE au parcours indiqué.
|
"""Associe cette UE aux parcours indiqués.
|
||||||
Assure que ce soit la seule dans son parcours.
|
Si un niveau est déjà associé, vérifie sa cohérence.
|
||||||
Sinon, raises ScoFormationConflict.
|
Renvoie (True, "") si ok, sinon (False, error_message)
|
||||||
|
|
||||||
Si niveau est None, désassocie.
|
|
||||||
"""
|
"""
|
||||||
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
|
# Le niveau est-il dans ce parcours ? Sinon, l'enlève
|
||||||
|
# breakpoint()
|
||||||
if (
|
if (
|
||||||
parcour
|
parcours
|
||||||
and self.niveau_competence
|
and self.niveau_competence
|
||||||
and self.niveau_competence.id
|
and self.niveau_competence.id not in self._parcours_niveaux_ids(parcours)
|
||||||
not in (
|
|
||||||
n.id
|
|
||||||
for n in self.niveau_competence.niveaux_annee_de_parcours(
|
|
||||||
parcour, self.annee(), self.formation.referentiel_competence
|
|
||||||
)
|
|
||||||
)
|
|
||||||
):
|
):
|
||||||
self.niveau_competence = None
|
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.add(self)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
# Invalidation du cache
|
# Invalidation du cache
|
||||||
self.formation.invalidate_cached_sems()
|
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):
|
class DispenseUE(db.Model):
|
||||||
|
@ -1009,10 +1009,7 @@ class ApoData(object):
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
codes_ues = set().union(
|
codes_ues = set().union(
|
||||||
*[
|
*[ue.get_codes_apogee() for ue in formsemestre.get_ues(with_sport=True)]
|
||||||
ue.get_codes_apogee()
|
|
||||||
for ue in formsemestre.query_ues(with_sport=True)
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
s = set()
|
s = set()
|
||||||
codes_by_sem[sem["formsemestre_id"]] = s
|
codes_by_sem[sem["formsemestre_id"]] = s
|
||||||
|
@ -107,7 +107,7 @@ def html_edit_formation_apc(
|
|||||||
icons=icons,
|
icons=icons,
|
||||||
ues_by_sem=ues_by_sem,
|
ues_by_sem=ues_by_sem,
|
||||||
ects_by_sem=ects_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:
|
for semestre_idx in semestre_ids:
|
||||||
|
@ -737,8 +737,10 @@ def ue_table(formation_id=None, semestre_idx=1, msg=""): # was ue_list
|
|||||||
)
|
)
|
||||||
H = [
|
H = [
|
||||||
html_sco_header.sco_header(
|
html_sco_header.sco_header(
|
||||||
cssstyles=["libjs/jQuery-tagEditor/jquery.tag-editor.css"],
|
cssstyles=html_sco_header.BOOTSTRAP_MULTISELECT_CSS
|
||||||
javascripts=[
|
+ ["libjs/jQuery-tagEditor/jquery.tag-editor.css"],
|
||||||
|
javascripts=html_sco_header.BOOTSTRAP_MULTISELECT_JS
|
||||||
|
+ [
|
||||||
"libjs/jinplace-1.2.1.min.js",
|
"libjs/jinplace-1.2.1.min.js",
|
||||||
"js/ue_list.js",
|
"js/ue_list.js",
|
||||||
"js/edit_ue.js",
|
"js/edit_ue.js",
|
||||||
|
@ -79,7 +79,7 @@ def evaluation_create_form(
|
|||||||
mod = modimpl_o["module"]
|
mod = modimpl_o["module"]
|
||||||
formsemestre_id = modimpl_o["formsemestre_id"]
|
formsemestre_id = modimpl_o["formsemestre_id"]
|
||||||
formsemestre = modimpl.formsemestre
|
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_malus = mod["module_type"] == ModuleType.MALUS
|
||||||
is_apc = mod["module_type"] in (ModuleType.RESSOURCE, ModuleType.SAE)
|
is_apc = mod["module_type"] in (ModuleType.RESSOURCE, ModuleType.SAE)
|
||||||
preferences = sco_preferences.SemPreferences(formsemestre.id)
|
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_annee"] = ue.niveau_competence.annee
|
||||||
ue_dict["apc_niveau_ordre"] = ue.niveau_competence.ordre
|
ue_dict["apc_niveau_ordre"] = ue.niveau_competence.ordre
|
||||||
# Et le parcour:
|
# Et le parcour:
|
||||||
if ue.parcour:
|
if ue.parcours:
|
||||||
ue_dict["parcour"] = [ue.parcour.to_dict(with_annees=False)]
|
ue_dict["parcours"] = [
|
||||||
|
parcour.to_dict(with_annees=False) for parcour in ue.parcours
|
||||||
|
]
|
||||||
# pour les coefficients:
|
# pour les coefficients:
|
||||||
ue_dict["reference"] = ue.id if ue_reference_style == "id" else ue.acronyme
|
ue_dict["reference"] = ue.id if ue_reference_style == "id" else ue.acronyme
|
||||||
if not export_ids:
|
if not export_ids:
|
||||||
@ -372,6 +374,7 @@ def formation_import_xml(doc: str, import_tags=True, use_local_refcomp=False):
|
|||||||
|
|
||||||
# -- create matieres
|
# -- create matieres
|
||||||
for mat_info in ue_info[2]:
|
for mat_info in ue_info[2]:
|
||||||
|
# Backward compat: un seul parcours par UE (ScoDoc < 9.4.71)
|
||||||
if mat_info[0] == "parcour":
|
if mat_info[0] == "parcour":
|
||||||
# Parcours (BUT)
|
# Parcours (BUT)
|
||||||
code_parcours = mat_info[1]["code"]
|
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,
|
referentiel_id=referentiel_competence_id,
|
||||||
).first()
|
).first()
|
||||||
if parcour:
|
if parcour:
|
||||||
ue.parcour = parcour
|
ue.parcours = [parcour]
|
||||||
db.session.add(ue)
|
db.session.add(ue)
|
||||||
else:
|
else:
|
||||||
|
flash(f"Attention: parcours {code_parcours} inexistant !")
|
||||||
log(f"Warning: parcours {code_parcours} inexistant !")
|
log(f"Warning: parcours {code_parcours} inexistant !")
|
||||||
continue
|
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"
|
assert mat_info[0] == "matiere"
|
||||||
mat_info[1]["ue_id"] = ue_id
|
mat_info[1]["ue_id"] = ue_id
|
||||||
mat_id = sco_edit_matiere.do_matiere_create(mat_info[1])
|
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 flask_login import current_user
|
||||||
|
|
||||||
from app import log
|
from app import log
|
||||||
|
from app.but.cursus_but import formsemestre_warning_apc_setup
|
||||||
from app.comp import res_sem
|
from app.comp import res_sem
|
||||||
from app.comp.res_common import ResultatsSemestre
|
from app.comp.res_common import ResultatsSemestre
|
||||||
from app.comp.res_compat import NotesTableCompat
|
from app.comp.res_compat import NotesTableCompat
|
||||||
@ -604,7 +605,7 @@ def formsemestre_description_table(
|
|||||||
columns_ids += ["Coef."]
|
columns_ids += ["Coef."]
|
||||||
ues = [] # liste des UE, seulement en APC pour les coefs
|
ues = [] # liste des UE, seulement en APC pour les coefs
|
||||||
else:
|
else:
|
||||||
ues = formsemestre.query_ues().all()
|
ues = formsemestre.get_ues()
|
||||||
columns_ids += [f"ue_{ue.id}" for ue in ues]
|
columns_ids += [f"ue_{ue.id}" for ue in ues]
|
||||||
if sco_preferences.get_preference("bul_show_ects", formsemestre_id):
|
if sco_preferences.get_preference("bul_show_ects", formsemestre_id):
|
||||||
columns_ids += ["ects"]
|
columns_ids += ["ects"]
|
||||||
@ -1057,6 +1058,7 @@ def formsemestre_status(formsemestre_id=None, check_parcours=True):
|
|||||||
formsemestre_status_head(
|
formsemestre_status_head(
|
||||||
formsemestre_id=formsemestre_id, page_title="Tableau de bord"
|
formsemestre_id=formsemestre_id, page_title="Tableau de bord"
|
||||||
),
|
),
|
||||||
|
formsemestre_warning_apc_setup(formsemestre, nt),
|
||||||
formsemestre_warning_etuds_sans_note(formsemestre, nt)
|
formsemestre_warning_etuds_sans_note(formsemestre, nt)
|
||||||
if can_change_all_notes
|
if can_change_all_notes
|
||||||
else "",
|
else "",
|
||||||
@ -1282,7 +1284,7 @@ def formsemestre_tableau_modules(
|
|||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
if mod.module_type in (ModuleType.RESSOURCE, ModuleType.SAE):
|
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}">')
|
H.append(f'<a class="invisible_link" href="#" title="{mod_descr}">')
|
||||||
for coef in coefs:
|
for coef in coefs:
|
||||||
if coef[1] > 0:
|
if coef[1] > 0:
|
||||||
|
@ -606,7 +606,9 @@ def formsemestre_recap_parcours_table(
|
|||||||
else:
|
else:
|
||||||
# si l'étudiant n'est pas inscrit à un parcours mais que le semestre a plus d'UE
|
# si l'étudiant n'est pas inscrit à un parcours mais que le semestre a plus d'UE
|
||||||
# signale un éventuel problème:
|
# 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"""
|
parcours_name = f"""
|
||||||
<span class="code_parcours no_parcours">{scu.EMO_WARNING} pas de parcours
|
<span class="code_parcours no_parcours">{scu.EMO_WARNING} pas de parcours
|
||||||
</span>"""
|
</span>"""
|
||||||
|
@ -992,8 +992,8 @@ def icontag(name, file_format="png", no_size=False, **attrs):
|
|||||||
file_format,
|
file_format,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
im = PILImage.open(img_file)
|
with PILImage.open(img_file) as image:
|
||||||
width, height = im.size[0], im.size[1]
|
width, height = image.size[0], image.size[1]
|
||||||
ICONSIZES[name] = (width, height) # cache
|
ICONSIZES[name] = (width, height) # cache
|
||||||
else:
|
else:
|
||||||
width, height = ICONSIZES[name]
|
width, height = ICONSIZES[name]
|
||||||
|
@ -89,7 +89,7 @@ function update_menus_niveau_competence() {
|
|||||||
// );
|
// );
|
||||||
|
|
||||||
// nouveau:
|
// nouveau:
|
||||||
document.querySelectorAll("select.select_niveau_ue").forEach(
|
document.querySelectorAll("select.niveau_select").forEach(
|
||||||
elem => {
|
elem => {
|
||||||
let ue_id = elem.dataset.ue_id;
|
let ue_id = elem.dataset.ue_id;
|
||||||
$.get("get_ue_niveaux_options_html",
|
$.get("get_ue_niveaux_options_html",
|
||||||
@ -102,4 +102,66 @@ 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:
|
# couples (modimpl, ue) effectivement présents dans la table:
|
||||||
self.modimpl_ue_ids = set()
|
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]
|
ues_sans_bonus = [ue for ue in ues if ue.type != UE_SPORT]
|
||||||
|
|
||||||
if res.formsemestre.etuds_inscriptions: # table non vide
|
if res.formsemestre.etuds_inscriptions: # table non vide
|
||||||
|
@ -65,7 +65,7 @@
|
|||||||
}}">modifier</a>
|
}}">modifier</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{{ form_ue_choix_niveau(ue)|safe }}
|
{{ form_ue_choix_parcours_niveau(ue)|safe }}
|
||||||
|
|
||||||
|
|
||||||
{% if ue.type == 1 and ue.modules.count() == 0 %}
|
{% 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
|
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")
|
@bp.route("/get_ue_niveaux_options_html")
|
||||||
@scodoc
|
@scodoc
|
||||||
@permission_required(Permission.ScoView)
|
@permission_required(Permission.ScoView)
|
||||||
@ -448,6 +429,9 @@ def get_ue_niveaux_options_html():
|
|||||||
niveau de compétences associé à une UE
|
niveau de compétences associé à une UE
|
||||||
"""
|
"""
|
||||||
ue_id = request.args.get("ue_id")
|
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)
|
ue: UniteEns = UniteEns.query.get_or_404(ue_id)
|
||||||
return apc_edit_ue.get_ue_niveaux_options_html(ue)
|
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 = [
|
ues = [
|
||||||
ue
|
ue
|
||||||
for ue in ues
|
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 = [
|
modules = [
|
||||||
mod
|
mod
|
||||||
@ -113,13 +113,14 @@ def table_modules_ue_coefs(formation_id, semestre_idx=None, parcours_id: int = N
|
|||||||
cells = []
|
cells = []
|
||||||
for (row, mod) in enumerate(modules, start=2):
|
for (row, mod) in enumerate(modules, start=2):
|
||||||
style = "champs champs_" + scu.ModuleType(mod.module_type).name
|
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):
|
for (col, ue) in enumerate(ues, start=2):
|
||||||
# met en gris les coefs qui devraient être nuls
|
# met en gris les coefs qui devraient être nuls
|
||||||
# car le module n'est pas dans le parcours de l'UE:
|
# car le module n'est pas dans le parcours de l'UE:
|
||||||
if (
|
if (
|
||||||
(mod.parcours is not None)
|
(mod.parcours is not None)
|
||||||
and (ue.parcour_id is not None)
|
and (ue.parcours)
|
||||||
and ue.parcour_id not in (p.id for p in mod.parcours)
|
and not {p.id for p in ue.parcours}.intersection(mod_parcours_ids)
|
||||||
):
|
):
|
||||||
cell_style = style + " champs_coef_hors_parcours"
|
cell_style = style + " champs_coef_hors_parcours"
|
||||||
else:
|
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"
|
competence: "Solutions TP"
|
||||||
'UE5.3':
|
'UE5.3':
|
||||||
annee: BUT3
|
annee: BUT3
|
||||||
parcours: RAPEB # + BEC
|
parcours: [RAPEB, BEC]
|
||||||
competence: "Dimensionner"
|
competence: "Dimensionner"
|
||||||
'UE5.4':
|
'UE5.4':
|
||||||
annee: BUT3
|
annee: BUT3
|
||||||
parcours: BAT # + TP
|
parcours: [BAT, TP]
|
||||||
competence: Organiser
|
competence: Organiser
|
||||||
'UE5.5':
|
'UE5.5':
|
||||||
annee: BUT3
|
annee: BUT3
|
||||||
parcours: BAT # + TP
|
parcours: [BAT, TP]
|
||||||
competence: Piloter
|
competence: Piloter
|
||||||
# S6 Parcours BAT + TP
|
# S6 Parcours BAT + TP
|
||||||
'UE6.1': # Parcours BAT seulement
|
'UE6.1': # Parcours BAT seulement
|
||||||
@ -104,19 +104,19 @@ Formation:
|
|||||||
competence: "Solutions Bâtiment"
|
competence: "Solutions Bâtiment"
|
||||||
'UE6.2': # Parcours TP seulement
|
'UE6.2': # Parcours TP seulement
|
||||||
annee: BUT3
|
annee: BUT3
|
||||||
parcours: TP # + BEC
|
parcours: [TP,BEC]
|
||||||
competence: "Solutions TP"
|
competence: "Solutions TP"
|
||||||
'UE6.3':
|
'UE6.3':
|
||||||
annee: BUT3
|
annee: BUT3
|
||||||
parcours: RAPEB # + BEC
|
parcours: [RAPEB,BEC]
|
||||||
competence: "Dimensionner"
|
competence: "Dimensionner"
|
||||||
'UE6.4':
|
'UE6.4':
|
||||||
annee: BUT3
|
annee: BUT3
|
||||||
parcours: BAT # + TP
|
parcours: [BAT, TP]
|
||||||
competence: Organiser
|
competence: Organiser
|
||||||
'UE6.5':
|
'UE6.5':
|
||||||
annee: BUT3
|
annee: BUT3
|
||||||
parcours: BAT # + TP
|
parcours: [BAT,TP]
|
||||||
competence: Piloter
|
competence: Piloter
|
||||||
|
|
||||||
modules_parcours:
|
modules_parcours:
|
||||||
|
@ -111,7 +111,7 @@ def build_modules_with_evaluations(
|
|||||||
modimpl = models.ModuleImpl.query.get(moduleimpl_id)
|
modimpl = models.ModuleImpl.query.get(moduleimpl_id)
|
||||||
assert modimpl.formsemestre.formation.get_cursus().APC_SAE # BUT
|
assert modimpl.formsemestre.formation.get_cursus().APC_SAE # BUT
|
||||||
# Check ModuleImpl
|
# Check ModuleImpl
|
||||||
ues = modimpl.formsemestre.query_ues().all()
|
ues = modimpl.formsemestre.get_ues()
|
||||||
assert len(ues) == 3
|
assert len(ues) == 3
|
||||||
#
|
#
|
||||||
for _ in range(nb_evals_per_modimpl):
|
for _ in range(nb_evals_per_modimpl):
|
||||||
|
@ -24,7 +24,7 @@ from tests.unit import yaml_setup, yaml_setup_but
|
|||||||
|
|
||||||
import app
|
import app
|
||||||
from app.but.jury_but_validation_auto import formsemestre_validation_auto_but
|
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
|
from config import TestConfig
|
||||||
|
|
||||||
DEPT = TestConfig.DEPT_TEST
|
DEPT = TestConfig.DEPT_TEST
|
||||||
@ -133,7 +133,11 @@ def test_but_jury_GCCD_CY(test_client):
|
|||||||
assert parcour_BAT
|
assert parcour_BAT
|
||||||
# check le nombre d'UE dans chaque semestre BUT:
|
# check le nombre d'UE dans chaque semestre BUT:
|
||||||
assert [
|
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)
|
for i in range(1, 7)
|
||||||
] == [5, 5, 5, 5, 3, 3]
|
] == [5, 5, 5, 5, 3, 3]
|
||||||
# Vérifie les UEs du parcours TP
|
# Vérifie les UEs du parcours TP
|
||||||
@ -141,6 +145,10 @@ def test_but_jury_GCCD_CY(test_client):
|
|||||||
assert parcour_TP
|
assert parcour_TP
|
||||||
# check le nombre d'UE dans chaque semestre BUT:
|
# check le nombre d'UE dans chaque semestre BUT:
|
||||||
assert [
|
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)
|
for i in range(1, 7)
|
||||||
] == [5, 5, 5, 5, 3, 3]
|
] == [5, 5, 5, 5, 3, 3]
|
||||||
|
@ -30,6 +30,9 @@ REF_MLT_XML = open(
|
|||||||
REF_GCCD_XML = open(
|
REF_GCCD_XML = open(
|
||||||
"ressources/referentiels/but2022/competences/but-GCCD-05012022-081630.xml"
|
"ressources/referentiels/but2022/competences/but-GCCD-05012022-081630.xml"
|
||||||
).read()
|
).read()
|
||||||
|
REF_INFO_XML = open(
|
||||||
|
"ressources/referentiels/but2022/competences/but-INFO-05012022-081701.xml"
|
||||||
|
).read()
|
||||||
|
|
||||||
|
|
||||||
def test_but_refcomp(test_client):
|
def test_but_refcomp(test_client):
|
||||||
@ -125,20 +128,20 @@ def test_refcomp_niveaux_mlt(test_client):
|
|||||||
# Vérifier les niveaux_by_parcours
|
# Vérifier les niveaux_by_parcours
|
||||||
parcour = ref_comp.parcours.first()
|
parcour = ref_comp.parcours.first()
|
||||||
# BUT 1
|
# 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 parcours == [parcour] # le parcours indiqué
|
||||||
assert (tuple(niveaux_by_parcours.keys())) == (parcour.id, "TC")
|
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[parcour.id] == [] # tout en tronc commun en BUT1 MLT
|
||||||
assert niveaux_by_parcours["TC"][0].competence.titre == "Transporter"
|
assert niveaux_by_parcours["TC"][0].competence.titre == "Transporter"
|
||||||
assert len(niveaux_by_parcours["TC"]) == 3
|
assert len(niveaux_by_parcours["TC"]) == 3
|
||||||
# BUT 2
|
# 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 parcours == [parcour] # le parcours indiqué
|
||||||
assert (tuple(niveaux_by_parcours.keys())) == (parcour.id, "TC")
|
assert (tuple(niveaux_by_parcours.keys())) == (parcour.id, "TC")
|
||||||
assert len(niveaux_by_parcours[parcour.id]) == 1
|
assert len(niveaux_by_parcours[parcour.id]) == 1
|
||||||
assert len(niveaux_by_parcours["TC"]) == 3
|
assert len(niveaux_by_parcours["TC"]) == 3
|
||||||
# BUT 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 parcours == [parcour] # le parcours indiqué
|
||||||
assert (tuple(niveaux_by_parcours.keys())) == (parcour.id, "TC")
|
assert (tuple(niveaux_by_parcours.keys())) == (parcour.id, "TC")
|
||||||
assert len(niveaux_by_parcours[parcour.id]) == 1
|
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
|
# Vérifier les niveaux_by_parcours
|
||||||
parcour = ref_comp.parcours.first()
|
parcour = ref_comp.parcours.first()
|
||||||
# BUT 1
|
# 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 parcours == [parcour] # le parcours indiqué
|
||||||
assert (tuple(niveaux_by_parcours.keys())) == (parcour.id, "TC")
|
assert (tuple(niveaux_by_parcours.keys())) == (parcour.id, "TC")
|
||||||
assert len(niveaux_by_parcours[parcour.id]) == 0
|
assert len(niveaux_by_parcours[parcour.id]) == 0
|
||||||
assert len(niveaux_by_parcours["TC"]) == 5
|
assert len(niveaux_by_parcours["TC"]) == 5
|
||||||
# BUT 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 parcours == [parcour] # le parcours indiqué
|
||||||
assert (tuple(niveaux_by_parcours.keys())) == (parcour.id, "TC")
|
assert (tuple(niveaux_by_parcours.keys())) == (parcour.id, "TC")
|
||||||
assert len(niveaux_by_parcours[parcour.id]) == 3
|
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
|
assert ue is not None # l'UE doit exister dans la formation avec cet acronyme
|
||||||
# Parcours:
|
# Parcours:
|
||||||
if ue_infos.get("parcours", False):
|
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
|
||||||
code=ue_infos["parcours"]
|
if isinstance(ue_infos["parcours"], list):
|
||||||
).first()
|
parcours = [
|
||||||
assert parcour is not None # le parcours indiqué pour cette UE doit exister
|
referentiel_competence.parcours.filter_by(code=code_parcour).first()
|
||||||
ue.set_parcour(parcour)
|
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:
|
# Niveaux compétences:
|
||||||
competence = referentiel_competence.competences.filter_by(
|
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 deca.validation is None # pas encore de validation enregistrée
|
||||||
assert False is deca.recorded
|
assert False is deca.recorded
|
||||||
assert deca.code_valide is None
|
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:
|
if formsemestre.semestre_id % 2:
|
||||||
assert deca.formsemestre_impair == formsemestre
|
assert deca.formsemestre_impair == formsemestre
|
||||||
assert formsemestre.query_ues_parcours_etud(etud.id).all() == deca.ues_impair
|
assert ues == deca.ues_impair
|
||||||
else:
|
else:
|
||||||
assert deca.formsemestre_pair == formsemestre
|
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 == scu.INSCRIT
|
||||||
assert deca.inscription_etat_impair == scu.INSCRIT
|
assert deca.inscription_etat_impair == scu.INSCRIT
|
||||||
assert (deca.parcour is None) or (
|
assert (deca.parcour is None) or (
|
||||||
@ -271,24 +290,27 @@ def check_deca_fields(formsemestre: FormSemestre, etud: Identite = None):
|
|||||||
)
|
)
|
||||||
|
|
||||||
nb_ues = (
|
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
|
if deca.formsemestre_pair
|
||||||
else 0
|
else 0
|
||||||
)
|
)
|
||||||
nb_ues += (
|
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
|
if deca.formsemestre_impair
|
||||||
else 0
|
else 0
|
||||||
)
|
)
|
||||||
assert len(deca.decisions_ues) == nb_ues
|
assert len(deca.decisions_ues) == nb_ues
|
||||||
|
|
||||||
nb_ues_un_sem = (
|
assert len(deca.niveaux_competences) == len(ues)
|
||||||
len(deca.formsemestre_impair.query_ues_parcours_etud(etud.id).all())
|
assert deca.nb_competences == len(ues)
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
def but_test_jury(formsemestre: FormSemestre, doc: dict):
|
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_debut = formsemestre.date_debut
|
||||||
date_fin = formsemestre.date_fin
|
date_fin = formsemestre.date_fin
|
||||||
|
|
||||||
list_ues = formsemestre.query_ues()
|
list_ues = formsemestre.get_ues()
|
||||||
|
|
||||||
def saisir_notes(evaluation_id: int, condition: int):
|
def saisir_notes(evaluation_id: int, condition: int):
|
||||||
"""
|
"""
|
||||||
|
Loading…
Reference in New Issue
Block a user