WIP: jury BUT: validations annee, RCUE, UE

This commit is contained in:
Emmanuel Viennet 2022-06-21 11:21:17 +02:00
parent cd2de51bcc
commit a37a2c08e2
2 changed files with 87 additions and 46 deletions

View File

@ -117,15 +117,15 @@ class DecisionsProposees:
self.codes = []
"Les codes attribuables par ce jury"
if include_communs:
self.codes = self.codes_communs
self.codes = self.codes_communs.copy()
if isinstance(code, list):
self.codes = code + self.codes_communs
self.codes = code + self.codes
elif code is not None:
self.codes = [code] + self.codes_communs
self.codes = [code] + self.codes
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"
"Explication à afficher à côté de la décision"
def __repr__(self) -> str:
return f"""<{self.__class__.__name__} valid={self.code_valide
@ -193,48 +193,72 @@ class DecisionsProposeesAnnee(DecisionsProposees):
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
self.decisions_ues = {
ue.id: DecisionsProposeesUE(etud, formsemestre_impair, ue)
for ue in self.ues_impair
}
"{ue_id : DecisionsProposeesUE} pour toutes les UE de l'année"
self.decisions_ues.update(
{
ue.id: DecisionsProposeesUE(etud, formsemestre_pair, ue)
for ue in self.ues_pair
}
)
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.niveaux_competences = ApcNiveau.niveaux_annee_de_parcours(
self.parcour, self.annee_but
).all() # XXX à trier, selon l'ordre des UE associées ?
"liste des niveaux de compétences associés à cette année"
self.decisions_rcue_by_niveau = self.compute_decisions_niveaux()
"les décisions rcue associées aux niveau_id"
self.nb_competences = len(self.niveaux_competences)
"le nombre de niveaux de compétences à valider cette année"
self.nb_validables = len(
[rcue for rcue in self.rcues_annee if rcue.est_validable()]
)
"le nombre de comp. validables (éventuellement par compensation)"
self.nb_rcues_under_8 = len(
[rcue for rcue in self.rcues_annee if not rcue.est_suffisant()]
)
"le nb de comp. sous la barre de 8/20"
# 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
self.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)
self.passage_de_droit = self.valide_moitie_rcue and (self.nb_rcues_under_8 == 0)
# XXX TODO ajouter condition pour passage en S5
# Enfin calcule les codes des UE:
for dec_ue in self.decisions_ues.values():
dec_ue.compute_codes()
# Reste à attribuer ADM, ADJ, PASD, PAS1NCI, RED, NAR
expl_rcues = f"{self.nb_validables} validables sur {self.nb_competences}"
expl_rcues = (
f"{self.nb_validables} niveau validable(s) sur {self.nb_competences}"
)
if admis:
self.codes = [sco_codes.ADM] + self.codes
self.explanation = expl_rcues
elif passage_de_droit:
elif self.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
elif self.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"
self.explanation = expl_rcues + f" et {self.nb_rcues_under_8} niveau < 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}
formsemestre_pair: {self.formsemestre_pair}
RCUEs: {self.rcues_annee}
nb_competences: {self.nb_competences}
nb_nb_validables: {self.nb_validables}
@ -286,7 +310,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
# 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)
ues = list(res.etud_ues(etudid))
else:
parcour = ApcParcours.query.get(res.etuds_parcour_id[etudid])
if parcour is not None:
@ -343,10 +367,34 @@ class DecisionsProposeesAnnee(DecisionsProposees):
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()
ue = UniteEns.query.get(ues_impair_sans_rcue.pop())
raise ScoValueError(f"pas de RCUE pour l'UE {ue.acronyme}")
return rcues_annee
def compute_decisions_niveaux(self) -> dict[int, "DecisionsProposeesRCUE"]:
"""Pour chaque niveau de compétence de cette année, donne le DecisionsProposeesRCUE
ou None s'il n'y en a pas (ne devrait pas arriver car
compute_rcues_annee vérifie déjà cela).
Return: { niveau_id : DecisionsProposeesRCUE }
"""
# Retrouve le RCUE associé à chaque niveau
rc_niveaux = []
for niveau in self.niveaux_competences:
rcue = None
for rc in self.rcues_annee:
if rc.ue_1.niveau_competence_id == niveau.id:
rcue = rc
break
dec_rcue = DecisionsProposeesRCUE(self, rcue)
rc_niveaux.append((dec_rcue, niveau.id))
# prévient les UE concernées :-)
self.decisions_ues[dec_rcue.rcue.ue_1.id].set_rcue(dec_rcue.rcue)
self.decisions_ues[dec_rcue.rcue.ue_2.id].set_rcue(dec_rcue.rcue)
# Ordonne par numéro d'UE
rc_niveaux.sort(key=lambda x: x[0].rcue.ue_1.numero)
decisions_rcue_by_niveau = {x[1]: x[0] for x in rc_niveaux}
return decisions_rcue_by_niveau
class DecisionsProposeesRCUE(DecisionsProposees):
"""Liste des codes de décisions que l'on peut proposer pour
@ -371,7 +419,7 @@ class DecisionsProposeesRCUE(DecisionsProposees):
validation = rcue.query_validations().first()
if validation is not None:
self.code_valide = validation.code
if rcue.est_compense():
if rcue.est_compensable():
self.codes.insert(0, sco_codes.CMP)
elif rcue.est_validable():
self.codes.insert(0, sco_codes.ADM)
@ -412,6 +460,7 @@ class DecisionsProposeesUE(DecisionsProposees):
super().__init__(etud=etud)
self.ue: UniteEns = ue
self.rcue: RegroupementCoherentUE = None
"Le rcu auquel est rattaché cette UE, ou 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(
@ -423,17 +472,7 @@ class DecisionsProposeesUE(DecisionsProposees):
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:
@ -442,23 +481,25 @@ class DecisionsProposeesUE(DecisionsProposees):
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.moy_ue = res.etud_moy_ue[ue.id][etud.id]
def set_rcue(self, rcue: RegroupementCoherentUE):
"""Rattache cette UE à un RCUE. Cela peut modifier les codes
proposés (si compensation)"""
self.rcue = rcue
def compute_codes(self):
"""Calcul des .codes attribuables et de l'explanation associée"""
if self.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"
elif self.rcue and self.rcue.est_compensable():
self.codes.insert(0, sco_codes.CMP)
self.explanation = "compensable dans le RCUE"
else:
# Échec à valider cette UE
self.codes = [sco_codes.AJ, sco_codes.ADJ] + self.codes
self.explanation = "notes insuffisantes"
class BUTCursusEtud: # WIP TODO

View File

@ -67,7 +67,7 @@ class ApcValidationRCUE(db.Model):
# 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.
de la même année (BUT1,2,3) liées au *même niveau de compétence*.
La moyenne (10/20) au RCU déclenche la compensation des UE.
"""
@ -139,7 +139,7 @@ class RegroupementCoherentUE:
etudid=self.etud.id,
)
.join(UniteEns, UniteEns.id == ApcValidationRCUE.ue2_id)
.join(ApcNiveau, UniteEns.niveau_id == ApcNiveau.id)
.join(ApcNiveau, UniteEns.niveau_competence_id == ApcNiveau.id)
.filter(ApcNiveau.id == niveau.id)
)
@ -157,7 +157,7 @@ class RegroupementCoherentUE:
"""
return self.query_validations().count() > 0
def est_compense(self):
def est_compensable(self):
"""Vrai si ce RCUE est validable par compensation
c'est à dire que sa moyenne est > 10 avec une UE < 10
"""