forked from ScoDoc/ScoDoc
246 lines
9.6 KiB
Python
246 lines
9.6 KiB
Python
|
##############################################################################
|
|||
|
# ScoDoc
|
|||
|
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
|||
|
# See LICENSE
|
|||
|
##############################################################################
|
|||
|
|
|||
|
"""Jury BUT: logique de gestion
|
|||
|
"""
|
|||
|
from operator import attrgetter
|
|||
|
|
|||
|
from app.comp.res_but import ResultatsSemestreBUT
|
|||
|
from app.comp import res_sem
|
|||
|
from app.models import but_validations
|
|||
|
from app.models.but_refcomp import (
|
|||
|
ApcAnneeParcours,
|
|||
|
ApcCompetence,
|
|||
|
ApcNiveau,
|
|||
|
ApcParcoursNiveauCompetence,
|
|||
|
)
|
|||
|
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.scodoc import sco_codes_parcours as codes
|
|||
|
from app.scodoc import sco_utils as scu
|
|||
|
from app.scodoc.sco_exceptions import ScoException
|
|||
|
|
|||
|
|
|||
|
class RegroupementCoherentUE:
|
|||
|
def __init__(
|
|||
|
self,
|
|||
|
etud: Identite,
|
|||
|
formsemestre_1: FormSemestre,
|
|||
|
ue_1: UniteEns,
|
|||
|
formsemestre_2: FormSemestre,
|
|||
|
ue_2: UniteEns,
|
|||
|
):
|
|||
|
self.formsemestre_1 = formsemestre_1
|
|||
|
self.ue_1 = ue_1
|
|||
|
self.formsemestre_2 = formsemestre_2
|
|||
|
self.ue_2 = ue_2
|
|||
|
# stocke les moyennes d'UE
|
|||
|
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre_1)
|
|||
|
if ue_1.id in res.etud_moy_ue and etud.id in res.etud_moy_ue[ue_1.id]:
|
|||
|
self.moy_ue_1 = res.etud_moy_ue[ue_1.id][etud.id]
|
|||
|
else:
|
|||
|
self.moy_ue_1 = None
|
|||
|
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre_2)
|
|||
|
if ue_2.id in res.etud_moy_ue and etud.id in res.etud_moy_ue[ue_2.id]:
|
|||
|
self.moy_ue_2 = res.etud_moy_ue[ue_1.id][etud.id]
|
|||
|
else:
|
|||
|
self.moy_ue_2 = None
|
|||
|
# Calcul de la moyenne au RCUE
|
|||
|
if (self.moy_ue_1 is not None) and (self.moy_ue_2 is not None):
|
|||
|
# Moyenne RCUE non pondérée (pour le moment)
|
|||
|
self.moy_rcue = (self.moy_ue_1 + self.moy_ue_2) / 2
|
|||
|
else:
|
|||
|
self.moy_rcue = None
|
|||
|
|
|||
|
|
|||
|
class DecisionsProposees:
|
|||
|
# Codes toujours proposés sauf si include_communs est faux:
|
|||
|
codes_communs = [codes.RAT, codes.DEF, codes.ABAN, codes.DEM, codes.UEBSL]
|
|||
|
|
|||
|
def __init__(self, code: str = None, explanation="", include_communs=True):
|
|||
|
if include_communs:
|
|||
|
self.codes = self.codes_communs
|
|||
|
else:
|
|||
|
self.codes = []
|
|||
|
if isinstance(code, list):
|
|||
|
self.codes = code + self.codes_communs
|
|||
|
elif code is not None:
|
|||
|
self.codes = [code] + self.codes_communs
|
|||
|
self.explanation = explanation
|
|||
|
|
|||
|
def __repr__(self) -> str:
|
|||
|
return f"""<{self.__class__.__name__} codes={self.codes} explanation={self.explanation}"""
|
|||
|
|
|||
|
|
|||
|
def decisions_ue_proposees(
|
|||
|
etud: Identite, formsemestre: FormSemestre, ue: UniteEns
|
|||
|
) -> DecisionsProposees:
|
|||
|
"""Liste des codes de décisions que l'on peut proposer pour
|
|||
|
cette UE de cet étudiant dans ce semestre.
|
|||
|
|
|||
|
si DEF ou DEM ou ABAN ou ABL sur année BUT: seulement DEF, DEM, ABAN, ABL
|
|||
|
|
|||
|
si moy_ue > 10, ADM
|
|||
|
sinon si compensation dans RCUE: CMP
|
|||
|
sinon: ADJ, AJ
|
|||
|
et proposer toujours: RAT, DEF, ABAN, DEM, UEBSL
|
|||
|
"""
|
|||
|
if ue.type == codes.UE_SPORT:
|
|||
|
return DecisionsProposees(
|
|||
|
explanation="UE bonus, pas de décision de jury", include_communs=False
|
|||
|
)
|
|||
|
# Code sur année ?
|
|||
|
decision_annee = ApcValidationAnnee.query.filter_by(
|
|||
|
etudid=etud.id, annee_scolaire=formsemestre.annee_scolaire()
|
|||
|
).first()
|
|||
|
if (
|
|||
|
decision_annee is not None and decision_annee.code in codes.CODES_ANNEE_ARRET
|
|||
|
): # DEF, DEM, ABAN, ABL
|
|||
|
return DecisionsProposees(
|
|||
|
code=decision_annee.code,
|
|||
|
explanation=f"l'année a le code {decision_annee.code}",
|
|||
|
include_communs=False,
|
|||
|
)
|
|||
|
# Moyenne de l'UE ?
|
|||
|
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre)
|
|||
|
if not ue.id in res.etud_moy_ue:
|
|||
|
return DecisionsProposees(explanation="UE sans résultat")
|
|||
|
if not etud.id in res.etud_moy_ue[ue.id]:
|
|||
|
return DecisionsProposees(explanation="Étudiant sans résultat dans cette UE")
|
|||
|
moy_ue = res.etud_moy_ue[ue.id][etud.id]
|
|||
|
if moy_ue > (codes.ParcoursBUT.BARRE_MOY - codes.NOTES_TOLERANCE):
|
|||
|
return DecisionsProposees(
|
|||
|
code=codes.ADM,
|
|||
|
explanation=f"Moyenne >= {codes.ParcoursBUT.BARRE_MOY}/20",
|
|||
|
)
|
|||
|
# Compensation dans le RCUE ?
|
|||
|
other_ue, other_formsemestre = but_validations.get_other_ue_rcue(ue, etud.id)
|
|||
|
if other_ue is not None:
|
|||
|
# inscrit à une autre UE du même RCUE
|
|||
|
other_res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(
|
|||
|
other_formsemestre
|
|||
|
)
|
|||
|
if (other_ue.id in other_res.etud_moy_ue) and (
|
|||
|
etud.id in other_res.etud_moy_ue[other_ue.id]
|
|||
|
):
|
|||
|
other_moy_ue = other_res.etud_moy_ue[other_ue.id][etud.id]
|
|||
|
# Moyenne RCUE: non pondérée (pour le moment)
|
|||
|
moy_rcue = (moy_ue + other_moy_ue) / 2
|
|||
|
if moy_rcue > codes.NOTES_BARRE_GEN_COMPENSATION: # 10-epsilon
|
|||
|
return DecisionsProposees(
|
|||
|
code=codes.CMP,
|
|||
|
explanation=f"Compensée par {other_ue} (moyenne RCUE={scu.fmt_note(moy_rcue)}/20",
|
|||
|
)
|
|||
|
return DecisionsProposees(
|
|||
|
code=[codes.AJ, codes.ADJ],
|
|||
|
explanation="notes insuffisantes",
|
|||
|
)
|
|||
|
|
|||
|
|
|||
|
def decisions_rcue_proposees(
|
|||
|
etud: Identite,
|
|||
|
formsemestre_1: FormSemestre,
|
|||
|
ue_1: UniteEns,
|
|||
|
formsemestre_2: FormSemestre,
|
|||
|
ue_2: UniteEns,
|
|||
|
) -> DecisionsProposees:
|
|||
|
"""Liste des codes de décisions que l'on peut proposer pour
|
|||
|
le RCUE de cet étudiant dans ces semestres.
|
|||
|
|
|||
|
ADM, CMP, ADJ, AJ, RAT, DEF, ABAN
|
|||
|
|
|||
|
La validation des deux UE du niveau d’une compétence emporte la validation de
|
|||
|
l’ensemble des UE du niveau inférieur de cette même compétence.
|
|||
|
"""
|
|||
|
#
|
|||
|
|
|||
|
|
|||
|
class BUTCursusEtud:
|
|||
|
"""Validation du cursus d'un étudiant"""
|
|||
|
|
|||
|
def __init__(self, formsemestre: FormSemestre, etud: Identite):
|
|||
|
if formsemestre.formation.referentiel_competence is None:
|
|||
|
raise ScoException("BUTCursusEtud: pas de référentiel de compétences")
|
|||
|
assert len(etud.formsemestre_inscriptions) > 0
|
|||
|
self.formsemestre = formsemestre
|
|||
|
self.etud = etud
|
|||
|
#
|
|||
|
# La dernière inscription en date va donner le parcours (donc les compétences à valider)
|
|||
|
self.last_inscription = sorted(
|
|||
|
etud.formsemestre_inscriptions, key=attrgetter("formsemestre.date_debut")
|
|||
|
)[-1]
|
|||
|
|
|||
|
def est_diplomable(self) -> bool:
|
|||
|
"""Vrai si toutes les compétences sont validables"""
|
|||
|
return all(
|
|||
|
self.competence_validable(competence)
|
|||
|
for competence in self.competences_du_parcours()
|
|||
|
)
|
|||
|
|
|||
|
def est_diplome(self) -> bool:
|
|||
|
"""Vrai si BUT déjà validé"""
|
|||
|
# vrai si la troisième année est validée
|
|||
|
# On cherche les validations de 3ieme annee (ordre=3) avec le même référentiel
|
|||
|
# de formation que nous.
|
|||
|
return (
|
|||
|
ApcValidationAnnee.query.filter_by(etudid=self.etud.id, ordre=3)
|
|||
|
.join(FormSemestre, FormSemestre.id == ApcValidationAnnee.formsemestre_id)
|
|||
|
.join(Formation, FormSemestre.formation_id == Formation.id)
|
|||
|
.filter(
|
|||
|
Formation.referentiel_competence_id
|
|||
|
== self.formsemestre.formation.referentiel_competence_id
|
|||
|
)
|
|||
|
.count()
|
|||
|
> 0
|
|||
|
)
|
|||
|
|
|||
|
def competences_du_parcours(self) -> list[ApcCompetence]:
|
|||
|
"""Construit liste des compétences du parcours, qui doivent être
|
|||
|
validées pour obtenir le diplôme.
|
|||
|
Le parcours est celui de la dernière inscription.
|
|||
|
"""
|
|||
|
parcour = self.last_inscription.parcour
|
|||
|
query = self.formsemestre.formation.formation.query_competences_parcour(parcour)
|
|||
|
if query is None:
|
|||
|
return []
|
|||
|
return query.all()
|
|||
|
|
|||
|
def competence_validee(self, competence: ApcCompetence) -> bool:
|
|||
|
"""Vrai si la compétence est validée, c'est à dire que tous ses
|
|||
|
niveaux sont validés (ApcValidationRCUE).
|
|||
|
"""
|
|||
|
validations = (
|
|||
|
ApcValidationRCUE.query.filter_by(etudid=self.etud.id)
|
|||
|
.join(UniteEns, UniteEns.id == ApcValidationRCUE.ue1_id)
|
|||
|
.join(ApcNiveau, ApcNiveau.id == UniteEns.niveau_competence_id)
|
|||
|
.join(ApcCompetence, ApcCompetence.id == ApcNiveau.competence_id)
|
|||
|
)
|
|||
|
|
|||
|
def competence_validable(self, competence: ApcCompetence):
|
|||
|
"""Vrai si la compétence est "validable" automatiquement, c'est à dire
|
|||
|
que les conditions de notes sont satisfaites pour l'acquisition de
|
|||
|
son niveau le plus élevé, qu'il ne manque que l'enregistrement de la décision.
|
|||
|
|
|||
|
En vertu de la règle "La validation des deux UE du niveau d’une compétence
|
|||
|
emporte la validation de l'ensemble des UE du niveau inférieur de cette
|
|||
|
même compétence.",
|
|||
|
il suffit de considérer le dernier niveau dans lequel l'étudiant est inscrit.
|
|||
|
"""
|
|||
|
pass
|
|||
|
|
|||
|
def ues_emportees(self, niveau: ApcNiveau) -> list[tuple[FormSemestre, UniteEns]]:
|
|||
|
"""La liste des UE à valider si on valide ce niveau.
|
|||
|
Ne liste que les UE qui ne sont pas déjà acquises.
|
|||
|
|
|||
|
Selon la règle donéne par l'arrêté BUT:
|
|||
|
* La validation des deux UE du niveau d’une compétence emporte la validation de
|
|||
|
l'ensemble des UE du niveau inférieur de cette même compétence.
|
|||
|
"""
|
|||
|
pass
|