forked from ScoDoc/ScoDoc
885 lines
35 KiB
Python
885 lines
35 KiB
Python
##############################################################################
|
|
# ScoDoc
|
|
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
|
# See LICENSE
|
|
##############################################################################
|
|
|
|
"""Cursus en BUT
|
|
|
|
Classe raccordant avec ScoDoc 7:
|
|
ScoDoc 7 utilisait sco_cursus_dut.SituationEtudCursus
|
|
|
|
Ce module définit une classe SituationEtudCursusBUT
|
|
avec la même interface.
|
|
|
|
"""
|
|
import collections
|
|
from collections.abc import Iterable
|
|
from operator import attrgetter
|
|
|
|
from flask import g, url_for
|
|
|
|
from app import db, log
|
|
from app.comp.res_but import ResultatsSemestreBUT
|
|
from app.comp.res_compat import NotesTableCompat
|
|
|
|
from app.models.but_refcomp import (
|
|
ApcCompetence,
|
|
ApcNiveau,
|
|
ApcParcours,
|
|
ApcReferentielCompetences,
|
|
)
|
|
from app.models.ues import UEParcours
|
|
from app.models.but_validations import ApcValidationAnnee, ApcValidationRCUE
|
|
from app.models.etudiants import Identite
|
|
from app.models.formations import Formation
|
|
from app.models.formsemestre import FormSemestre
|
|
from app.models.ues import UniteEns
|
|
from app.models.validations import ScolarFormSemestreValidation
|
|
from app.scodoc import codes_cursus as sco_codes
|
|
from app.scodoc.codes_cursus import (
|
|
code_ue_validant,
|
|
CODES_UE_VALIDES,
|
|
CursusBUT,
|
|
UE_STANDARD,
|
|
)
|
|
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences, ScoValueError
|
|
from app.scodoc import sco_cursus_dut
|
|
|
|
|
|
class SituationEtudCursusBUT(sco_cursus_dut.SituationEtudCursusClassic):
|
|
"""Pour compat ScoDoc 7"""
|
|
|
|
def __init__(self, etud: Identite, formsemestre_id: int, res: ResultatsSemestreBUT):
|
|
super().__init__(etud, formsemestre_id, res)
|
|
# Ajustements pour le BUT
|
|
self.can_compensate_with_prev = False # jamais de compensation à la mode DUT
|
|
|
|
def check_compensation_dut(self, semc: dict, ntc: NotesTableCompat):
|
|
"Jamais de compensation façon DUT"
|
|
return False
|
|
|
|
def parcours_validated(self):
|
|
"""True si le parcours (ici diplôme BUT) est validé.
|
|
Considère le parcours du semestre en cours (res).
|
|
"""
|
|
parcour_id = self.nt.etuds_parcour_id.get(self.etud.id)
|
|
return but_parcours_validated(self.etud, parcour_id)
|
|
|
|
|
|
def but_annee_validated(
|
|
etudid: int, referentiel_competence_id: int, annee: int = 3
|
|
) -> bool:
|
|
"""Vrai si une validation de l'année BUT est enregistrée"""
|
|
return any(
|
|
sco_codes.code_annee_validant(v.code)
|
|
for v in ApcValidationAnnee.query.filter_by(
|
|
etudid=etudid,
|
|
ordre=annee,
|
|
referentiel_competence_id=referentiel_competence_id,
|
|
)
|
|
)
|
|
|
|
|
|
def but_parcours_validated(etud: Identite, parcour_id: int | None) -> bool:
|
|
"""Détermine si le parcours BUT est validé.
|
|
= 180 ECTS acquis dans les UEs du parcours.
|
|
"""
|
|
if parcour_id is None:
|
|
return False # étudiant non inscrit à un parcours
|
|
# Les ECTS
|
|
validations = but_validations_ues_parcours(etud, parcour_id)
|
|
ects_acquis = validations_count_ects(validations)
|
|
return ects_acquis >= CursusBUT.ECTS_DIPLOME
|
|
|
|
|
|
class EtudCursusBUT:
|
|
"""L'état de l'étudiant dans son cursus BUT
|
|
Liste des niveaux validés/à valider
|
|
(utilisé pour le résumé sur la fiche étudiant)
|
|
"""
|
|
|
|
def __init__(self, etud: Identite, formation: Formation):
|
|
"""formation indique la spécialité préparée"""
|
|
# Vérifie que l'étudiant est bien inscrit à un sem. de cette formation
|
|
if formation.id not in (
|
|
ins.formsemestre.formation.id for ins in etud.formsemestre_inscriptions
|
|
):
|
|
raise ScoValueError(
|
|
f"{etud.nomprenom} non inscrit dans {formation.titre} v{formation.version}"
|
|
)
|
|
if not formation.referentiel_competence:
|
|
raise ScoNoReferentielCompetences(formation=formation)
|
|
#
|
|
self.etud = etud
|
|
self.formation = formation
|
|
"Liste des inscriptions aux sem. de la formation, triées par indice et chronologie"
|
|
self.parcour: ApcParcours = get_etud_parcours(
|
|
etud, formation.referentiel_competence_id
|
|
)
|
|
"Le parcours à valider: celui du DERNIER semestre suivi (peut être None)"
|
|
self.niveaux_by_annee: dict[int, list[ApcNiveau]] = {}
|
|
"{ annee:int : 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] if self.parcour else None
|
|
)[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"), ... } }"""
|
|
validation_rcue: ApcValidationRCUE
|
|
for validation_rcue in ApcValidationRCUE.query.filter_by(etud=etud):
|
|
niveau = validation_rcue.niveau()
|
|
if niveau is None:
|
|
raise ScoValueError(
|
|
f"""UE d'un RCUE ({
|
|
validation_rcue.ue1.acronyme}/{validation_rcue.ue1.acronyme
|
|
}) non associée à un niveau de compétence.
|
|
Vérifiez la formation et les associations de ses UEs.
|
|
Étudiant {etud.nomprenom}.
|
|
Formations concernées: <a href="{
|
|
url_for('notes.ue_table', scodoc_dept=g.scodoc_dept,
|
|
formation_id=validation_rcue.ue1.formation_id,
|
|
semestre_idx=validation_rcue.ue1.semestre_idx)
|
|
}">{validation_rcue.ue1.acronyme}</a>,
|
|
<a href="{
|
|
url_for('notes.ue_table', scodoc_dept=g.scodoc_dept,
|
|
formation_id=validation_rcue.ue2.formation_id,
|
|
semestre_idx=validation_rcue.ue2.semestre_idx)
|
|
}">{validation_rcue.ue2.acronyme}</a>.
|
|
"""
|
|
)
|
|
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_ORDER[validation_rcue.code]
|
|
> sco_codes.BUT_CODES_ORDER[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 to_dict(self):
|
|
"""
|
|
{
|
|
competence_id : {
|
|
annee : meilleure_validation
|
|
}
|
|
}
|
|
"""
|
|
# XXX lent, provisoirement utilisé par TableJury.add_but_competences()
|
|
return {
|
|
competence.id: {
|
|
annee: self.validation_par_competence_et_annee.get(
|
|
competence.id, {}
|
|
).get(annee)
|
|
for annee in ("BUT1", "BUT2", "BUT3")
|
|
}
|
|
for competence in self.competences.values()
|
|
}
|
|
|
|
# XXX TODO OPTIMISATION ACCESS TABLE JURY
|
|
def to_dict_codes(self) -> dict[int, dict[str, int]]:
|
|
"""
|
|
{
|
|
competence_id : {
|
|
annee : { validation }
|
|
}
|
|
}
|
|
où validation est un petit dict avec niveau_id, etc.
|
|
"""
|
|
d = {}
|
|
for competence in self.competences.values():
|
|
d[competence.id] = {}
|
|
for annee in ("BUT1", "BUT2", "BUT3"):
|
|
validation_rcue: ApcValidationRCUE = (
|
|
self.validation_par_competence_et_annee.get(competence.id, {}).get(
|
|
annee
|
|
)
|
|
)
|
|
|
|
d[competence.id][annee] = (
|
|
validation_rcue.to_dict_codes() if validation_rcue else None
|
|
)
|
|
return d
|
|
|
|
def competence_annee_has_niveau(self, competence_id: int, annee: str) -> bool:
|
|
"vrai si la compétence à un niveau dans cette annee ('BUT1') pour le parcour de cet etud"
|
|
# slow, utile pour affichage fiche
|
|
return annee in [n.annee for n in self.competences[competence_id].niveaux]
|
|
|
|
def get_ects_acquis(self) -> int:
|
|
"Nombre d'ECTS validés par etud dans le parcours BUT de ce référentiel"
|
|
return but_ects_valides(
|
|
self.etud,
|
|
self.formation.referentiel_competence.id,
|
|
parcour_id=self.parcour.id if self.parcour is not None else None,
|
|
)
|
|
|
|
def load_validation_by_niveau(self) -> dict[int, list[ApcValidationRCUE]]:
|
|
"""Cherche les validations de jury enregistrées pour chaque niveau
|
|
Résultat: { niveau_id : [ ApcValidationRCUE ] }
|
|
meilleure validation pour ce niveau
|
|
"""
|
|
validations_by_niveau = collections.defaultdict(lambda: [])
|
|
for validation_rcue in ApcValidationRCUE.query.filter_by(etud=self.etud):
|
|
validations_by_niveau[validation_rcue.niveau().id].append(validation_rcue)
|
|
validation_by_niveau = {
|
|
niveau_id: sorted(
|
|
validations, key=lambda v: sco_codes.BUT_CODES_ORDER[v.code]
|
|
)[0]
|
|
for niveau_id, validations in validations_by_niveau.items()
|
|
if validations
|
|
}
|
|
return validation_by_niveau
|
|
|
|
|
|
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] = db.session.get(
|
|
ApcParcours, 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] if parcour else None
|
|
)[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_ORDER[validation_rcue.code]
|
|
# > sco_codes.BUT_CODES_ORDER[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] if self.parcour else None # XXX WIP
|
|
# )[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_ORDER[validation_rcue.code]
|
|
# > sco_codes.BUT_CODES_ORDER[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 get_etud_parcours(
|
|
etud: Identite, referentiel_competence_id: int | None
|
|
) -> ApcParcours | None:
|
|
"""Le parcours de l'étudiant dans ce réf. de compétence, ou None.
|
|
= celui du DERNIER semestre suivi dans le référentiel de compétence
|
|
(peut être None si l'incription n'a pas de parcours)
|
|
"""
|
|
inscriptions = sorted(
|
|
[
|
|
ins
|
|
for ins in etud.formsemestre_inscriptions
|
|
if ins.formsemestre.formation.referentiel_competence
|
|
and (
|
|
ins.formsemestre.formation.referentiel_competence.id
|
|
== referentiel_competence_id
|
|
)
|
|
],
|
|
key=lambda s: (s.formsemestre.date_debut, s.formsemestre.semestre_id),
|
|
)
|
|
return inscriptions[-1].parcour if inscriptions else None
|
|
|
|
|
|
def but_ects_valides(
|
|
etud: Identite,
|
|
referentiel_competence_id: int | None = None,
|
|
annees_but: None | Iterable[str] = None,
|
|
parcour_id: int | None = None,
|
|
) -> int:
|
|
"""Nombre d'ECTS validés par etud dans le BUT de référentiel indiqué.
|
|
Ne prend que les UE associées à des niveaux de compétences,
|
|
et ne les compte qu'une fois même en cas de redoublement avec re-validation.
|
|
|
|
On peut spécifier soit le referentiel_competence_id, soit le parcour.
|
|
Si parcour est spécifié, ne prend que les UEs de ce parcours et du tronc commun.
|
|
|
|
Si annees_but est spécifié, un iterable "BUT1, "BUT2" par exemple, ne prend que ces années.
|
|
"""
|
|
validations = (
|
|
but_validations_ues_parcours(etud, parcour_id, annees_but)
|
|
if parcour_id is not None
|
|
else but_validations_ues(etud, referentiel_competence_id, annees_but)
|
|
)
|
|
return validations_count_ects(validations)
|
|
|
|
|
|
def validations_count_ects(validations: list[ScolarFormSemestreValidation]) -> int:
|
|
"""Somme les ECTS validés par ces UEs, en éliminant les éventuels
|
|
doublons (niveaux de compétences validés plusieurs fois)"""
|
|
ects_dict = {}
|
|
for v in validations:
|
|
if v.code in CODES_UE_VALIDES:
|
|
key = (
|
|
v.ue.semestre_idx,
|
|
v.ue.niveau_competence.id if v.ue.niveau_competence else None,
|
|
)
|
|
ects_dict[key] = v.ue.ects or 0.0
|
|
|
|
return int(sum(ects_dict.values())) if ects_dict else 0
|
|
|
|
|
|
def but_validations_ues(
|
|
etud: Identite,
|
|
referentiel_competence_id: int,
|
|
annees_but: None | Iterable[str] = None,
|
|
) -> list[ScolarFormSemestreValidation]:
|
|
"""Query les validations d'UEs pour cet étudiant
|
|
dans des UEs appartenant à ce référentiel de compétence
|
|
et en option pour les années BUT indiquées.
|
|
annees_but : None (tout) ou liste [ "BUT1", ... ]
|
|
"""
|
|
validations = (
|
|
ScolarFormSemestreValidation.query.filter_by(etudid=etud.id)
|
|
.filter(ScolarFormSemestreValidation.ue_id != None)
|
|
.join(UniteEns)
|
|
.join(ApcNiveau)
|
|
)
|
|
# restreint à certaines années (utile pour les ECTS du DUT120)
|
|
if annees_but:
|
|
validations = validations.filter(ApcNiveau.annee.in_(annees_but))
|
|
# restreint au référentiel de compétence
|
|
validations = validations.join(ApcCompetence).filter_by(
|
|
referentiel_id=referentiel_competence_id
|
|
)
|
|
return sorted_validations(validations)
|
|
|
|
|
|
def sorted_validations(validations) -> list[ScolarFormSemestreValidation]:
|
|
"""Tri (nb: fait en python pour gérer les validations externes qui
|
|
n'ont pas de formsemestre)"""
|
|
return sorted(
|
|
validations,
|
|
key=lambda v: (
|
|
(v.formsemestre.semestre_id, v.ue.numero, v.ue.acronyme)
|
|
if v.formsemestre
|
|
else (v.ue.semestre_idx or -2, v.ue.numero, v.ue.acronyme)
|
|
),
|
|
)
|
|
|
|
|
|
def but_validations_ues_parcours(
|
|
etud: Identite, parcour_id: int, annees_but: None | Iterable[str] = None
|
|
) -> list[ScolarFormSemestreValidation]:
|
|
"""Query les validations d'UEs pour cet étudiant
|
|
dans des UEs appartenant à ce parcours ou à son tronc commun.
|
|
"""
|
|
# Rappel:
|
|
# Les UEs associées à un parcours:
|
|
# UniteEns.query.join(UEParcours).filter(UEParcours.parcours_id == parcour.id) )
|
|
# Les UEs associées au tronc commun (à aucun parcours)
|
|
# UniteEns.query.filter(~UniteEns.id.in_(UEParcours.query.with_entities(UEParcours.ue_id)))
|
|
|
|
parcour: ApcParcours = db.session.get(ApcParcours, parcour_id)
|
|
if not parcour:
|
|
raise ScoValueError(f"but_validations_ues_parcours: {parcour_id} inexistant")
|
|
# Les validations d'UE de ce parcours ou du tronc commun pour cet étudiant:
|
|
validations = (
|
|
ScolarFormSemestreValidation.query.filter_by(etudid=etud.id)
|
|
.filter(ScolarFormSemestreValidation.ue_id != None)
|
|
.join(UniteEns)
|
|
.filter(
|
|
db.or_(
|
|
UniteEns.id.in_(
|
|
UEParcours.query.with_entities(UEParcours.ue_id).filter(
|
|
UEParcours.parcours_id == parcour_id
|
|
)
|
|
),
|
|
~UniteEns.id.in_(UEParcours.query.with_entities(UEParcours.ue_id)),
|
|
)
|
|
)
|
|
.join(Formation)
|
|
.filter_by(referentiel_competence_id=parcour.referentiel_id)
|
|
)
|
|
# restreint à certaines années (utile pour les ECTS du DUT120)
|
|
if annees_but:
|
|
validations = validations.join(ApcNiveau).filter(
|
|
ApcNiveau.annee.in_(annees_but)
|
|
)
|
|
return sorted_validations(validations)
|
|
|
|
|
|
def etud_ues_de_but1_non_validees(
|
|
etud: Identite, formation: Formation, parcour: ApcParcours
|
|
) -> list[UniteEns]:
|
|
"""Liste des UEs de S1 et S2 non validées, dans son parcours"""
|
|
# Les UEs avec décisions, dans les S1 ou S2 d'une formation de même code:
|
|
validations = (
|
|
ScolarFormSemestreValidation.query.filter_by(etudid=etud.id)
|
|
.filter(ScolarFormSemestreValidation.ue_id != None)
|
|
.join(UniteEns)
|
|
.filter(db.or_(UniteEns.semestre_idx == 1, UniteEns.semestre_idx == 2))
|
|
.join(Formation)
|
|
.filter_by(formation_code=formation.formation_code)
|
|
)
|
|
codes_validations_by_ue_code = collections.defaultdict(list)
|
|
for v in validations:
|
|
codes_validations_by_ue_code[v.ue.ue_code].append(v.code)
|
|
|
|
# Les UEs du parcours en S1 et S2:
|
|
ues = formation.query_ues_parcour(parcour).filter(
|
|
db.or_(UniteEns.semestre_idx == 1, UniteEns.semestre_idx == 2)
|
|
)
|
|
# Liste triée des ues non validées
|
|
return sorted(
|
|
[
|
|
ue
|
|
for ue in ues
|
|
if not any(
|
|
(
|
|
code_ue_validant(code)
|
|
for code in codes_validations_by_ue_code[ue.ue_code]
|
|
)
|
|
)
|
|
],
|
|
key=attrgetter("numero", "acronyme"),
|
|
)
|
|
|
|
|
|
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 ""
|
|
url_formation = url_for(
|
|
"notes.ue_table",
|
|
scodoc_dept=g.scodoc_dept,
|
|
formation_id=formsemestre.formation.id,
|
|
semestre_idx=formsemestre.semestre_id,
|
|
)
|
|
if formsemestre.formation.referentiel_competence is None:
|
|
return f"""<div class="formsemestre_status_warning">
|
|
La <a class="stdlink" href="{url_formation}">formation
|
|
n'est pas associée à un référentiel de compétence.</a>
|
|
</div>
|
|
"""
|
|
H = []
|
|
# Le semestre n'a pas de parcours, mais les UE ont des parcours ?
|
|
if not formsemestre.parcours:
|
|
nb_ues_sans_parcours = len(
|
|
formsemestre.formation.query_ues_parcour(None)
|
|
.filter(UniteEns.semestre_idx == formsemestre.semestre_id)
|
|
.all()
|
|
)
|
|
nb_ues_tot = (
|
|
UniteEns.query.filter_by(formation=formsemestre.formation, type=UE_STANDARD)
|
|
.filter(UniteEns.semestre_idx == formsemestre.semestre_id)
|
|
.count()
|
|
)
|
|
if nb_ues_sans_parcours != nb_ues_tot:
|
|
H.append(
|
|
"""Le semestre n'est associé à aucun parcours,
|
|
mais les UEs de la formation ont des parcours
|
|
"""
|
|
)
|
|
# Vérifie les niveaux de chaque parcours
|
|
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
|
|
<a class="stdlink" href="{url_formation}">configuration de la formation</a>:
|
|
<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.parcour_formation", scodoc_dept=g.scodoc_dept, formation_id=formsemestre.formation.id)
|
|
}">dans la formation.</a>
|
|
</p>
|
|
</div>
|
|
"""
|
|
|
|
|
|
def formation_semestre_niveaux_warning(formation: Formation, semestre_idx: int) -> str:
|
|
"""Vérifie que tous les niveaux de compétences de cette année de formation
|
|
ont bien des UEs.
|
|
Afin de ne pas générer trop de messages, on ne considère que les parcours
|
|
du référentiel de compétences pour lesquels au moins une UE a été associée.
|
|
|
|
Renvoie fragment de html
|
|
"""
|
|
annee = (semestre_idx - 1) // 2 + 1 # année BUT
|
|
ref_comp: ApcReferentielCompetences = formation.referentiel_competence
|
|
if not ref_comp:
|
|
return "" # détecté ailleurs...
|
|
niveaux_sans_ue_by_parcour = {} # { parcour.code : [ ue ... ] }
|
|
parcours_ids = {
|
|
uep.parcours_id
|
|
for uep in UEParcours.query.join(UniteEns).filter_by(
|
|
formation_id=formation.id, type=UE_STANDARD
|
|
)
|
|
}
|
|
for parcour in ref_comp.parcours:
|
|
if parcour.id not in parcours_ids:
|
|
continue # saute parcours associés à aucune UE (tous semestres)
|
|
niveaux_sans_ue = []
|
|
niveaux = ApcNiveau.niveaux_annee_de_parcours(parcour, annee, ref_comp)
|
|
# print(f"\n# Parcours {parcour.code} : {len(niveaux)} niveaux")
|
|
for niveau in niveaux:
|
|
ues = [ue for ue in formation.ues if ue.niveau_competence_id == niveau.id]
|
|
if not ues:
|
|
niveaux_sans_ue.append(niveau)
|
|
# print( niveau.competence.titre + " " + str(niveau.ordre) + "\t" + str(ue) )
|
|
if niveaux_sans_ue:
|
|
niveaux_sans_ue_by_parcour[parcour.code] = niveaux_sans_ue
|
|
#
|
|
H = []
|
|
for parcour_code, niveaux in niveaux_sans_ue_by_parcour.items():
|
|
H.append(
|
|
f"""<li>Parcours {parcour_code} : {
|
|
len(niveaux)} niveaux sans UEs :
|
|
<span class="niveau-nom"><span>
|
|
{ '</span>, <span>'.join( f'{niveau.competence.titre} {niveau.ordre}'
|
|
for niveau in niveaux
|
|
)
|
|
}
|
|
</span>
|
|
</li>
|
|
"""
|
|
)
|
|
# Combien de compétences de tronc commun ?
|
|
_, niveaux_by_parcours = ref_comp.get_niveaux_by_parcours(annee)
|
|
nb_niveaux_tc = len(niveaux_by_parcours["TC"])
|
|
nb_ues_tc = len(
|
|
formation.query_ues_parcour(None)
|
|
.filter(UniteEns.semestre_idx == semestre_idx)
|
|
.all()
|
|
)
|
|
if nb_niveaux_tc != nb_ues_tc:
|
|
H.append(
|
|
f"""<li>{nb_niveaux_tc} niveaux de compétences de tronc commun,
|
|
mais {nb_ues_tc} UEs de tronc commun ! (c'est normal si
|
|
vous avez des UEs différenciées par parcours)</li>"""
|
|
)
|
|
|
|
if H:
|
|
return f"""<div class="formation_semestre_niveaux_warning">
|
|
<div>Problèmes détectés à corriger :</div>
|
|
<ul>
|
|
{"".join(H)}
|
|
</ul>
|
|
</div>
|
|
"""
|
|
return "" # no problem detected
|
|
|
|
|
|
def ue_associee_au_niveau_du_parcours(
|
|
ues_possibles: list[UniteEns], niveau: ApcNiveau, sem_name: str = "S"
|
|
) -> tuple[UniteEns, str]:
|
|
"""L'UE associée à ce niveau, ou None.
|
|
Renvoie aussi un message d'avertissement en cas d'associations multiples
|
|
(en principe un niveau ne doit être associé qu'à une seule UE)
|
|
"""
|
|
ues = [ue for ue in ues_possibles if ue.niveau_competence_id == niveau.id]
|
|
msg = ""
|
|
if len(ues) > 1:
|
|
msg = f"""{' et '.join(ue.acronyme for ue in ues)}
|
|
associées au niveau {niveau} / {sem_name}. Utilisez le cas échéant l'item "Désassocier"."""
|
|
# plusieurs UEs associées à ce niveau: élimine celles sans parcours
|
|
ues_avec_parcours = [ue for ue in ues if ue.parcours]
|
|
if ues_avec_parcours:
|
|
ues = ues_avec_parcours
|
|
if len(ues) > 1:
|
|
log(f"_niveau_ues: {len(ues)} associées au niveau {niveau} / {sem_name}")
|
|
return ues[0] if ues else None, msg
|
|
|
|
|
|
def parcour_formation_competences(
|
|
parcour: ApcParcours, formation: Formation
|
|
) -> tuple[list[dict], float]:
|
|
"""
|
|
[
|
|
{
|
|
'competence' : ApcCompetence,
|
|
'niveaux' : {
|
|
1 : { ... },
|
|
2 : { ... },
|
|
3 : {
|
|
'niveau' : ApcNiveau,
|
|
'ue_impair' : UniteEns, # actuellement associée
|
|
'ues_impair' : list[UniteEns], # choix possibles
|
|
'ue_pair' : UniteEns,
|
|
'ues_pair' : list[UniteEns],
|
|
}
|
|
}
|
|
}
|
|
],
|
|
ects_parcours (somme des ects des UEs associées)
|
|
"""
|
|
refcomp: ApcReferentielCompetences = formation.referentiel_competence
|
|
|
|
def _niveau_ues(competence: ApcCompetence, annee: int) -> dict:
|
|
"""niveau et ues pour cette compétence de cette année du parcours.
|
|
Si parcour est None, les niveaux du tronc commun
|
|
"""
|
|
if parcour is not None:
|
|
# L'étudiant est inscrit à un parcours: cherche les niveaux
|
|
niveaux = ApcNiveau.niveaux_annee_de_parcours(
|
|
parcour, annee, competence=competence
|
|
)
|
|
else:
|
|
# sans parcours, on cherche les niveaux du Tronc Commun de cette année
|
|
niveaux = [
|
|
niveau
|
|
for niveau in refcomp.get_niveaux_by_parcours(annee)[1]["TC"]
|
|
if niveau.competence_id == competence.id
|
|
]
|
|
|
|
if len(niveaux) > 0:
|
|
if len(niveaux) > 1:
|
|
log(
|
|
f"""_niveau_ues: plus d'un niveau pour {competence}
|
|
annee {annee} {("parcours " + parcour.code) if parcour else ""}"""
|
|
)
|
|
niveau = niveaux[0]
|
|
elif len(niveaux) == 0:
|
|
return {
|
|
"niveau": None,
|
|
"ue_pair": None,
|
|
"ue_impair": None,
|
|
"ues_pair": [],
|
|
"ues_impair": [],
|
|
"warning": "",
|
|
}
|
|
# Toutes les UEs de la formation dans ce parcours ou tronc commun
|
|
ues = [
|
|
ue
|
|
for ue in formation.ues
|
|
if (
|
|
(not ue.parcours)
|
|
or (parcour is not None and (parcour.id in (p.id for p in ue.parcours)))
|
|
)
|
|
and ue.type == UE_STANDARD
|
|
]
|
|
ues_pair_possibles = [ue for ue in ues if ue.semestre_idx == (2 * annee)]
|
|
ues_impair_possibles = [ue for ue in ues if ue.semestre_idx == (2 * annee - 1)]
|
|
|
|
# UE associée au niveau dans ce parcours
|
|
ue_pair, warning_pair = ue_associee_au_niveau_du_parcours(
|
|
ues_pair_possibles, niveau, f"S{2*annee}"
|
|
)
|
|
ue_impair, warning_impair = ue_associee_au_niveau_du_parcours(
|
|
ues_impair_possibles, niveau, f"S{2*annee-1}"
|
|
)
|
|
|
|
return {
|
|
"niveau": niveau,
|
|
"ue_pair": ue_pair,
|
|
"ues_pair": [
|
|
ue
|
|
for ue in ues_pair_possibles
|
|
if (not ue.niveau_competence) or ue.niveau_competence.id == niveau.id
|
|
],
|
|
"ue_impair": ue_impair,
|
|
"ues_impair": [
|
|
ue
|
|
for ue in ues_impair_possibles
|
|
if (not ue.niveau_competence) or ue.niveau_competence.id == niveau.id
|
|
],
|
|
"warning": ", ".join(filter(None, [warning_pair, warning_impair])),
|
|
}
|
|
|
|
competences = [
|
|
{
|
|
"competence": competence,
|
|
"niveaux": {annee: _niveau_ues(competence, annee) for annee in (1, 2, 3)},
|
|
}
|
|
for competence in (
|
|
parcour.query_competences()
|
|
if parcour
|
|
else refcomp.competences.order_by(ApcCompetence.numero)
|
|
)
|
|
]
|
|
ects_parcours = sum(
|
|
sum(
|
|
(ni["ue_impair"].ects or 0) if ni["ue_impair"] else 0
|
|
for ni in cp["niveaux"].values()
|
|
)
|
|
for cp in competences
|
|
) + sum(
|
|
sum(
|
|
(ni["ue_pair"].ects or 0) if ni["ue_pair"] else 0
|
|
for ni in cp["niveaux"].values()
|
|
)
|
|
for cp in competences
|
|
)
|
|
return competences, ects_parcours
|