ScoDoc/app/but/cursus_but.py

867 lines
34 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(
"""UE d'un RCUE non associée à un niveau de compétence.
Vérifiez la formation et les associations de ses UEs.
"""
)
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:
key = (
v.ue.semestre_idx,
v.ue.niveau_competence.id if v.ue.niveau_competence else None,
)
if v.code in CODES_UE_VALIDES:
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)))
# 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)),
)
)
)
# 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&nbsp;:
<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