2022-07-07 16:24:52 +02:00
|
|
|
##############################################################################
|
|
|
|
# ScoDoc
|
2023-01-02 13:16:27 +01:00
|
|
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
2022-07-07 16:24:52 +02:00
|
|
|
# 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.
|
|
|
|
|
|
|
|
"""
|
2023-01-17 01:05:48 +01:00
|
|
|
import collections
|
2023-06-15 21:53:05 +02:00
|
|
|
from operator import attrgetter
|
2022-07-07 16:24:52 +02:00
|
|
|
|
|
|
|
from flask import g, url_for
|
|
|
|
|
2023-07-16 19:59:45 +02:00
|
|
|
from app import db, log
|
2022-07-07 16:24:52 +02:00
|
|
|
from app.comp.res_but import ResultatsSemestreBUT
|
2022-07-07 23:34:14 +02:00
|
|
|
from app.comp.res_compat import NotesTableCompat
|
|
|
|
|
2022-07-07 16:24:52 +02:00
|
|
|
from app.models.but_refcomp import (
|
|
|
|
ApcAnneeParcours,
|
|
|
|
ApcCompetence,
|
|
|
|
ApcNiveau,
|
|
|
|
ApcParcours,
|
|
|
|
ApcParcoursNiveauCompetence,
|
2023-04-03 17:46:31 +02:00
|
|
|
ApcReferentielCompetences,
|
2022-07-07 16:24:52 +02:00
|
|
|
)
|
|
|
|
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
|
2023-02-12 13:36:47 +01:00
|
|
|
from app.scodoc import codes_cursus as sco_codes
|
2023-07-16 19:59:45 +02:00
|
|
|
from app.scodoc.codes_cursus import code_ue_validant, CODES_UE_VALIDES, UE_STANDARD
|
|
|
|
|
2022-07-07 16:24:52 +02:00
|
|
|
from app.scodoc import sco_utils as scu
|
2023-01-17 01:05:48 +01:00
|
|
|
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences, ScoValueError
|
2022-07-07 16:24:52 +02:00
|
|
|
|
|
|
|
from app.scodoc import sco_cursus_dut
|
|
|
|
|
|
|
|
|
2022-07-07 23:34:14 +02:00
|
|
|
class SituationEtudCursusBUT(sco_cursus_dut.SituationEtudCursusClassic):
|
2023-01-17 01:05:48 +01:00
|
|
|
"""Pour compat ScoDoc 7: à revoir pour le BUT"""
|
|
|
|
|
2022-07-07 16:24:52 +02:00
|
|
|
def __init__(self, etud: dict, formsemestre_id: int, res: ResultatsSemestreBUT):
|
2022-07-07 23:34:14 +02:00
|
|
|
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
|
2023-01-17 01:05:48 +01:00
|
|
|
|
|
|
|
|
|
|
|
class EtudCursusBUT:
|
|
|
|
"""L'état de l'étudiant dans son cursus BUT
|
|
|
|
Liste des niveaux validés/à valider
|
2023-06-22 21:15:23 +02:00
|
|
|
(utilisé pour le résumé sur la fiche étudiant)
|
2023-01-17 01:05:48 +01:00
|
|
|
"""
|
|
|
|
|
|
|
|
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
|
2023-01-18 10:31:49 +01:00
|
|
|
if ins.formsemestre.formation.referentiel_competence
|
|
|
|
and (
|
|
|
|
ins.formsemestre.formation.referentiel_competence.id
|
|
|
|
== formation.referentiel_competence.id
|
|
|
|
)
|
2023-01-17 01:05:48 +01:00
|
|
|
],
|
|
|
|
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
|
2023-01-22 21:27:37 +01:00
|
|
|
"Le parcours à valider: celui du DERNIER semestre suivi (peut être None)"
|
2023-07-02 16:02:09 +02:00
|
|
|
self.niveaux_by_annee: dict[int, list[ApcNiveau]] = {}
|
2023-06-15 08:49:05 +02:00
|
|
|
"{ annee:int : liste des niveaux à valider }"
|
2023-01-17 01:05:48 +01:00
|
|
|
self.niveaux: dict[int, ApcNiveau] = {}
|
|
|
|
"cache les niveaux"
|
|
|
|
for annee in (1, 2, 3):
|
|
|
|
niveaux_d = formation.referentiel_competence.get_niveaux_by_parcours(
|
2023-04-04 12:02:09 +02:00
|
|
|
annee, [self.parcour] if self.parcour else None
|
2023-01-17 01:05:48 +01:00
|
|
|
)[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 = {}
|
2023-02-12 23:03:12 +01:00
|
|
|
"""{ competence_id : { 'BUT1' : validation_rcue (la "meilleure"), ... } }"""
|
2023-01-17 01:05:48 +01:00
|
|
|
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
|
2023-01-23 19:08:00 +01:00
|
|
|
).get(validation_rcue.annee())
|
2023-01-17 01:05:48 +01:00
|
|
|
# prend la "meilleure" validation
|
|
|
|
if (not previous_validation) or (
|
2023-06-22 08:22:37 +02:00
|
|
|
sco_codes.BUT_CODES_ORDER[validation_rcue.code]
|
|
|
|
> sco_codes.BUT_CODES_ORDER[previous_validation.code]
|
2023-01-17 01:05:48 +01:00
|
|
|
):
|
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
"""
|
2023-04-03 17:46:31 +02:00
|
|
|
# XXX lent, provisoirement utilisé par TableJury.add_but_competences()
|
2023-01-17 01:05:48 +01:00
|
|
|
return {
|
|
|
|
competence.id: {
|
2023-02-12 23:03:12 +01:00
|
|
|
annee: self.validation_par_competence_et_annee.get(
|
|
|
|
competence.id, {}
|
|
|
|
).get(annee)
|
2023-01-17 01:05:48 +01:00
|
|
|
for annee in ("BUT1", "BUT2", "BUT3")
|
|
|
|
}
|
|
|
|
for competence in self.competences.values()
|
|
|
|
}
|
2023-02-12 23:03:12 +01:00
|
|
|
|
|
|
|
# XXX TODO OPTIMISATION ACCESS TABLE JURY
|
|
|
|
def to_dict_codes(self) -> dict[int, dict[str, int]]:
|
|
|
|
"""
|
|
|
|
{
|
|
|
|
competence_id : {
|
2023-04-03 17:46:31 +02:00
|
|
|
annee : { validation }
|
2023-02-12 23:03:12 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
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
|
2023-04-03 17:46:31 +02:00
|
|
|
|
2023-06-22 21:15:23 +02:00
|
|
|
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]
|
|
|
|
|
2023-06-15 08:49:05 +02:00
|
|
|
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(
|
2023-06-22 08:22:37 +02:00
|
|
|
validations, key=lambda v: sco_codes.BUT_CODES_ORDER[v.code]
|
2023-06-15 08:49:05 +02:00
|
|
|
)[0]
|
|
|
|
for niveau_id, validations in validations_by_niveau.items()
|
|
|
|
if validations
|
|
|
|
}
|
|
|
|
return validation_by_niveau
|
|
|
|
|
2023-04-03 17:46:31 +02:00
|
|
|
|
|
|
|
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:
|
2023-07-11 06:57:38 +02:00
|
|
|
self.parcours_by_id[parcour_id] = db.session.get(
|
|
|
|
ApcParcours, parcour_id
|
|
|
|
)
|
2023-04-03 17:46:31 +02:00
|
|
|
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):
|
2023-04-04 12:02:09 +02:00
|
|
|
niveaux_d = ref_comp.get_niveaux_by_parcours(
|
|
|
|
annee, [parcour] if parcour else None
|
|
|
|
)[1]
|
2023-04-03 17:46:31 +02:00
|
|
|
# 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 (
|
2023-06-22 08:22:37 +02:00
|
|
|
sco_codes.BUT_CODES_ORDER[validation_rcue.code]
|
|
|
|
> sco_codes.BUT_CODES_ORDER[previous_validation["code"]]
|
2023-04-03 17:46:31 +02:00
|
|
|
):
|
|
|
|
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(
|
2023-04-04 12:02:09 +02:00
|
|
|
annee, [self.parcour] if self.parcour else None # XXX WIP
|
2023-04-03 17:46:31 +02:00
|
|
|
)[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 (
|
2023-06-22 08:22:37 +02:00
|
|
|
sco_codes.BUT_CODES_ORDER[validation_rcue.code]
|
|
|
|
> sco_codes.BUT_CODES_ORDER[previous_validation["code"]]
|
2023-04-03 17:46:31 +02:00
|
|
|
):
|
|
|
|
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 }"
|
|
|
|
|
|
|
|
|
2023-06-30 22:33:07 +02:00
|
|
|
def but_ects_valides(etud: Identite, referentiel_competence_id: int) -> float:
|
2023-07-01 08:21:40 +02:00
|
|
|
"""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)
|
2023-06-30 22:33:07 +02:00
|
|
|
.filter(ScolarFormSemestreValidation.ue_id != None)
|
|
|
|
.join(UniteEns)
|
|
|
|
.join(ApcNiveau)
|
|
|
|
.join(ApcCompetence)
|
|
|
|
.filter_by(referentiel_id=referentiel_competence_id)
|
2023-07-01 08:21:40 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
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
|
2023-06-30 22:33:07 +02:00
|
|
|
|
|
|
|
|
2023-06-15 21:53:05 +02:00
|
|
|
def etud_ues_de_but1_non_validees(
|
|
|
|
etud: Identite, formation: Formation, parcour: ApcParcours
|
|
|
|
) -> list[UniteEns]:
|
2023-06-30 14:23:05 +02:00
|
|
|
"""Liste des UEs de S1 et S2 non validées, dans son parcours"""
|
2023-06-15 21:53:05 +02:00
|
|
|
# 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)
|
|
|
|
)
|
2023-06-30 14:23:05 +02:00
|
|
|
codes_validations_by_ue_code = collections.defaultdict(list)
|
2023-06-15 21:53:05 +02:00
|
|
|
for v in validations:
|
2023-06-30 14:23:05 +02:00
|
|
|
codes_validations_by_ue_code[v.ue.ue_code].append(v.code)
|
2023-06-15 21:53:05 +02:00
|
|
|
|
|
|
|
# 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
|
2023-06-30 14:23:05 +02:00
|
|
|
if not any(
|
|
|
|
(
|
|
|
|
code_ue_validant(code)
|
|
|
|
for code in codes_validations_by_ue_code[ue.ue_code]
|
|
|
|
)
|
2023-06-15 21:53:05 +02:00
|
|
|
)
|
|
|
|
],
|
|
|
|
key=attrgetter("numero", "acronyme"),
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2023-04-03 17:46:31 +02:00
|
|
|
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">
|
2023-04-11 23:56:50 +02:00
|
|
|
La <a class="stdlink" href="{
|
2023-04-03 17:46:31 +02:00
|
|
|
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(
|
2023-10-19 22:24:56 +02:00
|
|
|
f"""Parcours {parcour.code if parcour else "Tronc commun"} :
|
2023-04-03 17:46:31 +02:00
|
|
|
{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>
|
2023-04-11 23:56:50 +02:00
|
|
|
<li>{ '</li><li>'.join(H) }</li>
|
2023-04-03 17:46:31 +02:00
|
|
|
</ul>
|
2023-10-19 22:24:56 +02:00
|
|
|
<p class="help">Vérifiez les parcours cochés pour ce semestre,
|
2023-04-11 23:56:50 +02:00
|
|
|
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>
|
2023-04-03 17:46:31 +02:00
|
|
|
</p>
|
|
|
|
</div>
|
|
|
|
"""
|
2023-07-16 19:59:45 +02:00
|
|
|
|
|
|
|
|
|
|
|
def ue_associee_au_niveau_du_parcours(
|
|
|
|
ues_possibles: list[UniteEns], niveau: ApcNiveau, sem_name: str = "S"
|
|
|
|
) -> UniteEns:
|
|
|
|
"L'UE associée à ce niveau, ou None"
|
|
|
|
ues = [ue for ue in ues_possibles if ue.niveau_competence_id == niveau.id]
|
|
|
|
if len(ues) > 1:
|
|
|
|
# plusieurs UEs associées à ce niveau: élimine celles sans parcours
|
|
|
|
ues_pair_avec_parcours = [ue for ue in ues if ue.parcours]
|
|
|
|
if ues_pair_avec_parcours:
|
|
|
|
ues = ues_pair_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
|
|
|
|
|
|
|
|
|
2023-10-19 22:24:56 +02:00
|
|
|
def parcour_formation_competences(
|
|
|
|
parcour: ApcParcours, formation: Formation
|
|
|
|
) -> tuple[list[dict], float]:
|
2023-07-16 19:59:45 +02:00
|
|
|
"""
|
|
|
|
[
|
|
|
|
{
|
|
|
|
'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],
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-10-19 22:24:56 +02:00
|
|
|
],
|
|
|
|
ects_parcours (somme des ects des UEs associées)
|
2023-07-16 19:59:45 +02:00
|
|
|
"""
|
|
|
|
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(
|
2023-10-19 22:24:56 +02:00
|
|
|
f"""_niveau_ues: plus d'un niveau pour {competence}
|
2023-07-16 19:59:45 +02:00
|
|
|
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": [],
|
|
|
|
}
|
|
|
|
# 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 = ue_associee_au_niveau_du_parcours(
|
|
|
|
ues_pair_possibles, niveau, f"S{2*annee}"
|
|
|
|
)
|
|
|
|
ue_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
|
|
|
|
],
|
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
|
|
|
)
|
|
|
|
]
|
2023-10-19 22:24:56 +02:00
|
|
|
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
|