482 lines
19 KiB
Python
482 lines
19 KiB
Python
##############################################################################
|
|
# ScoDoc
|
|
# Copyright (c) 1999 - 2023 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 operator import attrgetter
|
|
from typing import Union
|
|
|
|
from flask import g, url_for
|
|
|
|
from app import db
|
|
from app import log
|
|
from app.comp.res_but import ResultatsSemestreBUT
|
|
from app.comp.res_compat import NotesTableCompat
|
|
|
|
from app.models.but_refcomp import (
|
|
ApcAnneeParcours,
|
|
ApcCompetence,
|
|
ApcNiveau,
|
|
ApcParcours,
|
|
ApcParcoursNiveauCompetence,
|
|
ApcReferentielCompetences,
|
|
)
|
|
from app.models import Scolog, ScolarAutorisationInscription
|
|
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, FormSemestreInscription
|
|
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
|
|
from app.scodoc import sco_utils as scu
|
|
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: à revoir pour le BUT"""
|
|
|
|
def __init__(self, etud: dict, 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 est validé"
|
|
return False # XXX TODO
|
|
|
|
|
|
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
|
|
self.inscriptions = sorted(
|
|
[
|
|
ins
|
|
for ins in etud.formsemestre_inscriptions
|
|
if ins.formsemestre.formation.referentiel_competence
|
|
and (
|
|
ins.formsemestre.formation.referentiel_competence.id
|
|
== formation.referentiel_competence.id
|
|
)
|
|
],
|
|
key=lambda s: (s.formsemestre.semestre_id, s.formsemestre.date_debut),
|
|
)
|
|
"Liste des inscriptions aux sem. de la formation, triées par indice et chronologie"
|
|
self.parcour: ApcParcours = self.inscriptions[-1].parcour
|
|
"Le parcours à valider: celui du DERNIER semestre suivi (peut être None)"
|
|
self.niveaux_by_annee = {}
|
|
"{ 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"), ... } }"""
|
|
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 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 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] = 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] 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 but_ects_valides(etud: Identite, referentiel_competence_id: int) -> float:
|
|
"""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.
|
|
"""
|
|
validations = (
|
|
ScolarFormSemestreValidation.query.filter_by(etudid=etud.id)
|
|
.filter(ScolarFormSemestreValidation.ue_id != None)
|
|
.join(UniteEns)
|
|
.join(ApcNiveau)
|
|
.join(ApcCompetence)
|
|
.filter_by(referentiel_id=referentiel_competence_id)
|
|
)
|
|
|
|
ects_dict = {}
|
|
for v in validations:
|
|
key = (v.ue.semestre_idx, v.ue.niveau_competence.id)
|
|
if v.code in CODES_UE_VALIDES:
|
|
ects_dict[key] = v.ue.ects
|
|
|
|
return sum(ects_dict.values()) if ects_dict else 0.0
|
|
|
|
|
|
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 ""
|
|
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.parcour_formation", scodoc_dept=g.scodoc_dept, formation_id=formsemestre.formation.id)
|
|
}">dans la formation.</a>
|
|
</p>
|
|
</div>
|
|
"""
|