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
|
"""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 operator import attrgetter
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
from app import log
|
||||||
from app.comp.res_but import ResultatsSemestreBUT
|
from app.comp.res_but import ResultatsSemestreBUT
|
||||||
from app.comp import res_sem
|
from app.comp import res_sem
|
||||||
from app.models import but_validations
|
|
||||||
from app.models.but_refcomp import (
|
from app.models.but_refcomp import (
|
||||||
ApcAnneeParcours,
|
ApcAnneeParcours,
|
||||||
ApcCompetence,
|
ApcCompetence,
|
||||||
ApcNiveau,
|
ApcNiveau,
|
||||||
|
ApcParcours,
|
||||||
ApcParcoursNiveauCompetence,
|
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.etudiants import Identite
|
||||||
from app.models.formations import Formation
|
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.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 import sco_utils as scu
|
||||||
from app.scodoc.sco_exceptions import ScoException
|
from app.scodoc.sco_exceptions import ScoException, ScoValueError
|
||||||
|
|
||||||
|
|
||||||
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:
|
class DecisionsProposees:
|
||||||
# Codes toujours proposés sauf si include_communs est faux:
|
"""Une décision de jury proposé, constituée d'une liste de codes et d'une explication.
|
||||||
codes_communs = [codes.RAT, codes.DEF, codes.ABAN, codes.DEM, codes.UEBSL]
|
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:
|
if include_communs:
|
||||||
self.codes = self.codes_communs
|
self.codes = self.codes_communs
|
||||||
else:
|
|
||||||
self.codes = []
|
|
||||||
if isinstance(code, list):
|
if isinstance(code, list):
|
||||||
self.codes = code + self.codes_communs
|
self.codes = code + self.codes_communs
|
||||||
elif code is not None:
|
elif code is not None:
|
||||||
self.codes = [code] + self.codes_communs
|
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:
|
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(
|
class DecisionsProposeesAnnee(DecisionsProposees):
|
||||||
etud: Identite, formsemestre: FormSemestre, ue: UniteEns
|
"""Décisions de jury sur une année (ETP) du BUT
|
||||||
) -> DecisionsProposees:
|
|
||||||
|
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
|
"""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
|
si moy_ue > 10, ADM
|
||||||
sinon si compensation dans RCUE: CMP
|
sinon si compensation dans RCUE: CMP
|
||||||
sinon: ADJ, AJ
|
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(
|
# Codes toujours proposés sauf si include_communs est faux:
|
||||||
explanation="UE bonus, pas de décision de jury", include_communs=False
|
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 ?
|
# Code sur année ?
|
||||||
decision_annee = ApcValidationAnnee.query.filter_by(
|
decision_annee = ApcValidationAnnee.query.filter_by(
|
||||||
etudid=etud.id, annee_scolaire=formsemestre.annee_scolaire()
|
etudid=etud.id, annee_scolaire=formsemestre.annee_scolaire()
|
||||||
).first()
|
).first()
|
||||||
if (
|
if (
|
||||||
decision_annee is not None and decision_annee.code in codes.CODES_ANNEE_ARRET
|
decision_annee is not None
|
||||||
|
and decision_annee.code in sco_codes.CODES_ANNEE_ARRET
|
||||||
): # DEF, DEM, ABAN, ABL
|
): # DEF, DEM, ABAN, ABL
|
||||||
return DecisionsProposees(
|
self.explanation = f"l'année a le code {decision_annee.code}"
|
||||||
code=decision_annee.code,
|
self.codes = [decision_annee.code] # sans les codes communs
|
||||||
explanation=f"l'année a le code {decision_annee.code}",
|
return
|
||||||
include_communs=False,
|
|
||||||
)
|
|
||||||
# Moyenne de l'UE ?
|
# Moyenne de l'UE ?
|
||||||
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre)
|
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre)
|
||||||
if not ue.id in res.etud_moy_ue:
|
if not ue.id in res.etud_moy_ue:
|
||||||
return DecisionsProposees(explanation="UE sans résultat")
|
self.explanation = "UE sans résultat"
|
||||||
|
return
|
||||||
if not etud.id in res.etud_moy_ue[ue.id]:
|
if not etud.id in res.etud_moy_ue[ue.id]:
|
||||||
return DecisionsProposees(explanation="Étudiant sans résultat dans cette UE")
|
self.explanation = "Étudiant sans résultat dans cette UE"
|
||||||
|
return
|
||||||
moy_ue = res.etud_moy_ue[ue.id][etud.id]
|
moy_ue = res.etud_moy_ue[ue.id][etud.id]
|
||||||
if moy_ue > (codes.ParcoursBUT.BARRE_MOY - codes.NOTES_TOLERANCE):
|
if moy_ue > (sco_codes.ParcoursBUT.BARRE_MOY - sco_codes.NOTES_TOLERANCE):
|
||||||
return DecisionsProposees(
|
self.codes.insert(0, sco_codes.ADM)
|
||||||
code=codes.ADM,
|
self.explanation = (f"Moyenne >= {sco_codes.ParcoursBUT.BARRE_MOY}/20",)
|
||||||
explanation=f"Moyenne >= {codes.ParcoursBUT.BARRE_MOY}/20",
|
|
||||||
)
|
# Compensation dans un RCUE ?
|
||||||
# Compensation dans le RCUE ?
|
rcues = but_validations.find_rcues(formsemestre, ue, etud)
|
||||||
other_ue, other_formsemestre = but_validations.get_other_ue_rcue(ue, etud.id)
|
for rcue in rcues:
|
||||||
if other_ue is not None:
|
if rcue.est_validable():
|
||||||
# inscrit à une autre UE du même RCUE
|
self.codes.insert(0, sco_codes.CMP)
|
||||||
other_res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(
|
self.explanation = f"Compensée par {rcue.other_ue(ue)} (moyenne RCUE={scu.fmt_note(rcue.moy_rcue)}/20"
|
||||||
other_formsemestre
|
self.rcue = rcue
|
||||||
)
|
return # s'arrête au 1er RCU validable
|
||||||
if (other_ue.id in other_res.etud_moy_ue) and (
|
|
||||||
etud.id in other_res.etud_moy_ue[other_ue.id]
|
# Échec à valider cette UE
|
||||||
):
|
self.codes = [sco_codes.AJ, sco_codes.ADJ] + self.codes
|
||||||
other_moy_ue = other_res.etud_moy_ue[other_ue.id][etud.id]
|
self.explanation = "notes insuffisantes"
|
||||||
# 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(
|
class BUTCursusEtud: # WIP TODO
|
||||||
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"""
|
"""Validation du cursus d'un étudiant"""
|
||||||
|
|
||||||
def __init__(self, formsemestre: FormSemestre, etud: Identite):
|
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
|
"""Vrai si la compétence est validée, c'est à dire que tous ses
|
||||||
niveaux sont validés (ApcValidationRCUE).
|
niveaux sont validés (ApcValidationRCUE).
|
||||||
"""
|
"""
|
||||||
|
# XXX A REVOIR
|
||||||
validations = (
|
validations = (
|
||||||
ApcValidationRCUE.query.filter_by(etudid=self.etud.id)
|
ApcValidationRCUE.query.filter_by(etudid=self.etud.id)
|
||||||
.join(UniteEns, UniteEns.id == ApcValidationRCUE.ue1_id)
|
.join(UniteEns, UniteEns.id == ApcValidationRCUE.ue1_id)
|
||||||
|
@ -29,6 +29,7 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
|||||||
"modimpl_coefs_df",
|
"modimpl_coefs_df",
|
||||||
"modimpls_evals_poids",
|
"modimpls_evals_poids",
|
||||||
"sem_cube",
|
"sem_cube",
|
||||||
|
"etuds_parcour_id", # parcours de chaque étudiant
|
||||||
"ues_inscr_parcours_df", # inscriptions aux UE / parcours
|
"ues_inscr_parcours_df", # inscriptions aux UE / parcours
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -37,7 +38,8 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
|||||||
|
|
||||||
self.sem_cube = None
|
self.sem_cube = None
|
||||||
"""ndarray (etuds x modimpl x ue)"""
|
"""ndarray (etuds x modimpl x ue)"""
|
||||||
|
self.etuds_parcour_id = None
|
||||||
|
"""Parcours de chaque étudiant { etudid : parcour_id }"""
|
||||||
if not self.load_cached():
|
if not self.load_cached():
|
||||||
t0 = time.time()
|
t0 = time.time()
|
||||||
self.compute()
|
self.compute()
|
||||||
@ -190,13 +192,14 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
|||||||
La matrice avec ue ne comprend que les UE non bonus.
|
La matrice avec ue ne comprend que les UE non bonus.
|
||||||
1.0 si étudiant inscrit à l'UE, NaN sinon.
|
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
|
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]
|
ue_ids = [ue.id for ue in self.ues]
|
||||||
# matrice de 1, inscrits par défaut à toutes les UE:
|
# matrice de 1, inscrits par défaut à toutes les UE:
|
||||||
ues_inscr_parcours_df = pd.DataFrame(
|
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:
|
if self.formsemestre.formation.referentiel_competence is None:
|
||||||
return ues_inscr_parcours_df
|
return ues_inscr_parcours_df
|
||||||
@ -209,11 +212,11 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
|||||||
parcour
|
parcour
|
||||||
).filter_by(semestre_idx=self.formsemestre.semestre_id)
|
).filter_by(semestre_idx=self.formsemestre.semestre_id)
|
||||||
}
|
}
|
||||||
for etudid in etuds_parcours:
|
for etudid in etuds_parcour_id:
|
||||||
parcour = etuds_parcours[etudid]
|
parcour = etuds_parcour_id[etudid]
|
||||||
if parcour is not None:
|
if parcour is not None:
|
||||||
ues_inscr_parcours_df.loc[etudid] = ue_by_parcours[
|
ues_inscr_parcours_df.loc[etudid] = ue_by_parcours[
|
||||||
etuds_parcours[etudid]
|
etuds_parcour_id[etudid]
|
||||||
]
|
]
|
||||||
return ues_inscr_parcours_df
|
return ues_inscr_parcours_df
|
||||||
|
|
||||||
|
@ -3,13 +3,18 @@
|
|||||||
"""Décisions de jury (validations) des RCUE et années du BUT
|
"""Décisions de jury (validations) des RCUE et années du BUT
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import flask_sqlalchemy
|
||||||
from sqlalchemy.sql import text
|
from sqlalchemy.sql import text
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
|
|
||||||
from app.models import CODE_STR_LEN
|
from app.models import CODE_STR_LEN
|
||||||
from app.models.but_refcomp import ApcNiveau
|
from app.models.but_refcomp import ApcNiveau
|
||||||
|
from app.models.etudiants import Identite
|
||||||
from app.models.ues import UniteEns
|
from app.models.ues import UniteEns
|
||||||
from app.models.formsemestre import FormSemestre
|
from app.models.formsemestre import FormSemestre
|
||||||
|
from app.scodoc import sco_codes_parcours as sco_codes
|
||||||
|
|
||||||
|
|
||||||
class ApcValidationRCUE(db.Model):
|
class ApcValidationRCUE(db.Model):
|
||||||
@ -17,6 +22,7 @@ class ApcValidationRCUE(db.Model):
|
|||||||
|
|
||||||
aka "regroupements cohérents d'UE" dans le jargon BUT.
|
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"
|
__tablename__ = "apc_validation_rcue"
|
||||||
@ -58,16 +64,151 @@ class ApcValidationRCUE(db.Model):
|
|||||||
return self.ue2.niveau_competence
|
return self.ue2.niveau_competence
|
||||||
|
|
||||||
|
|
||||||
def get_other_ue_rcue(ue: UniteEns, etudid: int) -> tuple[UniteEns, FormSemestre]:
|
# Attention: ce n'est pas un modèle mais une classe ordinaire:
|
||||||
"""L'autre UE du RCUE (niveau de compétence) pour cet étudiant.
|
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.
|
La moyenne (10/20) au RCU déclenche la compensation des UE.
|
||||||
Résultat: le couple (UE, FormSemestre), ou (None, None) si pas trouvée.
|
"""
|
||||||
|
|
||||||
|
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):
|
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
|
other_semestre_idx = ue.semestre_idx + 1
|
||||||
else:
|
else:
|
||||||
other_semestre_idx = ue.semestre_idx - 1
|
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(
|
cursor = db.session.execute(
|
||||||
text(
|
text(
|
||||||
"""SELECT
|
"""SELECT
|
||||||
ue.id, sem.id
|
ue.id, formsemestre.id
|
||||||
FROM
|
FROM
|
||||||
notes_ue ue,
|
notes_ue ue,
|
||||||
notes_formsemestre_inscription inscr,
|
notes_formsemestre_inscription inscr,
|
||||||
notes_formsemestre sem
|
notes_formsemestre formsemestre
|
||||||
|
|
||||||
WHERE
|
WHERE
|
||||||
inscr.etudid = :etudid
|
inscr.etudid = :etudid
|
||||||
AND inscr.formsemestre_id = sem.id
|
AND inscr.formsemestre_id = formsemestre.id
|
||||||
|
|
||||||
AND sem.semestre_id = :other_semestre_idx
|
AND formsemestre.semestre_id = :other_semestre_idx
|
||||||
AND ue.formation_id = sem.formation_id
|
AND ue.formation_id = formsemestre.formation_id
|
||||||
AND ue.niveau_competence_id = :ue_niveau_competence_id
|
AND ue.niveau_competence_id = :ue_niveau_competence_id
|
||||||
AND ue.semestre_idx = :other_semestre_idx
|
AND ue.semestre_idx = :other_semestre_idx
|
||||||
"""
|
"""
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
"etudid": etudid,
|
"etudid": etud.id,
|
||||||
"other_semestre_idx": other_semestre_idx,
|
"other_semestre_idx": other_semestre_idx,
|
||||||
"ue_niveau_competence_id": ue.niveau_competence_id,
|
"ue_niveau_competence_id": ue.niveau_competence_id,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
r = cursor.fetchone()
|
rcues = []
|
||||||
if r is None:
|
for ue_id, formsemestre_id in cursor:
|
||||||
return None, None
|
other_ue = UniteEns.query.get(ue_id)
|
||||||
|
other_formsemestre = FormSemestre.query.get(formsemestre_id)
|
||||||
return UniteEns.query.get(r[0]), FormSemestre.query.get(r[1])
|
rcues.append(
|
||||||
|
RegroupementCoherentUE(etud, formsemestre, ue, other_formsemestre, other_ue)
|
||||||
# q = UniteEns.query.filter(
|
)
|
||||||
# FormSemestreInscription.etudid == etudid,
|
# safety check: 1 seul niveau de comp. concerné:
|
||||||
# FormSemestreInscription.formsemestre_id == FormSemestre.id,
|
assert len({rcue.ue_1.niveau_competence_id for rcue in rcues}) == 1
|
||||||
# FormSemestre.formation_id == UniteEns.formation_id,
|
return rcues
|
||||||
# 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()
|
|
||||||
|
|
||||||
|
|
||||||
class ApcValidationAnnee(db.Model):
|
class ApcValidationAnnee(db.Model):
|
||||||
@ -134,6 +268,7 @@ class ApcValidationAnnee(db.Model):
|
|||||||
formsemestre_id = db.Column(
|
formsemestre_id = db.Column(
|
||||||
db.Integer, db.ForeignKey("notes_formsemestre.id"), nullable=True
|
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
|
annee_scolaire = db.Column(db.Integer, nullable=False) # 2021
|
||||||
date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||||
code = db.Column(db.String(CODE_STR_LEN), nullable=False, index=True)
|
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)
|
ue = db.relationship("UniteEns", lazy="select", uselist=False)
|
||||||
|
|
||||||
def __repr__(self):
|
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):
|
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)
|
# (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:
|
# 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:
|
# Types d'UE:
|
||||||
@ -192,6 +193,8 @@ CODES_UE_VALIDES = {ADM: True, CMP: True} # UE validée
|
|||||||
# Pour le BUT:
|
# Pour le BUT:
|
||||||
CODES_ANNEE_ARRET = {DEF, DEM, ABAN, ABL}
|
CODES_ANNEE_ARRET = {DEF, DEM, ABAN, ABL}
|
||||||
CODES_RCUE = {ADM, AJ, CMP}
|
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:
|
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(
|
def _ue_table_ues(
|
||||||
parcours,
|
parcours,
|
||||||
ues,
|
ues: list[dict],
|
||||||
editable,
|
editable,
|
||||||
tag_editable,
|
tag_editable,
|
||||||
has_perm_change,
|
has_perm_change,
|
||||||
@ -936,7 +936,7 @@ def _ue_table_ues(
|
|||||||
arrow_none,
|
arrow_none,
|
||||||
delete_icon,
|
delete_icon,
|
||||||
delete_disabled_icon,
|
delete_disabled_icon,
|
||||||
):
|
) -> str:
|
||||||
"""Édition de programme: liste des UEs (avec leurs matières et modules).
|
"""Édition de programme: liste des UEs (avec leurs matières et modules).
|
||||||
Pour les formations classiques (non APC/BUT)
|
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:
|
if ue["semestre_id"] == sco_codes_parcours.UE_SEM_DEFAULT:
|
||||||
lab = "Pas d'indication de semestre:"
|
lab = "Pas d'indication de semestre:"
|
||||||
else:
|
else:
|
||||||
lab = "Semestre %s:" % ue["semestre_id"]
|
lab = f"""Semestre {ue["semestre_id"]}:"""
|
||||||
H.append(
|
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('<ul class="notes_ue_list">')
|
||||||
H.append('<li 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>
|
ue.color if ue.color is not none else 'blue'}}"></span>
|
||||||
<b>{{ue.acronyme}}</b> <a class="discretelink" href="{{
|
<b>{{ue.acronyme}}</b> <a class="discretelink" href="{{
|
||||||
url_for('notes.ue_infos', scodoc_dept=g.scodoc_dept, ue_id=ue.id)}}"
|
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>
|
>{{ue.titre}}</a>
|
||||||
{% set virg = joiner(", ") %}
|
{% set virg = joiner(", ") %}
|
||||||
<span class="ue_code">(
|
<span class="ue_code">(
|
||||||
|
@ -31,6 +31,7 @@ Module notes: issu de ScoDoc7 / ZNotes.py
|
|||||||
Emmanuel Viennet, 2021
|
Emmanuel Viennet, 2021
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import html
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
import time
|
import time
|
||||||
from xml.etree import ElementTree
|
from xml.etree import ElementTree
|
||||||
@ -41,8 +42,11 @@ from flask import current_app, g, request
|
|||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from werkzeug.utils import redirect
|
from werkzeug.utils import redirect
|
||||||
|
|
||||||
|
from app.but import jury_but
|
||||||
from app.comp import res_sem
|
from app.comp import res_sem
|
||||||
|
from app.comp.res_but import ResultatsSemestreBUT
|
||||||
from app.comp.res_compat import NotesTableCompat
|
from app.comp.res_compat import NotesTableCompat
|
||||||
|
from app.models.etudiants import Identite
|
||||||
from app.models.formsemestre import FormSemestre
|
from app.models.formsemestre import FormSemestre
|
||||||
from app.models.formsemestre import FormSemestreUEComputationExpr
|
from app.models.formsemestre import FormSemestreUEComputationExpr
|
||||||
from app.models.modules import Module
|
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"])
|
@bp.route("/formsemestre_validate_previous_ue", methods=["GET", "POST"])
|
||||||
@scodoc
|
@scodoc
|
||||||
@permission_required(Permission.ScoView)
|
@permission_required(Permission.ScoView)
|
||||||
|
Loading…
Reference in New Issue
Block a user