forked from ScoDoc/ScoDoc
WIP: jurys BUT: décisions possibles sur année
This commit is contained in:
parent
a3cccac2d2
commit
a709e9d6e9
@ -5,163 +5,463 @@
|
||||
##############################################################################
|
||||
|
||||
"""Jury BUT: logique de gestion
|
||||
|
||||
Utilisation:
|
||||
1) chargement page jury, pour un étudiant et un formsemestre BUT quelconque
|
||||
- DecisionsProposeesAnnee(formsemestre)
|
||||
cherche l'autre formsemestre de la même année scolaire (peut ne pas exister)
|
||||
cherche les RCUEs de l'année (BUT1, 2, 3)
|
||||
pour un redoublant, le RCUE peut considérer un formsemestre d'une année antérieure.
|
||||
|
||||
on instancie des DecisionsProposees pour les
|
||||
différents éléments (UEs, RCUEs, Année, Diplôme)
|
||||
Cela donne
|
||||
- les codes possibles (dans .codes)
|
||||
- le code actuel si une décision existe déjà (dans code_valide)
|
||||
- pour les UEs, le rcue s'il y en a un)
|
||||
|
||||
2) Validation pour l'utilisateur (form)) => enregistrement code
|
||||
- on vérifie que le code soumis est bien dans les codes possibles
|
||||
- on enregistre la décision (dans ScolarFormSemestreValidation pour les UE,
|
||||
ApcValidationRCUE pour les RCUE, et ApcValidationAnnee pour les années)
|
||||
- Si RCUE validé, on déclenche d'éventuelles validations:
|
||||
("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.")
|
||||
|
||||
Les jurys de semestre BUT impairs entrainent systématiquement la génération d'une
|
||||
autorisation d'inscription dans le semestre pair suivant: `ScolarAutorisationInscription`.
|
||||
Les jurys de semestres pairs non (S2, S4, S6): il y a une décision sur l'année (ETP)
|
||||
- autorisation en S_2n+1 (S3 ou S5) si: ADM, ADJ, PASD, PAS1CN
|
||||
- autorisation en S2n-1 (S1, S3 ou S5) si: RED
|
||||
- rien si pour les autres codes d'année.
|
||||
|
||||
Le formulaire permet de choisir des codes d'UE, RCUE et Année (ETP).
|
||||
Mais normalement, les codes d'UE sont à choisir: les RCUE et l'année s'en déduisent.
|
||||
Si l'utilisateur coche "décision manuelle", il peut alors choisir les codes RCUE et années.
|
||||
|
||||
La soumission du formulaire:
|
||||
- etud, formation
|
||||
- UEs: [(formsemestre, ue, code), ...]
|
||||
- RCUE: [(formsemestre, ue, code), ...] le formsemestre est celui d'indice pair du niveau
|
||||
(S2, S4 ou S6), il sera regoupé avec celui impair de la même année ou de la suivante.
|
||||
- Année: [(formsemestre, code)]
|
||||
|
||||
DecisionsProposeesAnnee:
|
||||
si 1/2 des rcue et aucun < 8 + pour S5 condition sur les UE de BUT1 et BUT2
|
||||
=> charger les DecisionsProposeesRCUE
|
||||
|
||||
DecisionsProposeesRCUE: les RCUEs pour cette année
|
||||
validable, compensable, ajourné. Utilise classe RegroupementCoherentUE
|
||||
|
||||
DecisionsProposeesUE: décisions de jury sur une UE du BUT
|
||||
initialisation sans compensation (ue isolée), mais
|
||||
DecisionsProposeesRCUE appelera .set_compensable()
|
||||
si on a la possibilité de la compenser dans le RCUE.
|
||||
"""
|
||||
from operator import attrgetter
|
||||
from typing import Union
|
||||
|
||||
from app import log
|
||||
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,
|
||||
ApcParcours,
|
||||
ApcParcoursNiveauCompetence,
|
||||
)
|
||||
from app.models.but_validations import ApcValidationAnnee, ApcValidationRCUE
|
||||
from app.models import but_validations
|
||||
from app.models.but_validations import (
|
||||
ApcValidationAnnee,
|
||||
ApcValidationRCUE,
|
||||
RegroupementCoherentUE,
|
||||
)
|
||||
from app.models.etudiants import Identite
|
||||
from app.models.formations import Formation
|
||||
from app.models.formsemestre import FormSemestre
|
||||
from app.models.formsemestre import FormSemestre, FormSemestreInscription
|
||||
from app.models.ues import UniteEns
|
||||
from app.scodoc import sco_codes_parcours as codes
|
||||
from app.models.validations import ScolarFormSemestreValidation
|
||||
from app.scodoc import sco_codes_parcours as sco_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
|
||||
from app.scodoc.sco_exceptions import ScoException, ScoValueError
|
||||
|
||||
|
||||
class DecisionsProposees:
|
||||
# Codes toujours proposés sauf si include_communs est faux:
|
||||
codes_communs = [codes.RAT, codes.DEF, codes.ABAN, codes.DEM, codes.UEBSL]
|
||||
"""Une décision de jury proposé, constituée d'une liste de codes et d'une explication.
|
||||
Super-classe, spécialisée pour les UE, les RCUE, les années et le diplôme.
|
||||
|
||||
def __init__(self, code: str = None, explanation="", include_communs=True):
|
||||
validation : None ou une instance de d'une classe avec un champ code
|
||||
ApcValidationRCUE, ApcValidationAnnee ou ScolarFormSemestreValidation
|
||||
"""
|
||||
|
||||
# Codes toujours proposés sauf si include_communs est faux:
|
||||
codes_communs = [
|
||||
sco_codes.RAT,
|
||||
sco_codes.DEF,
|
||||
sco_codes.ABAN,
|
||||
sco_codes.DEM,
|
||||
sco_codes.UEBSL,
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
etud: Identite = None,
|
||||
code: Union[str, list[str]] = None,
|
||||
explanation="",
|
||||
code_valide=None,
|
||||
include_communs=True,
|
||||
):
|
||||
self.etud = etud
|
||||
self.codes = []
|
||||
"Les codes attribuables par ce jury"
|
||||
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
|
||||
self.code_valide: str = code_valide
|
||||
"La décision actuelle enregistrée"
|
||||
self.explanation: str = explanation
|
||||
"Explication en à afficher à côté de la décision"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"""<{self.__class__.__name__} codes={self.codes} explanation={self.explanation}"""
|
||||
return f"""<{self.__class__.__name__} valid={self.code_valide
|
||||
} codes={self.codes} explanation={self.explanation}"""
|
||||
|
||||
|
||||
def decisions_ue_proposees(
|
||||
etud: Identite, formsemestre: FormSemestre, ue: UniteEns
|
||||
) -> DecisionsProposees:
|
||||
class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
"""Décisions de jury sur une année (ETP) du BUT
|
||||
|
||||
Le texte:
|
||||
La poursuite d'études dans un semestre pair d’une même année est de droit
|
||||
pour tout étudiant. La poursuite d’études dans un semestre impair est
|
||||
possible si et seulement si l’étudiant a obtenu :
|
||||
- la moyenne à plus de la moitié des regroupements cohérents d’UE;
|
||||
- et une moyenne égale ou supérieure à 8 sur 20 à chaque RCUE.
|
||||
La poursuite d'études dans le semestre 5 nécessite de plus la validation
|
||||
de toutes les UE des semestres 1 et 2 dans les conditions de validation
|
||||
des points 4.3 (moy_ue >= 10) et 4.4 (compensation rcue), ou par décision
|
||||
de jury.
|
||||
"""
|
||||
|
||||
# Codes toujours proposés sauf si include_communs est faux:
|
||||
codes_communs = [
|
||||
sco_codes.RAT,
|
||||
sco_codes.ABAN,
|
||||
sco_codes.ABL,
|
||||
sco_codes.DEF,
|
||||
sco_codes.DEM,
|
||||
sco_codes.EXCLU,
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
etud: Identite,
|
||||
formsemestre: FormSemestre,
|
||||
):
|
||||
super().__init__(etud=etud)
|
||||
formsemestre_impair, formsemestre_pair = self.comp_formsemestres(formsemestre)
|
||||
assert (
|
||||
(formsemestre_pair is None)
|
||||
or (formsemestre_impair is None)
|
||||
or (
|
||||
((formsemestre_pair.semestre_id - formsemestre_impair.semestre_id) == 1)
|
||||
and (
|
||||
formsemestre_pair.formation.referentiel_competence_id
|
||||
== formsemestre_impair.formation.referentiel_competence_id
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
self.formsemestre_impair = formsemestre_impair
|
||||
"le 1er semestre de l'année scolaire considérée (S1, S3, S5)"
|
||||
self.formsemestre_pair = formsemestre_pair
|
||||
"le second formsemestre de la même année scolaire (S2, S4, S6)"
|
||||
self.annee_but = formsemestre_impair.semestre_id // 2 + 1
|
||||
"le rang de l'année dans le BUT: 1, 2, 3"
|
||||
assert self.annee_but in (1, 2, 3)
|
||||
self.validation = ApcValidationAnnee.query.filter_by(
|
||||
etudid=self.etud.id,
|
||||
formsemestre_id=formsemestre_impair.id,
|
||||
ordre=self.annee_but,
|
||||
).first()
|
||||
if self.validation is not None:
|
||||
self.code_valide = self.validation.code
|
||||
self.parcour = None
|
||||
"Le parcours considéré (celui du semestre pair, ou à défaut impair)"
|
||||
self.ues_impair, self.ues_pair = self.compute_ues_annee() # pylint: disable=all
|
||||
assert self.parcour is not None
|
||||
self.rcues_annee = self.compute_rcues_annee()
|
||||
"RCUEs de l'année"
|
||||
|
||||
self.nb_competences = len(
|
||||
ApcNiveau.niveaux_annee_de_parcours(self.parcour, self.annee_but).all()
|
||||
) # note that .count() won't give the same res
|
||||
self.nb_validables = len(
|
||||
[rcue for rcue in self.rcues_annee if rcue.est_validable()]
|
||||
)
|
||||
self.nb_rcues_under_8 = len(
|
||||
[rcue for rcue in self.rcues_annee if not rcue.est_suffisant()]
|
||||
)
|
||||
# année ADM si toutes RCUE validées (sinon PASD)
|
||||
admis = self.nb_validables == self.nb_competences
|
||||
valide_moitie_rcue = self.nb_validables > self.nb_competences // 2
|
||||
# Peut passer si plus de la moitié validables et tous > 8
|
||||
passage_de_droit = valide_moitie_rcue and (self.nb_rcues_under_8 == 0)
|
||||
# XXX TODO ajouter condition pour passage en S5
|
||||
|
||||
# Reste à attribuer ADM, ADJ, PASD, PAS1NCI, RED, NAR
|
||||
expl_rcues = f"{self.nb_validables} validables sur {self.nb_competences}"
|
||||
if admis:
|
||||
self.codes = [sco_codes.ADM] + self.codes
|
||||
self.explanation = expl_rcues
|
||||
elif passage_de_droit:
|
||||
self.codes = [sco_codes.PASD, sco_codes.ADJ] + self.codes
|
||||
self.explanation = expl_rcues
|
||||
elif valide_moitie_rcue: # mais au moins 1 rcue insuffisante
|
||||
self.codes = [sco_codes.PAS1NCI, sco_codes.ADJ] + self.codes
|
||||
self.explanation = expl_rcues + f" et {self.nb_rcues_under_8} < 8"
|
||||
else:
|
||||
self.codes = [sco_codes.RED, sco_codes.NAR, sco_codes.ADJ] + self.codes
|
||||
self.explanation = expl_rcues + f" et {self.nb_rcues_under_8} < 8"
|
||||
#
|
||||
|
||||
def infos(self) -> str:
|
||||
"informations, for debugging purpose"
|
||||
return f"""DecisionsProposeesAnnee
|
||||
etud: {self.etud}
|
||||
formsemestre_pair: {self.formsemestre_pair}
|
||||
formsemestre_impair: {self.formsemestre_impair}
|
||||
RCUEs: {self.rcues_annee}
|
||||
nb_competences: {self.nb_competences}
|
||||
nb_nb_validables: {self.nb_validables}
|
||||
codes: {self.codes}
|
||||
explanation: {self.explanation}
|
||||
"""
|
||||
|
||||
def comp_formsemestres(
|
||||
self, formsemestre: FormSemestre
|
||||
) -> tuple[FormSemestre, FormSemestre]:
|
||||
"les deux formsemestres de l'année scolaire à laquelle appartient formsemestre"
|
||||
if formsemestre.semestre_id % 2 == 0:
|
||||
other_semestre_id = formsemestre.semestre_id - 1
|
||||
else:
|
||||
other_semestre_id = formsemestre.semestre_id + 1
|
||||
annee_scolaire = formsemestre.annee_scolaire()
|
||||
other_formsemestre = None
|
||||
for inscr in self.etud.formsemestre_inscriptions:
|
||||
if (
|
||||
# Même spécialité BUT (tolère ainsi des variantes de formation)
|
||||
(
|
||||
inscr.formsemestre.formation.referentiel_competence
|
||||
== formsemestre.formation.referentiel_competence
|
||||
)
|
||||
# L'autre semestre
|
||||
and (inscr.formsemestre.semestre_id == other_semestre_id)
|
||||
# de la même année scolaire:
|
||||
and (inscr.formsemestre.annee_scolaire() == annee_scolaire)
|
||||
):
|
||||
other_formsemestre = inscr.formsemestre
|
||||
if formsemestre.semestre_id % 2 == 0:
|
||||
return other_formsemestre, formsemestre
|
||||
return formsemestre, other_formsemestre
|
||||
|
||||
def compute_ues_annee(self) -> list[list[UniteEns], list[UniteEns]]:
|
||||
"""UEs à valider cette année pour cet étudiant, selon son parcours.
|
||||
Ramène [ listes des UE du semestre impair, liste des UE du semestre pair ].
|
||||
"""
|
||||
etudid = self.etud.id
|
||||
ues_sems = []
|
||||
for formsemestre in self.formsemestre_impair, self.formsemestre_pair:
|
||||
if formsemestre is None:
|
||||
ues = []
|
||||
else:
|
||||
formation: Formation = formsemestre.formation
|
||||
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(
|
||||
formsemestre
|
||||
)
|
||||
# Parcour dans lequel l'étudiant est inscrit, et liste des UEs
|
||||
if res.etuds_parcour_id[etudid] is None:
|
||||
# pas de parcour: prend toutes les UEs (non bonus)
|
||||
ues = res.etud_ues(etudid)
|
||||
else:
|
||||
parcour = ApcParcours.query.get(res.etuds_parcour_id[etudid])
|
||||
if parcour is not None:
|
||||
self.parcour = parcour
|
||||
ues = (
|
||||
formation.query_ues_parcour(parcour)
|
||||
.filter_by(semestre_idx=formsemestre.semestre_id)
|
||||
.all()
|
||||
)
|
||||
ues_sems.append(ues)
|
||||
return ues_sems
|
||||
|
||||
def check_ues_ready_jury(self) -> list[str]:
|
||||
"""Vérifie que les toutes les UEs (hors bonus) de l'année sont
|
||||
bien associées à des niveaux de compétences.
|
||||
Renvoie liste vide si ok, sinon liste de message explicatifs
|
||||
"""
|
||||
messages = []
|
||||
for ue in self.ues_impair + self.ues_pair:
|
||||
if ue.niveau_competence is None:
|
||||
messages.append(
|
||||
f"UE {ue.acronyme} non associée à un niveau de compétence"
|
||||
)
|
||||
if ue.semestre_idx is None:
|
||||
messages.append(
|
||||
f"UE {ue.acronyme} n'a pas d'indice de semestre dans la formation"
|
||||
)
|
||||
return messages
|
||||
|
||||
def compute_rcues_annee(self) -> list[RegroupementCoherentUE]:
|
||||
"""Liste des regroupements d'UE à considérer cette année.
|
||||
Pour le moment on ne considère pas de RCUE à cheval sur plusieurs années (redoublants).
|
||||
Si on n'a pas les deux semestres, aucun RCUE.
|
||||
Raises ScoValueError s'il y a des UE sans RCUE.
|
||||
"""
|
||||
if self.formsemestre_pair is None or self.formsemestre_impair is None:
|
||||
return []
|
||||
rcues_annee = []
|
||||
ues_impair_sans_rcue = {ue.id for ue in self.ues_impair}
|
||||
for ue_pair in self.ues_pair:
|
||||
rcue = None
|
||||
for ue_impair in self.ues_impair:
|
||||
if ue_pair.niveau_competence_id == ue_impair.niveau_competence_id:
|
||||
rcue = RegroupementCoherentUE(
|
||||
self.etud,
|
||||
self.formsemestre_impair,
|
||||
ue_impair,
|
||||
self.formsemestre_pair,
|
||||
ue_pair,
|
||||
)
|
||||
ues_impair_sans_rcue.remove(ue_impair.id)
|
||||
break
|
||||
if rcue is None:
|
||||
raise ScoValueError(f"pas de RCUE pour l'UE {ue_pair.acronyme}")
|
||||
rcues_annee.append(rcue)
|
||||
if len(ues_impair_sans_rcue) > 0:
|
||||
ue = ues_impair_sans_rcue.pop()
|
||||
raise ScoValueError(f"pas de RCUE pour l'UE {ue.acronyme}")
|
||||
return rcues_annee
|
||||
|
||||
|
||||
class DecisionsProposeesRCUE(DecisionsProposees):
|
||||
"""Liste des codes de décisions que l'on peut proposer pour
|
||||
cette UE de cet étudiant dans ce semestre.
|
||||
le RCUE de cet étudiant dans cette année.
|
||||
|
||||
si DEF ou DEM ou ABAN ou ABL sur année BUT: seulement DEF, DEM, ABAN, ABL
|
||||
ADM, CMP, ADJ, AJ, RAT, DEF, ABAN
|
||||
"""
|
||||
|
||||
codes_communs = [
|
||||
sco_codes.ADJ,
|
||||
sco_codes.RAT,
|
||||
sco_codes.DEF,
|
||||
sco_codes.ABAN,
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self, dec_prop_annee: DecisionsProposeesAnnee, rcue: RegroupementCoherentUE
|
||||
):
|
||||
super().__init__(etud=dec_prop_annee.etud)
|
||||
self.rcue = rcue
|
||||
|
||||
validation = rcue.query_validations().first()
|
||||
if validation is not None:
|
||||
self.code_valide = validation.code
|
||||
if rcue.est_compense():
|
||||
self.codes.insert(0, sco_codes.CMP)
|
||||
elif rcue.est_validable():
|
||||
self.codes.insert(0, sco_codes.ADM)
|
||||
else:
|
||||
self.codes.insert(0, sco_codes.AJ)
|
||||
|
||||
|
||||
class DecisionsProposeesUE(DecisionsProposees):
|
||||
"""Décisions de jury sur une UE du BUT
|
||||
|
||||
Liste des codes de décisions que l'on peut proposer pour
|
||||
cette UE d'un étudiant dans un 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
|
||||
|
||||
et proposer toujours: RAT, DEF, ABAN, DEM, UEBSL (codes_communs)
|
||||
"""
|
||||
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",
|
||||
)
|
||||
|
||||
# Codes toujours proposés sauf si include_communs est faux:
|
||||
codes_communs = [
|
||||
sco_codes.RAT,
|
||||
sco_codes.DEF,
|
||||
sco_codes.ABAN,
|
||||
sco_codes.DEM,
|
||||
sco_codes.UEBSL,
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
etud: Identite,
|
||||
formsemestre: FormSemestre,
|
||||
ue: UniteEns,
|
||||
):
|
||||
super().__init__(etud=etud)
|
||||
self.ue: UniteEns = ue
|
||||
self.rcue: RegroupementCoherentUE = None
|
||||
# Une UE peut être validée plusieurs fois en cas de redoublement (qu'elle soit capitalisée ou non)
|
||||
# mais ici on a restreint au formsemestre donc une seule (prend la première)
|
||||
self.validation = ScolarFormSemestreValidation.query.filter_by(
|
||||
etudid=self.etud.id, formsemestre_id=formsemestre.id, ue_id=ue.id
|
||||
).first()
|
||||
if self.validation is not None:
|
||||
self.code_valide = self.validation.code
|
||||
if ue.type == sco_codes.UE_SPORT:
|
||||
self.explanation = "UE bonus, pas de décision de jury"
|
||||
self.codes = [] # aucun code proposé
|
||||
return
|
||||
# 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 sco_codes.CODES_ANNEE_ARRET
|
||||
): # DEF, DEM, ABAN, ABL
|
||||
self.explanation = f"l'année a le code {decision_annee.code}"
|
||||
self.codes = [decision_annee.code] # sans les codes communs
|
||||
return
|
||||
# Moyenne de l'UE ?
|
||||
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre)
|
||||
if not ue.id in res.etud_moy_ue:
|
||||
self.explanation = "UE sans résultat"
|
||||
return
|
||||
if not etud.id in res.etud_moy_ue[ue.id]:
|
||||
self.explanation = "Étudiant sans résultat dans cette UE"
|
||||
return
|
||||
moy_ue = res.etud_moy_ue[ue.id][etud.id]
|
||||
if moy_ue > (sco_codes.ParcoursBUT.BARRE_MOY - sco_codes.NOTES_TOLERANCE):
|
||||
self.codes.insert(0, sco_codes.ADM)
|
||||
self.explanation = (f"Moyenne >= {sco_codes.ParcoursBUT.BARRE_MOY}/20",)
|
||||
|
||||
# Compensation dans un RCUE ?
|
||||
rcues = but_validations.find_rcues(formsemestre, ue, etud)
|
||||
for rcue in rcues:
|
||||
if rcue.est_validable():
|
||||
self.codes.insert(0, sco_codes.CMP)
|
||||
self.explanation = f"Compensée par {rcue.other_ue(ue)} (moyenne RCUE={scu.fmt_note(rcue.moy_rcue)}/20"
|
||||
self.rcue = rcue
|
||||
return # s'arrête au 1er RCU validable
|
||||
|
||||
# Échec à valider cette UE
|
||||
self.codes = [sco_codes.AJ, sco_codes.ADJ] + self.codes
|
||||
self.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:
|
||||
class BUTCursusEtud: # WIP TODO
|
||||
"""Validation du cursus d'un étudiant"""
|
||||
|
||||
def __init__(self, formsemestre: FormSemestre, etud: Identite):
|
||||
@ -215,6 +515,7 @@ class BUTCursusEtud:
|
||||
"""Vrai si la compétence est validée, c'est à dire que tous ses
|
||||
niveaux sont validés (ApcValidationRCUE).
|
||||
"""
|
||||
# XXX A REVOIR
|
||||
validations = (
|
||||
ApcValidationRCUE.query.filter_by(etudid=self.etud.id)
|
||||
.join(UniteEns, UniteEns.id == ApcValidationRCUE.ue1_id)
|
||||
|
@ -29,6 +29,7 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
||||
"modimpl_coefs_df",
|
||||
"modimpls_evals_poids",
|
||||
"sem_cube",
|
||||
"etuds_parcour_id", # parcours de chaque étudiant
|
||||
"ues_inscr_parcours_df", # inscriptions aux UE / parcours
|
||||
)
|
||||
|
||||
@ -37,7 +38,8 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
||||
|
||||
self.sem_cube = None
|
||||
"""ndarray (etuds x modimpl x ue)"""
|
||||
|
||||
self.etuds_parcour_id = None
|
||||
"""Parcours de chaque étudiant { etudid : parcour_id }"""
|
||||
if not self.load_cached():
|
||||
t0 = time.time()
|
||||
self.compute()
|
||||
@ -190,13 +192,14 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
||||
La matrice avec ue ne comprend que les UE non bonus.
|
||||
1.0 si étudiant inscrit à l'UE, NaN sinon.
|
||||
"""
|
||||
etuds_parcours = {
|
||||
etuds_parcour_id = {
|
||||
inscr.etudid: inscr.parcour_id for inscr in self.formsemestre.inscriptions
|
||||
}
|
||||
self.etuds_parcour_id = etuds_parcour_id
|
||||
ue_ids = [ue.id for ue in self.ues]
|
||||
# matrice de 1, inscrits par défaut à toutes les UE:
|
||||
ues_inscr_parcours_df = pd.DataFrame(
|
||||
1.0, index=etuds_parcours.keys(), columns=ue_ids, dtype=float
|
||||
1.0, index=etuds_parcour_id.keys(), columns=ue_ids, dtype=float
|
||||
)
|
||||
if self.formsemestre.formation.referentiel_competence is None:
|
||||
return ues_inscr_parcours_df
|
||||
@ -209,11 +212,11 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
||||
parcour
|
||||
).filter_by(semestre_idx=self.formsemestre.semestre_id)
|
||||
}
|
||||
for etudid in etuds_parcours:
|
||||
parcour = etuds_parcours[etudid]
|
||||
for etudid in etuds_parcour_id:
|
||||
parcour = etuds_parcour_id[etudid]
|
||||
if parcour is not None:
|
||||
ues_inscr_parcours_df.loc[etudid] = ue_by_parcours[
|
||||
etuds_parcours[etudid]
|
||||
etuds_parcour_id[etudid]
|
||||
]
|
||||
return ues_inscr_parcours_df
|
||||
|
||||
|
@ -3,13 +3,18 @@
|
||||
"""Décisions de jury (validations) des RCUE et années du BUT
|
||||
"""
|
||||
|
||||
import flask_sqlalchemy
|
||||
from sqlalchemy.sql import text
|
||||
from typing import Union
|
||||
|
||||
from app import db
|
||||
|
||||
from app.models import CODE_STR_LEN
|
||||
from app.models.but_refcomp import ApcNiveau
|
||||
from app.models.etudiants import Identite
|
||||
from app.models.ues import UniteEns
|
||||
from app.models.formsemestre import FormSemestre
|
||||
from app.scodoc import sco_codes_parcours as sco_codes
|
||||
|
||||
|
||||
class ApcValidationRCUE(db.Model):
|
||||
@ -17,6 +22,7 @@ class ApcValidationRCUE(db.Model):
|
||||
|
||||
aka "regroupements cohérents d'UE" dans le jargon BUT.
|
||||
|
||||
le formsemestre est celui du semestre PAIR du niveau de compétence
|
||||
"""
|
||||
|
||||
__tablename__ = "apc_validation_rcue"
|
||||
@ -58,16 +64,151 @@ class ApcValidationRCUE(db.Model):
|
||||
return self.ue2.niveau_competence
|
||||
|
||||
|
||||
def get_other_ue_rcue(ue: UniteEns, etudid: int) -> tuple[UniteEns, FormSemestre]:
|
||||
"""L'autre UE du RCUE (niveau de compétence) pour cet étudiant.
|
||||
# Attention: ce n'est pas un modèle mais une classe ordinaire:
|
||||
class RegroupementCoherentUE:
|
||||
"""Le regroupement cohérent d'UE, dans la terminologie du BUT, est le couple d'UEs
|
||||
de la même année (BUT1,2,3) liées au même niveau de compétence.
|
||||
|
||||
Cherche une UE du même niveau de compétence, à laquelle l'étudiant soit inscrit.
|
||||
Résultat: le couple (UE, FormSemestre), ou (None, None) si pas trouvée.
|
||||
La moyenne (10/20) au RCU déclenche la compensation des UE.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
etud: Identite,
|
||||
formsemestre_1: FormSemestre,
|
||||
ue_1: UniteEns,
|
||||
formsemestre_2: FormSemestre,
|
||||
ue_2: UniteEns,
|
||||
):
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_but import ResultatsSemestreBUT
|
||||
|
||||
# Ordonne les UE dans le sens croissant (S1,S2) ou (S3,S4)...
|
||||
if formsemestre_1.semestre_id > formsemestre_2.semestre_id:
|
||||
(ue_1, formsemestre_1), (ue_2, formsemestre_2) = (
|
||||
(
|
||||
ue_2,
|
||||
formsemestre_2,
|
||||
),
|
||||
(ue_1, formsemestre_1),
|
||||
)
|
||||
assert formsemestre_1.semestre_id % 2 == 1
|
||||
assert formsemestre_2.semestre_id % 2 == 0
|
||||
assert abs(formsemestre_1.semestre_id - formsemestre_2.semestre_id) == 1
|
||||
assert ue_1.niveau_competence_id == ue_2.niveau_competence_id
|
||||
self.etud = etud
|
||||
self.formsemestre_1 = formsemestre_1
|
||||
"semestre impair"
|
||||
self.ue_1 = ue_1
|
||||
self.formsemestre_2 = formsemestre_2
|
||||
"semestre pair"
|
||||
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]
|
||||
self.moy_ue_1_val = self.moy_ue_1 # toujours float, peut être NaN
|
||||
else:
|
||||
self.moy_ue_1 = None
|
||||
self.moy_ue_1_val = 0.0
|
||||
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_2.id][etud.id]
|
||||
self.moy_ue_2_val = self.moy_ue_2
|
||||
else:
|
||||
self.moy_ue_2 = None
|
||||
self.moy_ue_2_val = 0.0
|
||||
# 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 -- TODO)
|
||||
self.moy_rcue = (self.moy_ue_1 + self.moy_ue_2) / 2
|
||||
else:
|
||||
self.moy_rcue = None
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<{self.__class__.__name__} {self.ue_1.acronyme}({self.moy_ue_1}) {self.ue_2.acronyme}({self.moy_ue_2})>"
|
||||
|
||||
def query_validations(
|
||||
self,
|
||||
) -> flask_sqlalchemy.BaseQuery: # list[ApcValidationRCUE]
|
||||
"""Les validations de jury enregistrées pour ce RCUE"""
|
||||
niveau = self.ue_2.niveau_competence
|
||||
|
||||
return (
|
||||
ApcValidationRCUE.query.filter_by(
|
||||
etudid=self.etud.id,
|
||||
)
|
||||
.join(UniteEns, UniteEns.id == ApcValidationRCUE.ue2_id)
|
||||
.join(ApcNiveau, UniteEns.niveau_id == ApcNiveau.id)
|
||||
.filter(ApcNiveau.id == niveau.id)
|
||||
)
|
||||
|
||||
def other_ue(self, ue: UniteEns) -> UniteEns:
|
||||
"""L'autre UE du regroupement. Si ue ne fait pas partie du regroupement, ValueError"""
|
||||
if ue.id == self.ue_1.id:
|
||||
return self.ue_2
|
||||
elif ue.id == self.ue_2.id:
|
||||
return self.ue_1
|
||||
raise ValueError(f"ue {ue} hors RCUE {self}")
|
||||
|
||||
def est_enregistre(self) -> bool:
|
||||
"""Vrai si ce RCUE, donc le niveau de compétences correspondant
|
||||
a une décision jury enregistrée
|
||||
"""
|
||||
return self.query_validations().count() > 0
|
||||
|
||||
def est_compense(self):
|
||||
"""Vrai si ce RCUE est validable par compensation
|
||||
c'est à dire que sa moyenne est > 10 avec une UE < 10
|
||||
"""
|
||||
return (
|
||||
(self.moy_rcue is not None)
|
||||
and (self.moy_rcue > sco_codes.BUT_BARRE_RCUE)
|
||||
and (
|
||||
(self.moy_ue_1_val < sco_codes.NOTES_BARRE_GEN)
|
||||
or (self.moy_ue_2_val < sco_codes.NOTES_BARRE_GEN)
|
||||
)
|
||||
)
|
||||
|
||||
def est_suffisant(self) -> bool:
|
||||
"""Vrai si ce RCUE est > 8"""
|
||||
return (self.moy_rcue is not None) and (
|
||||
self.moy_rcue > sco_codes.BUT_RCUE_SUFFISANT
|
||||
)
|
||||
|
||||
def est_validable(self) -> bool:
|
||||
"""Vrai si ce RCU satisfait les conditions pour être validé
|
||||
Pour cela, il suffit que la moyenne des UE qui le constitue soit > 10
|
||||
"""
|
||||
return (self.moy_rcue is not None) and (
|
||||
self.moy_rcue > sco_codes.BUT_BARRE_RCUE
|
||||
)
|
||||
|
||||
def code_valide(self) -> Union[ApcValidationRCUE, None]:
|
||||
"Si ce RCUE est ADM, CMP ou ADJ, la validation. Sinon, None"
|
||||
validation = self.query_validations().first()
|
||||
if (validation is not None) and (
|
||||
validation.code in {sco_codes.ADM, sco_codes.ADJ, sco_codes.CMP}
|
||||
):
|
||||
return validation
|
||||
return None
|
||||
|
||||
|
||||
def find_rcues(
|
||||
formsemestre: FormSemestre, ue: UniteEns, etud: Identite
|
||||
) -> list[RegroupementCoherentUE]:
|
||||
"""Les RCUE (niveau de compétence) à considérer pour cet étudiant dans
|
||||
ce semestre pour cette UE.
|
||||
|
||||
Cherche les UEs du même niveau de compétence auxquelles l'étudiant est inscrit.
|
||||
En cas de redoublement, il peut y en avoir plusieurs, donc plusieurs RCUEs.
|
||||
|
||||
Résultat: la liste peut être vide.
|
||||
"""
|
||||
if (ue.niveau_competence is None) or (ue.semestre_idx is None):
|
||||
return None, None
|
||||
return []
|
||||
|
||||
if ue.semestre_idx % 2:
|
||||
if ue.semestre_idx % 2: # S1, S3, S5
|
||||
other_semestre_idx = ue.semestre_idx + 1
|
||||
else:
|
||||
other_semestre_idx = ue.semestre_idx - 1
|
||||
@ -75,45 +216,38 @@ def get_other_ue_rcue(ue: UniteEns, etudid: int) -> tuple[UniteEns, FormSemestre
|
||||
cursor = db.session.execute(
|
||||
text(
|
||||
"""SELECT
|
||||
ue.id, sem.id
|
||||
ue.id, formsemestre.id
|
||||
FROM
|
||||
notes_ue ue,
|
||||
notes_formsemestre_inscription inscr,
|
||||
notes_formsemestre sem
|
||||
notes_formsemestre formsemestre
|
||||
|
||||
WHERE
|
||||
inscr.etudid = :etudid
|
||||
AND inscr.formsemestre_id = sem.id
|
||||
AND inscr.formsemestre_id = formsemestre.id
|
||||
|
||||
AND sem.semestre_id = :other_semestre_idx
|
||||
AND ue.formation_id = sem.formation_id
|
||||
AND formsemestre.semestre_id = :other_semestre_idx
|
||||
AND ue.formation_id = formsemestre.formation_id
|
||||
AND ue.niveau_competence_id = :ue_niveau_competence_id
|
||||
AND ue.semestre_idx = :other_semestre_idx
|
||||
"""
|
||||
),
|
||||
{
|
||||
"etudid": etudid,
|
||||
"etudid": etud.id,
|
||||
"other_semestre_idx": other_semestre_idx,
|
||||
"ue_niveau_competence_id": ue.niveau_competence_id,
|
||||
},
|
||||
)
|
||||
r = cursor.fetchone()
|
||||
if r is None:
|
||||
return None, None
|
||||
|
||||
return UniteEns.query.get(r[0]), FormSemestre.query.get(r[1])
|
||||
|
||||
# q = UniteEns.query.filter(
|
||||
# FormSemestreInscription.etudid == etudid,
|
||||
# FormSemestreInscription.formsemestre_id == FormSemestre.id,
|
||||
# FormSemestre.formation_id == UniteEns.formation_id,
|
||||
# FormSemestre.semestre_id == UniteEns.semestre_idx,
|
||||
# UniteEns.niveau_competence_id == ue.niveau_competence_id,
|
||||
# UniteEns.semestre_idx != ue.semestre_idx,
|
||||
# )
|
||||
# if q.count() > 1:
|
||||
# log("Warning: get_other_ue_rcue: {q.count()} candidates UE")
|
||||
# return q.first()
|
||||
rcues = []
|
||||
for ue_id, formsemestre_id in cursor:
|
||||
other_ue = UniteEns.query.get(ue_id)
|
||||
other_formsemestre = FormSemestre.query.get(formsemestre_id)
|
||||
rcues.append(
|
||||
RegroupementCoherentUE(etud, formsemestre, ue, other_formsemestre, other_ue)
|
||||
)
|
||||
# safety check: 1 seul niveau de comp. concerné:
|
||||
assert len({rcue.ue_1.niveau_competence_id for rcue in rcues}) == 1
|
||||
return rcues
|
||||
|
||||
|
||||
class ApcValidationAnnee(db.Model):
|
||||
@ -134,6 +268,7 @@ class ApcValidationAnnee(db.Model):
|
||||
formsemestre_id = db.Column(
|
||||
db.Integer, db.ForeignKey("notes_formsemestre.id"), nullable=True
|
||||
)
|
||||
"le semestre IMPAIR (le 1er) de l'année"
|
||||
annee_scolaire = db.Column(db.Integer, nullable=False) # 2021
|
||||
date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||
code = db.Column(db.String(CODE_STR_LEN), nullable=False, index=True)
|
||||
|
@ -54,7 +54,7 @@ class ScolarFormSemestreValidation(db.Model):
|
||||
ue = db.relationship("UniteEns", lazy="select", uselist=False)
|
||||
|
||||
def __repr__(self):
|
||||
return f"{self.__class__.__name__}({self.formsemestre_id}, {self.etudid}, code={self.code}, ue={self.ue_id}, moy_ue={self.moy_ue})"
|
||||
return f"{self.__class__.__name__}({self.formsemestre_id}, {self.etudid}, code={self.code}, ue={self.ue}, moy_ue={self.moy_ue})"
|
||||
|
||||
|
||||
class ScolarAutorisationInscription(db.Model):
|
||||
|
@ -68,7 +68,8 @@ NOTES_TOLERANCE = 0.00499999999999 # si note >= (BARRE-TOLERANCE), considere ok
|
||||
# (permet d'eviter d'afficher 10.00 sous barre alors que la moyenne vaut 9.999)
|
||||
|
||||
# Barre sur moyenne générale utilisée pour compensations semestres:
|
||||
NOTES_BARRE_GEN_COMPENSATION = 10.0 - NOTES_TOLERANCE
|
||||
NOTES_BARRE_GEN = 10.0
|
||||
NOTES_BARRE_GEN_COMPENSATION = NOTES_BARRE_GEN - NOTES_TOLERANCE
|
||||
|
||||
# ----------------------------------------------------------------
|
||||
# Types d'UE:
|
||||
@ -192,6 +193,8 @@ CODES_UE_VALIDES = {ADM: True, CMP: True} # UE validée
|
||||
# Pour le BUT:
|
||||
CODES_ANNEE_ARRET = {DEF, DEM, ABAN, ABL}
|
||||
CODES_RCUE = {ADM, AJ, CMP}
|
||||
BUT_BARRE_RCUE = 10.0 - NOTES_TOLERANCE
|
||||
BUT_RCUE_SUFFISANT = 8.0 - NOTES_TOLERANCE
|
||||
|
||||
|
||||
def code_semestre_validant(code: str) -> bool:
|
||||
|
@ -927,7 +927,7 @@ def _html_select_semestre_idx(formation_id, semestre_ids, semestre_idx):
|
||||
|
||||
def _ue_table_ues(
|
||||
parcours,
|
||||
ues,
|
||||
ues: list[dict],
|
||||
editable,
|
||||
tag_editable,
|
||||
has_perm_change,
|
||||
@ -936,7 +936,7 @@ def _ue_table_ues(
|
||||
arrow_none,
|
||||
delete_icon,
|
||||
delete_disabled_icon,
|
||||
):
|
||||
) -> str:
|
||||
"""Édition de programme: liste des UEs (avec leurs matières et modules).
|
||||
Pour les formations classiques (non APC/BUT)
|
||||
"""
|
||||
@ -964,9 +964,9 @@ def _ue_table_ues(
|
||||
if ue["semestre_id"] == sco_codes_parcours.UE_SEM_DEFAULT:
|
||||
lab = "Pas d'indication de semestre:"
|
||||
else:
|
||||
lab = "Semestre %s:" % ue["semestre_id"]
|
||||
lab = f"""Semestre {ue["semestre_id"]}:"""
|
||||
H.append(
|
||||
'<div class="ue_list_div"><div class="ue_list_tit_sem">%s</div>' % lab
|
||||
f'<div class="ue_list_div"><div class="ue_list_tit_sem">{lab}</div>'
|
||||
)
|
||||
H.append('<ul class="notes_ue_list">')
|
||||
H.append('<li class="notes_ue_list">')
|
||||
|
@ -32,6 +32,8 @@
|
||||
ue.color if ue.color is not none else 'blue'}}"></span>
|
||||
<b>{{ue.acronyme}}</b> <a class="discretelink" href="{{
|
||||
url_for('notes.ue_infos', scodoc_dept=g.scodoc_dept, ue_id=ue.id)}}"
|
||||
title="{{ue.acronyme}}: {{'pas de compétence associée' if ue.niveau_competence is none
|
||||
else 'compétence ' + ue.niveau_competence.annee + ' ' + ue.niveau_competence.competence.titre_long}}"
|
||||
>{{ue.titre}}</a>
|
||||
{% set virg = joiner(", ") %}
|
||||
<span class="ue_code">(
|
||||
|
@ -31,6 +31,7 @@ Module notes: issu de ScoDoc7 / ZNotes.py
|
||||
Emmanuel Viennet, 2021
|
||||
"""
|
||||
|
||||
import html
|
||||
from operator import itemgetter
|
||||
import time
|
||||
from xml.etree import ElementTree
|
||||
@ -41,8 +42,11 @@ from flask import current_app, g, request
|
||||
from flask_login import current_user
|
||||
from werkzeug.utils import redirect
|
||||
|
||||
from app.but import jury_but
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_but import ResultatsSemestreBUT
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models.etudiants import Identite
|
||||
from app.models.formsemestre import FormSemestre
|
||||
from app.models.formsemestre import FormSemestreUEComputationExpr
|
||||
from app.models.modules import Module
|
||||
@ -2209,6 +2213,54 @@ def formsemestre_validation_etud_manu(
|
||||
)
|
||||
|
||||
|
||||
# --- Jurys BUT
|
||||
@bp.route(
|
||||
"/formsemestre_validation_but/<int:formsemestre_id>/<int:etudid>",
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def formsemestre_validation_but(formsemestre_id: int, etudid: int):
|
||||
"Form. saisie décision jury semestre BUT"
|
||||
if not sco_permissions_check.can_validate_sem(formsemestre_id):
|
||||
return scu.confirm_dialog(
|
||||
message="<p>Opération non autorisée pour %s</h2>" % current_user,
|
||||
dest_url=url_for(
|
||||
"notes.formsemestre_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre_id,
|
||||
),
|
||||
)
|
||||
# XXX TODO Page expérimentale pour les devs
|
||||
H = [
|
||||
html_sco_header.sco_header(
|
||||
page_title="Validation BUT", formsemestre_id=formsemestre_id, etudid=etudid
|
||||
),
|
||||
f"""
|
||||
<h2>XXX Experimental XXX</h2>
|
||||
""",
|
||||
]
|
||||
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||
etud = Identite.query.get_or_404(etudid)
|
||||
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre)
|
||||
# ---- UEs
|
||||
H.append(f"<ul>")
|
||||
for ue in formsemestre.query_ues(): # volontairement toutes les UE
|
||||
dec_proposee = jury_but.DecisionsProposeesUE(etud, formsemestre, ue)
|
||||
H.append("<li>" + html.escape(f"""{ue} : {dec_proposee}""") + "</li>")
|
||||
H.append(f"</ul>")
|
||||
|
||||
if formsemestre.semestre_id % 2 == 0:
|
||||
# ---- RCUES
|
||||
H.append(f"<ul>")
|
||||
for ue in formsemestre.query_ues(): # volontairement toutes les UE
|
||||
dec_proposee = jury_but.decisions_ue_proposees(etud, formsemestre, ue)
|
||||
H.append("<li>" + html.escape(f"""{ue} : {dec_proposee}""") + "</li>")
|
||||
H.append(f"</ul>")
|
||||
|
||||
return "\n".join(H) + html_sco_header.sco_footer()
|
||||
|
||||
|
||||
@bp.route("/formsemestre_validate_previous_ue", methods=["GET", "POST"])
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
|
Loading…
Reference in New Issue
Block a user