Skip to content
Snippets Groups Projects
Commit 5d30b923 authored by Emmanuel Viennet's avatar Emmanuel Viennet
Browse files

WIP: nouvelles gestion jury BUT.

parent 4b49fd5e
Branches
No related tags found
No related merge requests found
......@@ -36,7 +36,6 @@ from app.models import Scolog, ScolarAutorisationInscription
from app.models.but_validations import (
ApcValidationAnnee,
ApcValidationRCUE,
RegroupementCoherentUE,
)
from app.models.etudiants import Identite
from app.models.formations import Formation
......
......@@ -25,8 +25,8 @@ Utilisation:
- 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 dune compétence emporte la validation
de lensemble des UE du niveau inférieur de cette même compétence.")
("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`.
......@@ -60,7 +60,6 @@ DecisionsProposeesUE: décisions de jury sur une UE du BUT
"""
from datetime import datetime
import html
from operator import attrgetter
import re
from typing import Union
......@@ -71,6 +70,7 @@ from app import db
from app import log
from app.but import cursus_but
from app.but.cursus_but import EtudCursusBUT
from app.but.rcue import RegroupementCoherentUE
from app.comp.res_but import ResultatsSemestreBUT
from app.comp import res_sem
......@@ -83,7 +83,6 @@ from app.models import Scolog, ScolarAutorisationInscription
from app.models.but_validations import (
ApcValidationAnnee,
ApcValidationRCUE,
RegroupementCoherentUE,
)
from app.models.etudiants import Identite
from app.models.formations import Formation
......@@ -186,10 +185,10 @@ class DecisionsProposeesAnnee(DecisionsProposees):
"""Décisions de jury sur une année (ETP) du BUT
Le texte:
La poursuite d'études dans un semestre pair dune 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 dUE;
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
......@@ -220,65 +219,71 @@ class DecisionsProposeesAnnee(DecisionsProposees):
raise ScoNoReferentielCompetences(formation=formsemestre.formation)
super().__init__(etud=etud)
self.formsemestre = formsemestre
"le formsemestre utilisé pour construire ce deca"
self.formsemestre_id = formsemestre.id
"l'id du formsemestre utilisé pour construire ce deca"
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
)
)
)
# Si les années scolaires sont distinctes, on est "à cheval"
self.a_cheval = (
formsemestre_impair
and formsemestre_pair
and formsemestre_impair.annee_scolaire()
!= formsemestre_pair.annee_scolaire()
)
"vrai si on groupe deux semestres d'années scolaires différentes"
"le formsemestre d'origine, utilisé pour construire ce deca"
# Si on part d'un semestre IMPAIR, il n'y aura pas de décision année proposée
# (mais on pourra évidemment valider des UE et même des RCUE)
self.jury_annuel: bool = formsemestre.semestre_id in (2, 4, 6)
"vrai si jury de fin d'année scolaire (sem. pair, propose code annuel)"
self.formsemestre_impair = formsemestre_impair
"le 1er semestre du groupement (S1, S3, S5)"
self.formsemestre_pair = formsemestre_pair
"le second formsemestre (S2, S4, S6), de la même année scolaire ou d'une précédente"
formsemestre_last = formsemestre_pair or formsemestre_impair
"le formsemestre le plus avancé (en indice de semestre) dans le groupement"
self.annee_but = (formsemestre_last.semestre_id + 1) // 2
self.annee_but = (formsemestre.semestre_id + 1) // 2
"le rang de l'année dans le BUT: 1, 2, 3"
assert self.annee_but in (1, 2, 3)
self.autorisations_recorded = False
"vrai si on a enregistré l'autorisation de passage"
self.rcues_annee = []
"""RCUEs de l'année
(peuvent concerner l'année scolaire antérieur pour les redoublants
avec UE capitalisées)
"""
self.inscription_etat = etud.inscription_etat(formsemestre_last.id)
"état de l'inscription dans le semestre le plus avancé (pair si année complète)"
self.inscription_etat_pair = (
etud.inscription_etat(formsemestre_pair.id)
if formsemestre_pair is not None
# ---- inscription et parcours
inscription = formsemestre.etuds_inscriptions.get(etud.id)
if inscription is None:
raise ValueError("Etudiant non inscrit au semestre")
self.inscription_etat = inscription.etat
"état de l'inscription dans le semestre origine"
self.parcour = inscription.parcour
"Le parcours considéré, qui est celui de l'étudiant dans le formsemestre origine"
self.formsemestre_impair, self.formsemestre_pair = self.comp_formsemestres(
formsemestre
)
# ---- résultats et UEs en cours cette année:
self.res_impair: ResultatsSemestreBUT = (
res_sem.load_formsemestre_results(self.formsemestre_impair)
if self.formsemestre_impair
else None
)
self.inscription_etat_impair = (
etud.inscription_etat(formsemestre_impair.id)
if formsemestre_impair is not None
self.res_pair: ResultatsSemestreBUT = (
res_sem.load_formsemestre_results(self.formsemestre_pair)
if self.formsemestre_pair
else None
)
if self.formsemestre_impair is not None:
self.cur_ues_impair = (
list_ue_parcour_etud(
self.formsemestre_impair, self.etud, self.parcour, self.res_impair
)
if self.formsemestre_impair
else []
)
self.cur_ues_pair = (
list_ue_parcour_etud(
self.formsemestre_pair, self.etud, self.parcour, self.res_pair
)
if self.formsemestre_pair
else []
)
# ---- Niveaux et RCUEs
niveaux_by_parcours = (
formsemestre.formation.referentiel_competence.get_niveaux_by_parcours(
self.annee_but, [self.parcour] if self.parcour else None
)[1]
)
self.niveaux_competences = niveaux_by_parcours["TC"] + (
niveaux_by_parcours[self.parcour.id] if self.parcour else []
)
"""Les niveaux à valider pour cet étudiant dans cette année, compte tenu de son parcours.
Liste non triée des niveaux de compétences associés à cette année pour cet étudiant.
= niveaux du tronc commun + niveau du parcours de l'étudiant.
"""
self.rcue_by_niveau = self._compute_rcues_annee()
"""RCUEs de l'année
(peuvent être construits avec des UEs validées antérieurement: redoublants
avec UEs capitalisées, validation "antérieures")
"""
# ---- Décision année et autorisation
self.autorisations_recorded = False
"vrai si on a enregistré l'autorisation de passage"
self.validation = (
ApcValidationAnnee.query.filter_by(
etudid=self.etud.id,
......@@ -288,61 +293,28 @@ class DecisionsProposeesAnnee(DecisionsProposees):
.filter_by(formation_code=self.formsemestre.formation.formation_code)
.first()
)
else:
self.validation = None
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)"
if self.formsemestre_pair is not None:
self.res_pair: ResultatsSemestreBUT = res_sem.load_formsemestre_results(
self.formsemestre_pair
)
else:
self.res_pair = None
if self.formsemestre_impair is not None:
self.res_impair: ResultatsSemestreBUT = res_sem.load_formsemestre_results(
self.formsemestre_impair
)
else:
self.res_impair = None
"Validation actuellement enregistrée pour cette année BUT"
self.code_valide = self.validation.code if self.validation is not None else None
"Le code jury annuel enregistré, ou None"
self.ues_impair, self.ues_pair = self.compute_ues_annee() # pylint: disable=all
# ---- Décisions d'UEs
self.decisions_ues = {
ue.id: DecisionsProposeesUE(
etud, formsemestre_impair, ue, self.inscription_etat_impair
rcue.ue_1.id: DecisionsProposeesUE(
etud, self.formsemestre_impair, rcue, False, self.inscription_etat
)
for ue in self.ues_impair
for rcue in self.rcue_by_niveau.values()
if rcue.ue_1
}
"{ue_id : DecisionsProposeesUE} pour toutes les UE de l'année"
self.decisions_ues.update(
{
ue.id: DecisionsProposeesUE(
etud, formsemestre_pair, ue, self.inscription_etat_pair
rcue.ue_2.id: DecisionsProposeesUE(
etud, self.formsemestre_pair, rcue, True, self.inscription_etat
)
for ue in self.ues_pair
for rcue in self.rcue_by_niveau.values()
if rcue.ue_2
}
)
self.rcues_annee = self.compute_rcues_annee()
formation = (
self.formsemestre_impair.formation
if self.formsemestre_impair
else self.formsemestre_pair.formation
)
(
parcours,
niveaux_by_parcours,
) = formation.referentiel_competence.get_niveaux_by_parcours(
self.annee_but, [self.parcour] if self.parcour else None
)
self.niveaux_competences = niveaux_by_parcours["TC"] + (
niveaux_by_parcours[self.parcour.id] if self.parcour else []
)
"""liste non triée des niveaux de compétences associés à cette année pour cet étudiant.
= niveaux du tronc commun + niveau du parcours de l'étudiant.
"""
self.decisions_rcue_by_niveau = self.compute_decisions_niveaux()
self.decisions_rcue_by_niveau = self._compute_decisions_niveaux()
"les décisions rcue associées aux niveau_id"
self.dec_rcue_by_ue = self._dec_rcue_by_ue()
"{ ue_id : DecisionsProposeesRCUE } pour toutes les UE associées à un niveau"
......@@ -371,12 +343,12 @@ class DecisionsProposeesAnnee(DecisionsProposees):
self.passage_de_droit = self.valide_moitie_rcue and (self.nb_rcues_under_8 == 0)
"Vrai si peut passer dans l'année BUT suivante: plus de la moitié validables et tous > 8"
explanation = ""
# Cas particulier du passage en BUT 3: nécessité davoir validé toutes les UEs du BUT 1.
# Cas particulier du passage en BUT 3: nécessité d'avoir validé toutes les UEs du BUT 1.
if self.passage_de_droit and self.annee_but == 2:
inscription = formsemestre.etuds_inscriptions.get(etud.id)
if inscription:
ues_but1_non_validees = cursus_but.etud_ues_de_but1_non_validees(
etud, formation, inscription.parcour
etud, self.formsemestre.formation, self.parcour
)
self.passage_de_droit = not ues_but1_non_validees
explanation += (
......@@ -434,7 +406,8 @@ class DecisionsProposeesAnnee(DecisionsProposees):
sco_codes.ADJ,
sco_codes.PASD, # voir #488 (discutable, conventions locales)
] + self.codes
explanation += f""" et {self.nb_rcues_under_8} niveau{'x' if self.nb_rcues_under_8 > 1 else ''} < 8"""
explanation += f""" et {self.nb_rcues_under_8
} niveau{'x' if self.nb_rcues_under_8 > 1 else ''} < 8"""
# Si l'un des semestres est extérieur, propose ADM
if (
......@@ -487,7 +460,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
text += "</li>"
text += f"""
<li>RCUEs: {html.escape(str(self.rcues_annee))}</li>
<li>RCUEs: {html.escape(str(self.rcue_by_niveau))}</li>
<li>nb_competences: {getattr(self, "nb_competences", "-")}</li>
<li>nb_validables: {getattr(self, "nb_validables", "-")}</li>
<li>codes: {self.codes}</li>
......@@ -509,177 +482,107 @@ class DecisionsProposeesAnnee(DecisionsProposees):
def comp_formsemestres(
self, formsemestre: FormSemestre
) -> tuple[FormSemestre, FormSemestre]:
"""Les deux formsemestres du niveau auquel appartient formsemestre.
Complète le niveau avec le formsemestre antérieur le plus récent.
L'"autre" formsemestre peut ainsi appartenir à l'année scolaire
antérieure (redoublants).
"""Les deux formsemestres auquel est inscrit l'étudiant (ni DEM ni DEF)
du niveau auquel appartient formsemestre.
-> S_impair, S_pair
Si l'origine est impair, S_impair est l'origine et S_pair est None
Si l'origine est paire, S_pair est l'origine, et S_impair l'antérieur
suivi par cet étudiant (ou None).
"""
if not formsemestre.formation.is_apc(): # garde fou
return None, None
if formsemestre.semestre_id % 2 == 0:
other_semestre_id = formsemestre.semestre_id - 1
if formsemestre.semestre_id % 2:
idx_autre = formsemestre.semestre_id + 1
else:
other_semestre_id = formsemestre.semestre_id + 1
idx_autre = formsemestre.semestre_id - 1
other_formsemestre = None
# Cherche l'autre semestre de la même année scolaire:
autre_formsemestre = None
for inscr in self.etud.formsemestre_inscriptions:
if (
(inscr.etat == scu.INSCRIT)
and
# 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)
# Antérieur
and inscr.formsemestre.date_debut < formsemestre.date_debut
# Et plus le récent possible
and (
(other_formsemestre is None)
or (other_formsemestre.date_debut < inscr.formsemestre.date_debut)
)
and (inscr.formsemestre.semestre_id == idx_autre)
# de la même année scolaire
and inscr.formsemestre.annee_scolaire() == formsemestre.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.
Affecte self.parcour suivant l'inscription de l'étudiant et
ramène [ listes des UE du semestre impair, liste des UE du semestre pair ].
"""
ues_sems = []
for formsemestre, res in (
(self.formsemestre_impair, self.res_impair),
(self.formsemestre_pair, self.res_pair),
):
if (formsemestre is None) or (not formsemestre.formation.is_apc()):
ues = []
autre_formsemestre = inscr.formsemestre
break
# autre_formsemestre peut être None
if formsemestre.semestre_id % 2:
return formsemestre, autre_formsemestre
else:
parcour, ues = list_ue_parcour_etud(formsemestre, self.etud, res)
if parcour is not None:
self.parcour = parcour
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
return autre_formsemestre, formsemestre
def compute_rcues_annee(self) -> list[RegroupementCoherentUE]:
"""Liste des regroupements d'UE à considérer cette année.
On peut avoir un RCUE à cheval sur plusieurs années (redoublants avec UE capitalisées).
Si on n'a pas les deux semestres, aucun 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 self.a_cheval:
# l'UE paire DOIT être capitalisée pour être utilisée
if (
self.decisions_ues[ue_pair.id].code_valide
not in CODES_UE_CAPITALISANTS
):
continue # ignore cette UE antérieure non capitalisée
# et l'UE impaire doit être actuellement meilleure que
# celle éventuellement capitalisée
if (
self.decisions_ues[ue_impair.id].ue_status
and self.decisions_ues[ue_impair.id].ue_status["is_capitalized"]
):
continue # ignore cette UE car capitalisée et actuelle moins bonne
if ue_pair.niveau_competence_id == ue_impair.niveau_competence_id:
rcue = RegroupementCoherentUE(
def get_decisions_rcues_annee(self) -> list["DecisionsProposeesRCUE"]:
"Liste des DecisionsProposeesRCUE de l'année, tirée par numéro d'UE"
return self.decisions_rcue_by_niveau.values()
def _compute_rcues_annee(self) -> dict[int, RegroupementCoherentUE]:
"calcule tous les RCUEs: { niveau_id : rcue }"
semestre_id_impair = ((self.formsemestre.semestre_id - 1) // 2) * 2 + 1
return {
niveau.id: RegroupementCoherentUE(
self.etud,
self.formsemestre_impair,
self.decisions_ues[ue_impair.id],
self.formsemestre_pair,
self.decisions_ues[ue_pair.id],
self.inscription_etat,
)
ues_impair_sans_rcue.discard(ue_impair.id)
break
# if rcue is None and not self.a_cheval:
# raise NoRCUEError(deca=self, ue=ue_pair)
if rcue is not None:
rcues_annee.append(rcue)
# Si jury annuel (pas à cheval), on doit avoir tous les RCUEs:
# if len(ues_impair_sans_rcue) > 0 and not self.a_cheval:
# ue = UniteEns.query.get(ues_impair_sans_rcue.pop())
# raise NoRCUEError(deca=self, ue=ue)
return rcues_annee
def compute_decisions_niveaux(self) -> dict[int, "DecisionsProposeesRCUE"]:
"""Pour chaque niveau de compétence de cette année, construit
le DecisionsProposeesRCUE, ou None s'il n'y en a pas
(ne devrait pas arriver car compute_rcues_annee vérifie déjà cela).
niveau,
self.res_pair,
self.res_impair,
semestre_id_impair,
self.cur_ues_pair,
self.cur_ues_impair,
)
for niveau in self.niveaux_competences
}
def _compute_decisions_niveaux(self) -> dict[int, "DecisionsProposeesRCUE"]:
"""Pour chaque niveau de compétence de cette année, construit
le DecisionsProposeesRCUE à partir du rcue déjà calculé.
Appelé à la construction du deca, donc avant décisions manuelles.
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
if rcue is not None:
dec_rcue = DecisionsProposeesRCUE(self, rcue, self.inscription_etat)
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
niv_rcue = sorted(
self.rcue_by_niveau.items(),
key=lambda x: x[1].ue_1.numero
if x[1].ue_1
else x[1].ue_2.numero
if x[1].ue_2
else 0,
)
return {
niveau_id: DecisionsProposeesRCUE(self, rcue, self.inscription_etat)
for (niveau_id, rcue) in niv_rcue
}
def _dec_rcue_by_ue(self) -> dict[int, "DecisionsProposeesRCUE"]:
"""construit dict { ue_id : DecisionsProposeesRCUE }
à partir de self.decisions_rcue_by_niveau"""
d = {}
for dec_rcue in self.decisions_rcue_by_niveau.values():
if dec_rcue.rcue.ue_1:
d[dec_rcue.rcue.ue_1.id] = dec_rcue
if dec_rcue.rcue.ue_2:
d[dec_rcue.rcue.ue_2.id] = dec_rcue
return d
def formsemestre_ects(self) -> float:
"ECTS validés dans le formsemestre de départ du deca"
ues = self.ues_impair if self.formsemestre.semestre_id % 2 else self.ues_pair
return sum(
[
self.decisions_ues[ue.id].ects_acquis()
for ue in ues
if ue.id in self.decisions_ues
]
)
def ects_annee(self) -> float:
"ECTS validés dans l'année BUT courante"
return sum([dec_ue.ects_acquis() for dec_ue in self.decisions_ues.values()])
def next_semestre_ids(self, code: str) -> set[int]:
"""Les indices des semestres dans lequels l'étudiant est autorisé
à poursuivre après le semestre courant.
"""
# La poursuite d'études dans un semestre pair dune même année
# La poursuite d'études dans un semestre pair d'une même année
# est de droit pour tout étudiant.
# Pas de redoublements directs de S_impair vers S_impair
# (pourront être traités manuellement)
......@@ -687,9 +590,9 @@ class DecisionsProposeesAnnee(DecisionsProposees):
self.formsemestre.semestre_id % 2
) and self.formsemestre.semestre_id < sco_codes.CursusBUT.NB_SEM:
return {self.formsemestre.semestre_id + 1}
# 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 dUE ;
# 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 condition a paru trop stricte à de nombreux collègues.
......@@ -914,20 +817,20 @@ class DecisionsProposeesAnnee(DecisionsProposees):
pour cette année: décisions d'UE, de RCUE, d'année,
et autorisations d'inscription émises.
Efface même si étudiant DEM ou DEF.
Si à cheval ou only_one_sem, n'efface que les décisions UE et les
Si only_one_sem, n'efface que les décisions UE et les
autorisations de passage du semestre d'origine du deca.
Dans tous les cas, efface les validations de l'année en cours.
(commite la session.)
"""
if only_one_sem or self.a_cheval:
if only_one_sem:
# N'efface que les autorisations venant de ce semestre,
# et les validations de ses UEs
ScolarAutorisationInscription.delete_autorisation_etud(
self.etud.id, self.formsemestre_id
self.etud.id, self.formsemestre.id
)
for dec_ue in self.decisions_ues.values():
if dec_ue.formsemestre.id == self.formsemestre_id:
if dec_ue.formsemestre.id == self.formsemestre.id:
dec_ue.erase()
else:
for dec_ue in self.decisions_ues.values():
......@@ -968,7 +871,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
# et autres UEs (en cas de changement d'architecture de formation depuis le jury ?)
#
for validation in ScolarFormSemestreValidation.query.filter_by(
etudid=self.etud.id, formsemestre_id=self.formsemestre_id
etudid=self.etud.id, formsemestre_id=self.formsemestre.id
):
db.session.delete(validation)
......@@ -1026,6 +929,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
for dec_rcue in self.decisions_rcue_by_niveau.values():
if dec_rcue.code_valide in CODES_RCUE_VALIDES:
for ue in (dec_rcue.rcue.ue_1, dec_rcue.rcue.ue_2):
if ue:
dec_ue = self.decisions_ues.get(ue.id)
if dec_ue:
if dec_ue.code_valide not in CODES_UE_VALIDES:
......@@ -1033,7 +937,9 @@ class DecisionsProposeesAnnee(DecisionsProposees):
f"L'UE {ue.acronyme} n'est pas validée mais son RCUE l'est !"
)
else:
messages.append(f"L'UE {ue.acronyme} n'a pas décision (???)")
messages.append(
f"L'UE {ue.acronyme} n'a pas décision (???)"
)
return messages
def valide_diplome(self) -> bool:
......@@ -1042,26 +948,25 @@ class DecisionsProposeesAnnee(DecisionsProposees):
def list_ue_parcour_etud(
formsemestre: FormSemestre, etud: Identite, res: ResultatsSemestreBUT
) -> tuple[ApcParcours, list[UniteEns]]:
"""Parcour dans lequel l'étudiant est inscrit,
et liste des UEs à valider pour ce semestre (sans les UE "dispensées")
"""
if res.etuds_parcour_id[etud.id] is None:
parcour = None
formsemestre: FormSemestre,
etud: Identite,
parcour: ApcParcours,
res: ResultatsSemestreBUT,
) -> list[UniteEns]:
"""Liste des UEs suivies ce semestre (sans les UE "dispensées")"""
if parcour is None:
# pas de parcour: prend toutes les UEs (non bonus)
ues = [ue for ue in res.etud_ues(etud.id) if ue.type == UE_STANDARD]
ues.sort(key=lambda u: u.numero)
else:
parcour = ApcParcours.query.get(res.etuds_parcour_id[etud.id])
ues = (
formsemestre.formation.query_ues_parcour(parcour)
.filter(UniteEns.semestre_idx == formsemestre.semestre_id)
.order_by(UniteEns.numero)
.all()
)
ues = [ue for ue in ues if (etud.id, ue.id) not in res.dispense_ues]
return parcour, ues
return [ue for ue in ues if (etud.id, ue.id) not in res.dispense_ues]
class DecisionsProposeesRCUE(DecisionsProposees):
......@@ -1117,8 +1022,11 @@ class DecisionsProposeesRCUE(DecisionsProposees):
self.codes.insert(0, sco_codes.AJ)
# Si au moins l'un des semestres est extérieur, propose ADM au cas où
if (
dec_prop_annee.formsemestre_impair.modalite == "EXT"
or dec_prop_annee.formsemestre_pair.modalite == "EXT"
dec_prop_annee.formsemestre_impair
and dec_prop_annee.formsemestre_impair.modalite == "EXT"
) or (
dec_prop_annee.formsemestre_pair
and dec_prop_annee.formsemestre_pair.modalite == "EXT"
):
self.codes.insert(0, sco_codes.ADM)
# S'il y a une décision enregistrée: si elle est plus favorable que celle que l'on
......@@ -1148,11 +1056,13 @@ class DecisionsProposeesRCUE(DecisionsProposees):
"""
if self.rcue is None:
return False # pas de RCUE a enregistrer
if not (self.rcue.ue_1 and self.rcue.ue_2):
return False # on n'a pas les deux UEs
if self.inscription_etat != scu.INSCRIT:
return False
if code and not code in self.codes:
raise ScoValueError(
f"code UE invalide pour ue_id={self.ue.id}: {html.escape(code)}"
f"code RCUE invalide pour {self.rcue}: {html.escape(code)}"
)
if code == self.code_valide:
self.recorded = True
......@@ -1166,7 +1076,7 @@ class DecisionsProposeesRCUE(DecisionsProposees):
else:
self.validation = ApcValidationRCUE(
etudid=self.etud.id,
formsemestre_id=self.rcue.formsemestre_2.id,
formsemestre_id=self.deca.formsemestre.id, # origine
ue1_id=self.rcue.ue_1.id,
ue2_id=self.rcue.ue_2.id,
parcours_id=parcours_id,
......@@ -1190,7 +1100,9 @@ class DecisionsProposeesRCUE(DecisionsProposees):
if dec_ue and dec_ue.code_valide not in CODES_UE_VALIDES:
log(f"rcue.record: force ADJR sur {dec_ue}")
flash(
f"""UEs du RCUE "{dec_ue.ue.niveau_competence.competence.titre}" passées en ADJR"""
f"""UEs du RCUE "{
dec_ue.ue.niveau_competence.competence.titre
}" passées en ADJR"""
)
dec_ue.record(sco_codes.ADJR)
......@@ -1198,13 +1110,13 @@ class DecisionsProposeesRCUE(DecisionsProposees):
if code in CODES_RCUE_VALIDES:
self.valide_niveau_inferieur()
if self.rcue.formsemestre_1 is not None:
if self.rcue.res_impair is not None:
sco_cache.invalidate_formsemestre(
formsemestre_id=self.rcue.formsemestre_1.id
formsemestre_id=self.rcue.res_impair.formsemestre.id
)
if self.rcue.formsemestre_2 is not None:
if self.rcue.res_pair is not None:
sco_cache.invalidate_formsemestre(
formsemestre_id=self.rcue.formsemestre_2.id
formsemestre_id=self.rcue.res_pair.formsemestre.id
)
self.code_valide = code # mise à jour état
self.recorded = True
......@@ -1236,13 +1148,13 @@ class DecisionsProposeesRCUE(DecisionsProposees):
def valide_niveau_inferieur(self) -> None:
"""Appelé juste après la validation d'un RCUE.
*La validation des deux UE du niveau dune compétence emporte la validation de
lensemble des UEs du niveau inférieur de cette même compétence.*
*La validation des deux UE du niveau d'une compétence emporte la validation de
l'ensemble des UEs du niveau inférieur de cette même compétence.*
"""
if not self.rcue or not self.rcue.ue_1 or not self.rcue.ue_1.niveau_competence:
if not self.rcue:
return
competence: ApcCompetence = self.rcue.ue_1.niveau_competence.competence
ordre_inferieur = self.rcue.ue_1.niveau_competence.ordre - 1
competence: ApcCompetence = self.rcue.niveau.competence
ordre_inferieur = self.rcue.niveau.ordre - 1
if ordre_inferieur < 1:
return # pas de niveau inferieur
......@@ -1259,43 +1171,14 @@ class DecisionsProposeesRCUE(DecisionsProposees):
if [v for v in validations_rcue if code_rcue_validant(v.code)]:
return # déjà validé
# --- Validations des UEs
ues, ue1, ue2 = self._get_ues_inferieures(competence, ordre_inferieur)
# Pour chaque UE inférieure non validée, valide:
for ue in ues:
validations_ue = ScolarFormSemestreValidation.query.filter_by(
etudid=self.etud.id, ue_id=ue.id
).all()
if [
validation
for validation in validations_ue
if sco_codes.code_ue_validant(validation.code)
]:
continue # on a déjà une validation
# aucune validation validante
validation_ue = validations_ue[0] if validations_ue else None
if validation_ue:
# Modifie validation existante
validation_ue.code = sco_codes.ADSUP
validation_ue.event_date = datetime.now()
if validation_ue.formsemestre_id is not None:
sco_cache.invalidate_formsemestre(
formsemestre_id=validation_ue.formsemestre_id
# --- Validations des UEs du niveau inférieur
self.valide_ue_inferieures(
self.rcue.semestre_id_impair, ordre_inferieur, competence
)
log(f"updating {validation_ue}")
else:
# Ajoute une validation,
# pas de formsemestre ni de note car pas une capitalisation
validation_ue = ScolarFormSemestreValidation(
etudid=self.etud.id,
code=sco_codes.ADSUP,
ue_id=ue.id,
is_external=True, # pas rattachée à un formsemestre
self.valide_ue_inferieures(
self.rcue.semestre_id_pair, ordre_inferieur, competence
)
log(f"recording {validation_ue}")
db.session.add(validation_ue)
# Valide le RCUE inférieur
# --- Valide le RCUE inférieur
if validations_rcue:
# Met à jour validation existante
validation_rcue = validations_rcue[0]
......@@ -1308,20 +1191,93 @@ class DecisionsProposeesRCUE(DecisionsProposees):
sco_cache.invalidate_formsemestre(
formsemestre_id=validation_rcue.formsemestre_id
)
elif ue1 and ue2:
else:
# Crée nouvelle validation
ue1 = self._get_ue_inferieure(
self.rcue.semestre_id_impair, ordre_inferieur, competence
)
ue2 = self._get_ue_inferieure(
self.rcue.semestre_id_pair, ordre_inferieur, competence
)
if ue1 and ue2:
validation_rcue = ApcValidationRCUE(
etudid=self.etud.id, ue1_id=ue1.id, ue2_id=ue2.id, code=sco_codes.ADSUP
etudid=self.etud.id,
ue1_id=ue1.id,
ue2_id=ue2.id,
code=sco_codes.ADSUP,
)
db.session.add(validation_rcue)
db.session.commit()
log(f"recording {validation_rcue}")
self.valide_annee_inferieure()
def _get_ue_inferieure(
self, semestre_id: int, ordre_inferieur: int, competence: ApcCompetence
) -> UniteEns:
"L'UE de la formation associée au semestre indiqué diu niveau de compétence"
return (
UniteEns.query.filter_by(
formation_id=self.deca.formsemestre.formation_id,
semestre_idx=semestre_id,
)
.join(ApcNiveau)
.filter_by(ordre=ordre_inferieur)
.join(ApcCompetence)
.filter_by(id=competence.id)
).first()
def valide_ue_inferieures(
self, semestre_id: int, ordre_inferieur: int, competence: ApcCompetence
):
"""Au besoin, enregistre une validation d'UE ADSUP pour le niveau de compétence
semestre_id : l'indice du semestre concerné (le pair ou l'impair)
"""
# Les validations d'UE impaires existantes pour ce niveau inférieur ?
validations_ues: list[ScolarFormSemestreValidation] = (
ScolarFormSemestreValidation.query.filter_by(etudid=self.etud.id)
.join(UniteEns)
.filter_by(semestre_idx=semestre_id)
.join(ApcNiveau)
.filter_by(ordre=ordre_inferieur)
.join(ApcCompetence)
.filter_by(id=competence.id)
).all()
validations_ues_validantes = [
validation
for validation in validations_ues
if sco_codes.code_ue_validant(validation.code)
]
if not validations_ues_validantes:
# Il faut créer une validation d'UE
# cherche l'UE de notre formation associée à ce niveau
# et warning si il n'y en a pas
ue = self._get_ue_inferieure(semestre_id, ordre_inferieur, competence)
if not ue:
# programme incomplet ou mal paramétré
flash(
f"""Impossible de valider l'UE inférieure du niveau {
ordre_inferieur
} de la compétence {competence.titre}
car elle n'existe pas dans la formation
""",
"warning",
)
log("valide_ue_inferieures: UE manquante dans la formation")
else:
validation_ue = ScolarFormSemestreValidation(
etudid=self.etud.id,
code=sco_codes.ADSUP,
ue_id=ue.id,
is_external=True, # pas rattachée à un formsemestre
)
db.session.add(validation_ue)
log(f"recording {validation_ue}")
def valide_annee_inferieure(self) -> None:
"""Si tous les RCUEs de l'année inférieure sont validés, la valide"""
# Indice de l'année inférieure:
annee_courante = self.rcue.ue_1.niveau_competence.annee # "BUT2"
annee_courante = self.rcue.niveau.annee # "BUT2"
if not re.match(r"^BUT\d$", annee_courante):
log("Warning: valide_annee_inferieure invalid annee_courante")
return
......@@ -1335,7 +1291,7 @@ class DecisionsProposeesRCUE(DecisionsProposees):
ordre=annee_inferieure,
)
.join(Formation)
.filter_by(formation_code=self.rcue.formsemestre_1.formation.formation_code)
.filter_by(formation_code=self.deca.formsemestre.formation.formation_code)
.all()
)
if len(validations_annee) > 1:
......@@ -1352,7 +1308,7 @@ class DecisionsProposeesRCUE(DecisionsProposees):
# Liste des niveaux à valider:
# ici on sort l'artillerie lourde
cursus: EtudCursusBUT = EtudCursusBUT(
self.etud, self.rcue.formsemestre_1.formation
self.etud, self.deca.formsemestre.formation
)
niveaux_a_valider = cursus.niveaux_by_annee[annee_inferieure]
# Pour chaque niveau, cherche validation RCUE
......@@ -1375,54 +1331,14 @@ class DecisionsProposeesRCUE(DecisionsProposees):
etudid=self.etud.id,
ordre=annee_inferieure,
code=sco_codes.ADSUP,
formation_id=self.rcue.formsemestre_1.formation_id,
# met cette validation sur l'année scolaire actuelle, pas la précédente (??)
annee_scolaire=self.rcue.formsemestre_1.annee_scolaire(),
formation_id=self.deca.formsemestre.formation_id,
# met cette validation sur l'année scolaire actuelle, pas la précédente
annee_scolaire=self.deca.formsemestre.annee_scolaire(),
)
log(f"recording {validation_annee}")
db.session.add(validation_annee)
db.session.commit()
def _get_ues_inferieures(
self, competence: ApcCompetence, ordre_inferieur: int
) -> tuple[list[UniteEns], UniteEns, UniteEns]:
"""Les UEs de cette formation associées au niveau de compétence inférieur ?
Note: on ne cherche que dans la formation courante, pas les UEs de
même code d'autres formations.
"""
formation: Formation = self.rcue.formsemestre_1.formation
ues: list[UniteEns] = (
UniteEns.query.filter_by(formation_id=formation.id)
.filter(UniteEns.semestre_idx != None)
.join(ApcNiveau)
.filter_by(ordre=ordre_inferieur)
.join(ApcCompetence)
.filter_by(id=competence.id)
.all()
)
log(f"valide_niveau_inferieur: {competence} UEs inférieures: {ues}")
if len(ues) != 2: # on n'a pas 2 UE associées au niveau inférieur !
flash(
"Impossible de valider le niveau de compétence inférieur: pas 2 UEs associées'",
"warning",
)
return [], None, None
ues_impaires = [ue for ue in ues if ue.semestre_idx % 2]
if len(ues_impaires) != 1:
flash(
"Impossible de valider le niveau de compétence inférieur: pas d'UE impaire associée"
)
return [], None, None
ue1 = ues_impaires[0]
ues_paires = [ue for ue in ues if not ue.semestre_idx % 2]
if len(ues_paires) != 1:
flash(
"Impossible de valider le niveau de compétence inférieur: pas d'UE paire associée"
)
return [], None, None
ue2 = ues_paires[0]
return ues, ue1, ue2
class DecisionsProposeesUE(DecisionsProposees):
"""Décisions de jury sur une UE du BUT
......@@ -1437,6 +1353,10 @@ class DecisionsProposeesUE(DecisionsProposees):
sinon: ADJ, AJ
et proposer toujours: RAT, DEF, ABAN, ADJR, DEM, UEBSL (codes_communs)
Le DecisionsProposeesUE peut concerner une UE du formsemestre, ou une validation
antérieure non éditable.
"""
# Codes toujours proposés sauf si include_communs est faux:
......@@ -1454,51 +1374,59 @@ class DecisionsProposeesUE(DecisionsProposees):
self,
etud: Identite,
formsemestre: FormSemestre,
ue: UniteEns,
rcue: RegroupementCoherentUE = None,
paire: bool = False,
inscription_etat: str = scu.INSCRIT,
):
# Une UE peut être validée plusieurs fois en cas de redoublement (qu'elle soit capitalisée ou non)
self.paire = paire
self.ue: UniteEns = rcue.ue_2 if paire else rcue.ue_1
self.inscription_etat = inscription_etat
# 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)
validation = ScolarFormSemestreValidation.query.filter_by(
etudid=etud.id, formsemestre_id=formsemestre.id, ue_id=ue.id
etudid=etud.id, formsemestre_id=formsemestre.id, ue_id=self.ue.id << XXX
).first()
super().__init__(
etud=etud,
code_valide=validation.code if validation is not None else None,
)
self.validation = validation
"validation dans le formsemestre courant"
self.formsemestre = formsemestre
self.ue: UniteEns = ue
self.rcue: RegroupementCoherentUE = None
self.rcue: RegroupementCoherentUE = rcue
"Le rcue auquel est rattaché cette UE, ou None"
self.inscription_etat = inscription_etat
"inscription: I, DEM, DEF dans le semestre de cette UE"
# Editable ou pas ?
# si ue courante, éditable.
self.editable = (
(self.rcue.ue_cur_pair is not None)
if paire
else (self.rcue.ue_cur_impair is not None)
)
res: ResultatsSemestreBUT = (
self.rcue.res_pair if paire else self.rcue.res_impair
)
self.moy_ue = np.NaN
self.moy_ue_with_cap = np.NaN
self.ue_status = {}
if ue.type == sco_codes.UE_SPORT:
self.explanation = "UE bonus, pas de décision de jury"
if self.ue.type != sco_codes.UE_STANDARD:
self.explanation = "UE non standard, pas de décision de jury BUT"
self.codes = [] # aucun code proposé
return
if inscription_etat != scu.INSCRIT:
if res and res.get_etud_etat(etud.id) != scu.INSCRIT:
self.validation = None # cache toute validation
self.explanation = "non incrit (dem. ou déf.)"
self.explanation = "non inscrit (dem. ou déf.)"
self.codes = [
sco_codes.DEM if inscription_etat == scu.DEMISSION else sco_codes.DEF
sco_codes.DEM
if res.get_etud_etat(etud.id) == scu.DEMISSION
else sco_codes.DEF
]
return
# Moyenne de l'UE ?
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre)
# Safety checks:
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
ue_status = res.get_etud_ue_status(etud.id, ue.id)
ue_status = self.rcue.ue_status_pair if paire else self.rcue.ue_status_impair
self.moy_ue = ue_status["cur_moy_ue"]
self.moy_ue_with_cap = ue_status["moy"]
self.ue_status = ue_status
......@@ -1508,11 +1436,6 @@ class DecisionsProposeesUE(DecisionsProposees):
return f"""<{self.__class__.__name__} ue={self.ue.acronyme} valid={self.code_valide
} codes={self.codes} explanation={self.explanation}>"""
def set_rcue(self, rcue: RegroupementCoherentUE):
"""Rattache cette UE à un RCUE. Cela peut modifier les codes
proposés par compute_codes() (si compensation)"""
self.rcue = rcue
def compute_codes(self):
"""Calcul des .codes attribuables et de l'explanation associée"""
if self.inscription_etat != scu.INSCRIT:
......@@ -1607,91 +1530,91 @@ class DecisionsProposeesUE(DecisionsProposees):
return 0.0
class BUTCursusEtud: # WIP TODO
"""Validation du cursus d'un étudiant"""
def __init__(self, formsemestre: FormSemestre, etud: Identite):
if formsemestre.formation.referentiel_competence is None:
raise ScoNoReferentielCompetences(formation=formsemestre.formation)
assert len(etud.formsemestre_inscriptions) > 0
self.formsemestre = formsemestre
self.etud = etud
#
# La dernière inscription en date va donner le parcours (donc les compétences à valider)
self.last_inscription = sorted(
etud.formsemestre_inscriptions, key=attrgetter("formsemestre.date_debut")
)[-1]
def est_diplomable(self) -> bool:
"""Vrai si toutes les compétences sont validables"""
return all(
self.competence_validable(competence)
for competence in self.competences_du_parcours()
)
def est_annee_validee(self, ordre: int) -> bool:
"""Vrai si l'année BUT ordre est validée"""
# On cherche les validations d'annee avec le même
# code formation que nous.
return (
ApcValidationAnnee.query.filter_by(
etudid=self.etud.id,
ordre=ordre,
)
.join(Formation)
.filter(
Formation.formation_code == self.formsemestre.formation.formation_code
)
.count()
> 0
)
def est_diplome(self) -> bool:
"""Vrai si BUT déjà validé"""
# vrai si la troisième année est validée
return self.est_annee_validee(3)
def competences_du_parcours(self) -> list[ApcCompetence]:
"""Construit liste des compétences du parcours, qui doivent être
validées pour obtenir le diplôme.
Le parcours est celui de la dernière inscription.
"""
parcour = self.last_inscription.parcour
query = self.formsemestre.formation.formation.query_competences_parcour(parcour)
if query is None:
return []
return query.all()
def competence_validee(self, competence: ApcCompetence) -> bool:
"""Vrai si la compétence est validée, c'est à dire que tous ses
niveaux sont validés (ApcValidationRCUE).
"""
# XXX A REVOIR
validations = (
ApcValidationRCUE.query.filter_by(etudid=self.etud.id)
.join(UniteEns, UniteEns.id == ApcValidationRCUE.ue1_id)
.join(ApcNiveau, ApcNiveau.id == UniteEns.niveau_competence_id)
.join(ApcCompetence, ApcCompetence.id == ApcNiveau.competence_id)
)
def competence_validable(self, competence: ApcCompetence):
"""Vrai si la compétence est "validable" automatiquement, c'est à dire
que les conditions de notes sont satisfaites pour l'acquisition de
son niveau le plus élevé, qu'il ne manque que l'enregistrement de la décision.
En vertu de la règle "La validation des deux UE du niveau dune compétence
emporte la validation de l'ensemble des UE du niveau inférieur de cette
même compétence.",
il suffit de considérer le dernier niveau dans lequel l'étudiant est inscrit.
"""
pass
def ues_emportees(self, niveau: ApcNiveau) -> list[tuple[FormSemestre, UniteEns]]:
"""La liste des UE à valider si on valide ce niveau.
Ne liste que les UE qui ne sont pas déjà acquises.
Selon la règle donnée par l'arrêté BUT:
* La validation des deux UE du niveau dune compétence emporte la validation de
l'ensemble des UE du niveau inférieur de cette même compétence.
"""
pass
# class BUTCursusEtud: # WIP TODO
# """Validation du cursus d'un étudiant"""
# def __init__(self, formsemestre: FormSemestre, etud: Identite):
# if formsemestre.formation.referentiel_competence is None:
# raise ScoNoReferentielCompetences(formation=formsemestre.formation)
# assert len(etud.formsemestre_inscriptions) > 0
# self.formsemestre = formsemestre
# self.etud = etud
# #
# # La dernière inscription en date va donner le parcours (donc les compétences à valider)
# self.last_inscription = sorted(
# etud.formsemestre_inscriptions, key=attrgetter("formsemestre.date_debut")
# )[-1]
# def est_diplomable(self) -> bool:
# """Vrai si toutes les compétences sont validables"""
# return all(
# self.competence_validable(competence)
# for competence in self.competences_du_parcours()
# )
# def est_annee_validee(self, ordre: int) -> bool:
# """Vrai si l'année BUT ordre est validée"""
# # On cherche les validations d'annee avec le même
# # code formation que nous.
# return (
# ApcValidationAnnee.query.filter_by(
# etudid=self.etud.id,
# ordre=ordre,
# )
# .join(Formation)
# .filter(
# Formation.formation_code == self.formsemestre.formation.formation_code
# )
# .count()
# > 0
# )
# def est_diplome(self) -> bool:
# """Vrai si BUT déjà validé"""
# # vrai si la troisième année est validée
# return self.est_annee_validee(3)
# def competences_du_parcours(self) -> list[ApcCompetence]:
# """Construit liste des compétences du parcours, qui doivent être
# validées pour obtenir le diplôme.
# Le parcours est celui de la dernière inscription.
# """
# parcour = self.last_inscription.parcour
# query = self.formsemestre.formation.formation.query_competences_parcour(parcour)
# if query is None:
# return []
# return query.all()
# def competence_validee(self, competence: ApcCompetence) -> bool:
# """Vrai si la compétence est validée, c'est à dire que tous ses
# niveaux sont validés (ApcValidationRCUE).
# """
# # XXX A REVOIR
# validations = (
# ApcValidationRCUE.query.filter_by(etudid=self.etud.id)
# .join(UniteEns, UniteEns.id == ApcValidationRCUE.ue1_id)
# .join(ApcNiveau, ApcNiveau.id == UniteEns.niveau_competence_id)
# .join(ApcCompetence, ApcCompetence.id == ApcNiveau.competence_id)
# )
# def competence_validable(self, competence: ApcCompetence):
# """Vrai si la compétence est "validable" automatiquement, c'est à dire
# que les conditions de notes sont satisfaites pour l'acquisition de
# son niveau le plus élevé, qu'il ne manque que l'enregistrement de la décision.
# En vertu de la règle "La validation des deux UE du niveau d'une compétence
# emporte la validation de l'ensemble des UE du niveau inférieur de cette
# même compétence.",
# il suffit de considérer le dernier niveau dans lequel l'étudiant est inscrit.
# """
# pass
# def ues_emportees(self, niveau: ApcNiveau) -> list[tuple[FormSemestre, UniteEns]]:
# """La liste des UE à valider si on valide ce niveau.
# Ne liste que les UE qui ne sont pas déjà acquises.
# Selon la règle donnée par l'arrêté BUT:
# * La validation des deux UE du niveau d'une compétence emporte la validation de
# l'ensemble des UE du niveau inférieur de cette même compétence.
# """
# pass
......@@ -153,7 +153,7 @@ def pvjury_table_but(
etudid=etud.id,
),
"cursus": _descr_cursus_but(etud),
"ects": f"{deca.formsemestre_ects():g}",
"ects": f"{deca.ects_annee():g}",
"ues": deca.descr_ues_validation(line_sep=line_sep) if deca else "-",
"niveaux": deca.descr_niveaux_validation(line_sep=line_sep)
if deca
......
......@@ -48,9 +48,9 @@ def _get_jury_but_etud_result(
# --- Les RCUEs
rcue_list = []
if deca:
for rcue in deca.rcues_annee:
dec_rcue = deca.dec_rcue_by_ue.get(rcue.ue_1.id)
if dec_rcue is not None: # None si l'UE n'est pas associée à un niveau
for dec_rcue in deca.get_decisions_rcues_annee():
rcue = dec_rcue.rcue
if rcue.complete: # n'exporte que les RCUEs complets
dec_ue1 = deca.decisions_ues[rcue.ue_1.id]
dec_ue2 = deca.decisions_ues[rcue.ue_2.id]
rcue_dict = {
......
......@@ -93,35 +93,25 @@ def show_etud(deca: DecisionsProposeesAnnee, read_only: bool = True) -> str:
<div class="titre">RCUE</div>
"""
)
for niveau in deca.niveaux_competences:
for dec_rcue in deca.get_decisions_rcues_annee():
rcue = dec_rcue.rcue
niveau = rcue.niveau
H.append(
f"""<div class="but_niveau_titre">
<div title="{niveau.competence.titre_long}">{niveau.competence.titre}</div>
</div>"""
)
dec_rcue = deca.decisions_rcue_by_niveau.get(niveau.id) # peut être None
ues = [
ue
for ue in deca.ues_impair
if ue.niveau_competence and ue.niveau_competence.id == niveau.id
]
ue_impair = ues[0] if ues else None
ues = [
ue
for ue in deca.ues_pair
if ue.niveau_competence and ue.niveau_competence.id == niveau.id
]
ue_pair = ues[0] if ues else None
ue_impair, ue_pair = rcue.ue_1, rcue.ue_2
# Les UEs à afficher,
# qui seront toujours en readonly sur le formsemestre de l'année précédente du redoublant
# qui
ues_ro = [
(
ue_impair,
(deca.a_cheval and deca.formsemestre_id != deca.formsemestre_impair.id),
rcue.ue_cur_impair is None,
),
(
ue_pair,
deca.a_cheval and deca.formsemestre_id != deca.formsemestre_pair.id,
rcue.ue_cur_pair is None,
),
]
# Ordonne selon les dates des 2 semestres considérés:
......@@ -155,12 +145,13 @@ def _gen_but_select(
code_valide: str,
disabled: bool = False,
klass: str = "",
data: dict = {},
data: dict = None,
code_valide_label: str = "",
) -> str:
"Le menu html select avec les codes"
# if disabled: # mauvaise idée car le disabled est traité en JS
# return f"""<div class="but_code {klass}">{code_valide}</div>"""
data = data or {}
options_htm = "\n".join(
[
f"""<option value="{code}"
......@@ -220,8 +211,14 @@ def _gen_but_niveau_ue(
else:
scoplement = ""
return f"""<div class="but_niveau_ue {
'recorded' if dec_ue.code_valide is not None else ''}
ue_class = "" # 'recorded' if dec_ue.code_valide is not None else ''
if dec_ue.code_valide is not None and dec_ue.codes:
if dec_ue.code_valide == dec_ue.codes[0]:
ue_class = "recorded"
else:
ue_class = "recorded_different"
return f"""<div class="but_niveau_ue {ue_class}
{'annee_prec' if annee_prec else ''}
">
<div title="{ue.titre}">{ue.acronyme}</div>
......@@ -242,7 +239,7 @@ def _gen_but_niveau_ue(
def _gen_but_rcue(dec_rcue: DecisionsProposeesRCUE, niveau: ApcNiveau) -> str:
if dec_rcue is None:
if dec_rcue is None or not dec_rcue.rcue.complete:
return """
<div class="but_niveau_rcue niveau_vide with_scoplement">
<div></div>
......
......@@ -4,7 +4,6 @@
"""
from typing import Union
from flask_sqlalchemy.query import Query
from app import db
from app.models import CODE_STR_LEN
......@@ -13,8 +12,6 @@ from app.models.etudiants import Identite
from app.models.formations import Formation
from app.models.formsemestre import FormSemestre
from app.models.ues import UniteEns
from app.scodoc import codes_cursus as sco_codes
from app.scodoc import sco_utils as scu
class ApcValidationRCUE(db.Model):
......@@ -22,7 +19,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
Le formsemestre est l'origine, utilisé pour effacer
"""
__tablename__ = "apc_validation_rcue"
......@@ -109,139 +106,6 @@ 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*.
La moyenne (10/20) au RCUE déclenche la compensation des UE.
"""
def __init__(
self,
etud: Identite,
formsemestre_1: FormSemestre,
dec_ue_1: "DecisionsProposeesUE",
formsemestre_2: FormSemestre,
dec_ue_2: "DecisionsProposeesUE",
inscription_etat: str,
):
ue_1 = dec_ue_1.ue
ue_2 = dec_ue_2.ue
# 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
if inscription_etat != scu.INSCRIT:
self.moy_rcue = None
self.moy_ue_1 = self.moy_ue_2 = "-"
self.moy_ue_1_val = self.moy_ue_2_val = 0.0
return
self.moy_ue_1 = dec_ue_1.moy_ue_with_cap
self.moy_ue_1_val = self.moy_ue_1 if self.moy_ue_1 is not None else 0.0
self.moy_ue_2 = dec_ue_2.moy_ue_with_cap
self.moy_ue_2_val = self.moy_ue_2 if self.moy_ue_2 is not None else 0.0
# Calcul de la moyenne au RCUE (utilise les moy d'UE capitalisées)
if (self.moy_ue_1 is not None) and (self.moy_ue_2 is not None):
# Moyenne RCUE (les pondérations par défaut sont 1.)
self.moy_rcue = (
self.moy_ue_1 * ue_1.coef_rcue + self.moy_ue_2 * ue_2.coef_rcue
) / (ue_1.coef_rcue + ue_2.coef_rcue)
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 __str__(self) -> str:
return f"""RCUE {
self.ue_1.acronyme}({self.moy_ue_1}) + {
self.ue_2.acronyme}({self.moy_ue_2})"""
def query_validations(
self,
) -> Query: # 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_competence_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_compensable(self):
"""Vrai si ce RCUE est validable (uniquement) par compensation
c'est à dire que sa moyenne est > 10 avec une UE < 10.
Note: si ADM, est_compensable est faux.
"""
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 RCUE satisfait les conditions pour être validé,
c'est à dire que la moyenne des UE qui le constituent 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.CODES_RCUE_VALIDES
):
return validation
return None
# unused
# def find_rcues(
# formsemestre: FormSemestre, ue: UniteEns, etud: Identite, inscription_etat: str
......@@ -319,7 +183,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"
"le semestre origine, normalement l'IMPAIR (le 1er) de l'année"
formation_id = db.Column(
db.Integer,
db.ForeignKey("notes_formations.id"),
......
......@@ -265,11 +265,8 @@ class ScolarNews(db.Model):
# Informations générales
H.append(
f"""<div>
Pour être informé des évolutions de ScoDoc,
vous pouvez vous
<a class="stdlink" href="{scu.SCO_ANNONCES_WEBSITE}">
abonner à la liste de diffusion</a>.
f"""<div><a class="discretelink" href="{scu.SCO_ANNONCES_WEBSITE}">
Pour en savoir plus sur ScoDoc voir le site scodoc.org</a>.
</div>
"""
)
......
......@@ -68,7 +68,7 @@ class ScolarFormSemestreValidation(db.Model):
if self.ue_id:
# Note: si l'objet vient d'être créé, ue_id peut exister mais pas ue !
return f"""décision sur UE {self.ue.acronyme if self.ue else self.ue_id
}: {self.code}"""
} ({self.ue_id}): {self.code}"""
return f"""décision sur semestre {self.formsemestre.titre_mois()} du {
self.event_date.strftime("%d/%m/%Y")}"""
......
......@@ -793,7 +793,13 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
{tf[1]}
"""
elif tf[0] == -1:
return "<h4>annulation</h4>"
return redirect(
url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
)
)
else:
if tf[2]["gestion_compensation_lst"]:
tf[2]["gestion_compensation"] = True
......
......@@ -349,7 +349,7 @@ SCO_DEFAULT_SQL_USERS_CNX = "dbname=SCOUSERS port=%s" % SCO_DEFAULT_SQL_PORT
# Valeurs utilisées pour affichage seulement, pas de requetes ni de mails envoyés:
SCO_WEBSITE = "https://scodoc.org"
SCO_USER_MANUAL = "https://scodoc.org/GuideUtilisateur"
SCO_ANNONCES_WEBSITE = "https://listes.univ-paris13.fr/mailman/listinfo/scodoc-annonces"
SCO_ANNONCES_WEBSITE = "https://scodoc.org/Contact"
SCO_DEVEL_LIST = "scodoc-devel@listes.univ-paris13.fr"
SCO_USERS_LIST = "notes@listes.univ-paris13.fr"
SCO_LISTS_URL = "https://scodoc.org/Contact"
......
// JS for all ScoDoc pages (using jQuery UI)
$(function () {
// Autocomplete recherche etudiants par nom
$(".in-expnom").autocomplete(
{
$(".in-expnom").autocomplete({
delay: 300, // wait 300ms before suggestions
minLength: 2, // min nb of chars before suggest
position: { collision: 'flip' }, // automatic menu position up/down
position: { collision: "flip" }, // automatic menu position up/down
source: SCO_URL + "/search_etud_by_name",
select: function (event, ui) {
$(".in-expnom").val(ui.item.value);
$("#form-chercheetud").submit();
}
},
});
// Date picker
$(".datepicker").datepicker({
showOn: 'button',
buttonImage: '/ScoDoc/static/icons/calendar_img.png',
showOn: "button",
buttonImage: "/ScoDoc/static/icons/calendar_img.png",
buttonImageOnly: true,
dateFormat: 'dd/mm/yy',
duration: 'fast',
dateFormat: "dd/mm/yy",
duration: "fast",
});
$('.datepicker').datepicker('option', $.extend({ showMonthAfterYear: false },
$.datepicker.regional['fr']));
$(".datepicker").datepicker(
"option",
$.extend({ showMonthAfterYear: false }, $.datepicker.regional["fr"])
);
/* Barre menu */
var sco_menu_position = { my: "left top", at: "left bottom" };
$("#sco_menu").menu({
$("#sco_menu")
.menu({
position: sco_menu_position,
blur: function () {
$(this).menu("option", "position", sco_menu_position);
},
focus: function (e, ui) {
if ($("#sco_menu").get(0) !== $(ui).get(0).item.parent().get(0)) {
$(this).menu("option", "position", { my: "left top", at: "right top" });
}
$(this).menu("option", "position", {
my: "left top",
at: "right top",
});
}
}).mouseleave(function (x, y) {
$("#sco_menu").menu('collapseAll');
},
})
.mouseleave(function (x, y) {
$("#sco_menu").menu("collapseAll");
});
$("#sco_menu > li > a > span").switchClass("ui-icon-carat-1-e", "ui-icon-carat-1-s");
$("#sco_menu > li > a > span").switchClass(
"ui-icon-carat-1-e",
"ui-icon-carat-1-s"
);
/* Les menus isoles dropdown */
$(".sco_dropdown_menu").menu({
position: sco_menu_position
}).mouseleave(function (x, y) {
$(".sco_dropdown_menu").menu('collapseAll');
}
$(".sco_dropdown_menu")
.menu({
position: sco_menu_position,
})
.mouseleave(function (x, y) {
$(".sco_dropdown_menu").menu("collapseAll");
});
$(".sco_dropdown_menu > li > a > span").switchClass(
"ui-icon-carat-1-e",
"ui-icon-carat-1-s"
);
$(".sco_dropdown_menu > li > a > span").switchClass("ui-icon-carat-1-e", "ui-icon-carat-1-s");
/* up-to-date status */
var update_div = document.getElementById("update_warning");
if (update_div) {
fetch('install_info').then(
response => response.text()
).then(text => {
fetch("install_info")
.then((response) => response.text())
.then((text) => {
update_div.innerHTML = text;
if (text) {
update_div.style.display = "block";
......@@ -80,58 +92,60 @@ function sco_message(msg, className = "message_custom", duration = 0) {
if (duration) {
setTimeout(() => {
div.remove();
}, 3000);
}, 8000);
}
}
function sco_error_message(msg) {
sco_message(msg, className = "message_error", duration = 0);
sco_message(msg, (className = "message_error"), (duration = 0));
}
function get_query_args() {
var s = window.location.search; // eg "?x=1&y=2"
var vars = {};
s.replace(
/[?&]+([^=&]+)=?([^&]*)?/gi, // regexp
function (m, key, value) { // callback
vars[key] = value !== undefined ? value : '';
function (m, key, value) {
// callback
vars[key] = value !== undefined ? value : "";
}
);
return vars;
}
// Tables (gen_tables)
$(function () {
if ($('table.gt_table').length > 0) {
if ($("table.gt_table").length > 0) {
var table_options = {
"paging": false,
"searching": false,
"info": false,
paging: false,
searching: false,
info: false,
/* "autoWidth" : false, */
"fixedHeader": {
"header": true,
"footer": true
fixedHeader: {
header: true,
footer: true,
},
"orderCellsTop": true, // cellules ligne 1 pour tri
"aaSorting": [], // Prevent initial sorting
orderCellsTop: true, // cellules ligne 1 pour tri
aaSorting: [], // Prevent initial sorting
};
$('table.gt_table').DataTable(table_options);
$("table.gt_table").DataTable(table_options);
table_options["searching"] = true;
$('table.gt_table_searchable').DataTable(table_options);
$("table.gt_table_searchable").DataTable(table_options);
}
});
// Show tags (readonly)
function readOnlyTags(nodes) {
// nodes are textareas, hide them and create a span showing tags
for (var i = 0; i < nodes.length; i++) {
var node = $(nodes[i]);
node.hide();
var tags = nodes[i].value.split(',');
node.after('<span class="ro_tags"><span class="ro_tag">' + tags.join('</span><span class="ro_tag">') + '</span></span>');
var tags = nodes[i].value.split(",");
node.after(
'<span class="ro_tags"><span class="ro_tag">' +
tags.join('</span><span class="ro_tag">') +
"</span></span>"
);
}
}
......@@ -164,7 +178,8 @@ class ScoFieldEditor {
// id: ${obj.dataset.oid}
// `);
$.post(this.save_url,
$.post(
this.save_url,
{
oid: obj.dataset.oid,
value: value,
......@@ -185,11 +200,19 @@ class ScoFieldEditor {
}
document.body.addEventListener("keydown", this.key);
let editor = this;
this.handleSelectCell = (event) => { editor.selectCell(event) };
this.handleModifCell = (event) => { editor.modifCell(event) };
this.handleBlur = (event) => { editor.blurCell(event) };
this.handleKeyCell = (event) => { editor.keyCell(event) };
document.querySelectorAll(this.selector).forEach(cellule => {
this.handleSelectCell = (event) => {
editor.selectCell(event);
};
this.handleModifCell = (event) => {
editor.modifCell(event);
};
this.handleBlur = (event) => {
editor.blurCell(event);
};
this.handleKeyCell = (event) => {
editor.keyCell(event);
};
document.querySelectorAll(this.selector).forEach((cellule) => {
cellule.addEventListener("click", this.handleSelectCell);
cellule.addEventListener("dblclick", this.handleModifCell);
cellule.addEventListener("blur", this.handleBlur);
......@@ -224,7 +247,9 @@ class ScoFieldEditor {
}
}
unselectCell() {
document.querySelectorAll(".sco_selected, .sco_modifying").forEach(cellule => {
document
.querySelectorAll(".sco_selected, .sco_modifying")
.forEach((cellule) => {
cellule.classList.remove("sco_selected", "sco_modifying");
cellule.removeAttribute("contentEditable");
cellule.removeEventListener("keydown", this.handleKeyCell);
......@@ -254,7 +279,7 @@ class ScoFieldEditor {
event.preventDefault();
event.stopPropagation();
if (!this.save(obj)) {
return
return;
}
obj.classList.remove("sco_modifying");
// ArrowMove(0, 1);
......@@ -267,7 +292,7 @@ class ScoFieldEditor {
function getCurrentScriptPath() {
// Get all the script elements on the page
var scripts = document.getElementsByTagName('script');
var scripts = document.getElementsByTagName("script");
// Find the last script element (which is the currently executing script)
var currentScript = scripts[scripts.length - 1];
......@@ -279,13 +304,13 @@ function getCurrentScriptPath() {
}
function removeLastTwoComponents(path) {
// Split the path into individual components
var components = path.split('/');
var components = path.split("/");
// Remove the last two components (filename and enclosing directory)
components.splice(-2);
// Join the remaining components back into a path
var newPath = components.join('/');
var newPath = components.join("/");
return newPath;
}
......@@ -74,9 +74,8 @@ class TableJury(TableRecap):
self.freq_codes_annuels[deca.code_valide] += 1
row.add_nb_rcues_cell()
# --- Les RCUEs
for rcue in deca.rcues_annee:
dec_rcue = deca.dec_rcue_by_ue.get(rcue.ue_1.id)
if dec_rcue is not None: # None si l'UE n'est pas associée à un niveau
for dec_rcue in deca.get_decisions_rcues_annee():
if dec_rcue.rcue.complete:
row.add_rcue_cols(dec_rcue)
self.freq_codes_annuels["total"] = len(self.rows)
......@@ -205,7 +204,7 @@ class RowJury(RowRecap):
else:
classes.append("moy_ue_valid")
if len(deca.rcues_annee) > 0:
if len(deca.get_decisions_rcues_annee()) > 0:
# permet un tri par nb de niveaux validables + moyenne gen indicative S_pair
if deca.res_pair and deca.etud.id in deca.res_pair.etud_moy_gen:
moy = deca.res_pair.etud_moy_gen[deca.etud.id]
......@@ -260,9 +259,11 @@ class RowJury(RowRecap):
def add_rcue_cols(self, dec_rcue: DecisionsProposeesRCUE):
"2 cells: moyenne du RCUE, code enregistré"
self.table.group_titles["rcue"] = "RCUEs en cours"
rcue = dec_rcue.rcue
col_id = f"moy_rcue_{rcue.ue_1.niveau_competence_id}" # le niveau_id
if not rcue.complete:
return
col_id = f"moy_rcue_{rcue.niveau.id}" # le niveau_id
self.table.group_titles["rcue"] = "RCUEs en cours"
note_class = ""
val = rcue.moy_rcue
if isinstance(val, float):
......
......@@ -130,6 +130,12 @@
<td class="amue">CODJ</td>
<td>Acquis par décision du jury</td>
</tr>
<tr>
<td>ADSUP</td>
<td>{{codes["ADSUP"]}}</td>
<td class="amue"></td>
<td>Acquis parce que le niveau de compétence supérieur est acquis</td>
</tr>
<tr>
<td>AJ</td>
<td>{{codes["AJ"]}}</td>
......@@ -200,6 +206,12 @@
<td class="amue"></td>
<td>Acquis par décision de jury sur le RCUE (ECTS acquis)</td>
</tr>
<tr>
<td>ADSUP</td>
<td>{{codes["ADSUP"]}}</td>
<td class="amue"></td>
<td>Acquis parce que le niveau de compétence supérieur est acquis</td>
</tr>
<tr>
<td>AJ</td>
<td>{{codes["AJ"]}}</td>
......
......@@ -13,6 +13,7 @@
<ul>
<li>Semestre: {{ue.semestre_idx}}</li>
<li>Code: <tt>{{ue.ue_code}}</tt></li>
<li>ECTS: <b>{{ue.ects or 0}}</b></li>
<li>Type: {{ue.type}}</li>
<li>Externe: {{ "oui" if ue.is_external else "non" }}</li>
<li>Code Apogée: {{ue.code_apogee or "aucun"}}</li>
......
......@@ -2430,7 +2430,7 @@ def formsemestre_validation_but(
)
deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre)
if len(deca.rcues_annee) == 0:
if len(deca.get_decisions_rcues_annee()) == 0:
return jury_but_view.jury_but_semestriel(
formsemestre, etud, read_only, navigation_div=navigation_div
)
......@@ -2459,22 +2459,25 @@ def formsemestre_validation_but(
warning = ""
if len(deca.niveaux_competences) != len(deca.decisions_rcue_by_niveau):
if deca.a_cheval:
warning += """<div class="warning">Attention: regroupements RCUE
entre années (redoublement).</div>"""
else:
warning += f"""<div class="warning">Attention: {len(deca.niveaux_competences)}
niveaux mais {len(deca.decisions_rcue_by_niveau)} regroupements RCUE.</div>"""
if (deca.parcour is None) and len(formsemestre.parcours) > 0:
warning += (
"""<div class="warning">L'étudiant n'est pas inscrit à un parcours.</div>"""
)
if deca.formsemestre_impair and deca.inscription_etat_impair != scu.INSCRIT:
etat_ins = scu.ETATS_INSCRIPTION.get(deca.inscription_etat_impair, "inconnu?")
if deca.formsemestre_impair:
inscription = deca.formsemestre_impair.etuds_inscriptions.get(etud.id)
if (not inscription) or inscription.etat != scu.INSCRIT:
etat_ins = scu.ETATS_INSCRIPTION.get(inscription.etat, "inconnu?")
warning += f"""<div class="warning">{etat_ins} en S{deca.formsemestre_impair.semestre_id}</div>"""
if deca.formsemestre_pair and deca.inscription_etat_pair != scu.INSCRIT:
etat_ins = scu.ETATS_INSCRIPTION.get(deca.inscription_etat_pair, "inconnu?")
if deca.formsemestre_pair:
inscription = deca.formsemestre_pair.etuds_inscriptions.get(etud.id)
if (not inscription) or inscription.etat != scu.INSCRIT:
etat_ins = scu.ETATS_INSCRIPTION.get(inscription.etat, "inconnu?")
warning += f"""<div class="warning">{etat_ins} en S{deca.formsemestre_pair.semestre_id}</div>"""
if deca.has_notes_en_attente():
warning += f"""<div class="warning">{etud.nomprenom} a des notes en ATTente.
Vous devriez régler cela avant de statuer en jury !</div>"""
......@@ -2531,7 +2534,7 @@ def formsemestre_validation_but(
else:
erase_span = f"""<a href="{
url_for("notes.formsemestre_jury_but_erase",
scodoc_dept=g.scodoc_dept, formsemestre_id=deca.formsemestre_id,
scodoc_dept=g.scodoc_dept, formsemestre_id=deca.formsemestre.id,
etudid=deca.etud.id)}" class="stdlink"
title="efface décisions issues des jurys de cette année"
>effacer décisions de ce jury</a>
......@@ -2564,15 +2567,7 @@ def formsemestre_validation_but(
)
H.append(navigation_div)
H.append("</form>")
if deca.a_cheval:
H.append(
f"""<div class="but_doc_codes but_warning_rcue_cap">
{scu.EMO_WARNING} Rappel: pour les redoublants, seules les UE <b>capitalisées</b> (note > 10)
lors d'une année précédente peuvent être prise en compte pour former
un RCUE (associé à un niveau de compétence du BUT).
</div>
"""
)
# Affichage cursus BUT
but_cursus = cursus_but.EtudCursusBUT(etud, deca.formsemestre.formation)
H += [
......@@ -2595,7 +2590,14 @@ def formsemestre_validation_but(
codes=ScoDocSiteConfig.get_codes_apo_dict(),
)
)
H.append(
f"""<div class="but_doc_codes but_warning_rcue_cap">
{scu.EMO_WARNING} Rappel: pour les redoublants, seules les UE <b>capitalisées</b> (note > 10)
lors d'une année précédente peuvent être prise en compte pour former
un RCUE (associé à un niveau de compétence du BUT).
</div>
"""
)
return "\n".join(H) + html_sco_header.sco_footer()
......
# -*- mode: python -*-
# -*- coding: utf-8 -*-
SCOVERSION = "9.4.93"
SCOVERSION = "9.4.94"
SCONAME = "ScoDoc"
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment