BUT: jury inter-année pour les redoublants

This commit is contained in:
Emmanuel Viennet 2022-12-20 15:20:00 -03:00
parent 1324d93b80
commit 5488475416
5 changed files with 121 additions and 55 deletions

View File

@ -91,7 +91,7 @@ from app.models.ues import UniteEns
from app.models.validations import ScolarFormSemestreValidation from app.models.validations import ScolarFormSemestreValidation
from app.scodoc import sco_cache from app.scodoc import sco_cache
from app.scodoc import sco_codes_parcours as sco_codes from app.scodoc import sco_codes_parcours as sco_codes
from app.scodoc.sco_codes_parcours import RED, UE_STANDARD from app.scodoc.sco_codes_parcours import CODES_UE_VALIDES, RED, UE_STANDARD
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
from app.scodoc.sco_exceptions import ScoException, ScoValueError from app.scodoc.sco_exceptions import ScoException, ScoValueError
@ -205,6 +205,8 @@ class DecisionsProposeesAnnee(DecisionsProposees):
formsemestre: FormSemestre, formsemestre: FormSemestre,
): ):
super().__init__(etud=etud) super().__init__(etud=etud)
self.formsemestre = formsemestre
"le formsemestre utilisé pour construire ce deca"
self.formsemestre_id = formsemestre.id self.formsemestre_id = formsemestre.id
"l'id du formsemestre utilisé pour construire ce deca" "l'id du formsemestre utilisé pour construire ce deca"
formsemestre_impair, formsemestre_pair = self.comp_formsemestres(formsemestre) formsemestre_impair, formsemestre_pair = self.comp_formsemestres(formsemestre)
@ -219,23 +221,34 @@ class DecisionsProposeesAnnee(DecisionsProposees):
) )
) )
) )
# 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"
# Si on part d'un semestre IMPAIR, il n'y aura pas de décision année proposée # 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) # (mais on pourra évidemment valider des UE et même des RCUE)
self.jury_annuel: bool = formsemestre.semestre_id in (2, 4, 6) self.jury_annuel: bool = formsemestre.semestre_id in (2, 4, 6)
"vrai si jury de fin d'année scolaire (propose code annuel)" "vrai si jury de fin d'année scolaire (propose code annuel)"
self.formsemestre_impair = formsemestre_impair self.formsemestre_impair = formsemestre_impair
"le 1er semestre de l'année scolaire considérée (S1, S3, S5)" "le 1er semestre du groupement (S1, S3, S5)"
self.formsemestre_pair = formsemestre_pair self.formsemestre_pair = formsemestre_pair
"le second formsemestre de la même année scolaire (S2, S4, S6)" "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 formsemestre_last = formsemestre_pair or formsemestre_impair
"le formsemestre le plus avancé dans cette année" "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_last.semestre_id + 1) // 2
"le rang de l'année dans le BUT: 1, 2, 3" "le rang de l'année dans le BUT: 1, 2, 3"
assert self.annee_but in (1, 2, 3) assert self.annee_but in (1, 2, 3)
self.rcues_annee = [] self.rcues_annee = []
"RCUEs de l'année" """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) 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)" "état de l'inscription dans le semestre le plus avancé (pair si année complète)"
self.inscription_etat_pair = ( self.inscription_etat_pair = (
@ -526,9 +539,9 @@ class DecisionsProposeesAnnee(DecisionsProposees):
def compute_rcues_annee(self) -> list[RegroupementCoherentUE]: def compute_rcues_annee(self) -> list[RegroupementCoherentUE]:
"""Liste des regroupements d'UE à considérer cette année. """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). 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. Si on n'a pas les deux semestres, aucun RCUE.
Raises ScoValueError s'il y a des UE sans RCUE. Raises ScoValueError s'il y a des UE sans RCUE. <= ??? XXX
""" """
if self.formsemestre_pair is None or self.formsemestre_impair is None: if self.formsemestre_pair is None or self.formsemestre_impair is None:
return [] return []
@ -537,6 +550,13 @@ class DecisionsProposeesAnnee(DecisionsProposees):
for ue_pair in self.ues_pair: for ue_pair in self.ues_pair:
rcue = None rcue = None
for ue_impair in self.ues_impair: 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_VALIDES
):
continue # ignore cette UE antérieure non capitalisée
if ue_pair.niveau_competence_id == ue_impair.niveau_competence_id: if ue_pair.niveau_competence_id == ue_impair.niveau_competence_id:
rcue = RegroupementCoherentUE( rcue = RegroupementCoherentUE(
self.etud, self.etud,
@ -548,10 +568,12 @@ class DecisionsProposeesAnnee(DecisionsProposees):
) )
ues_impair_sans_rcue.discard(ue_impair.id) ues_impair_sans_rcue.discard(ue_impair.id)
break break
if rcue is None: if rcue is None and not self.a_cheval:
raise NoRCUEError(deca=self, ue=ue_pair) raise NoRCUEError(deca=self, ue=ue_pair)
if rcue is not None:
rcues_annee.append(rcue) rcues_annee.append(rcue)
if len(ues_impair_sans_rcue) > 0: # 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()) ue = UniteEns.query.get(ues_impair_sans_rcue.pop())
raise NoRCUEError(deca=self, ue=ue) raise NoRCUEError(deca=self, ue=ue)
return rcues_annee return rcues_annee
@ -1026,6 +1048,10 @@ class DecisionsProposeesUE(DecisionsProposees):
self.moy_ue_with_cap = ue_status["moy"] self.moy_ue_with_cap = ue_status["moy"]
self.ue_status = ue_status self.ue_status = ue_status
def __repr__(self) -> str:
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): def set_rcue(self, rcue: RegroupementCoherentUE):
"""Rattache cette UE à un RCUE. Cela peut modifier les codes """Rattache cette UE à un RCUE. Cela peut modifier les codes
proposés (si compensation)""" proposés (si compensation)"""

View File

@ -72,16 +72,24 @@ def show_etud(deca: DecisionsProposeesAnnee, read_only: bool = True) -> str:
else: else:
avertissement_redoublement = "" avertissement_redoublement = ""
formsemestre_1 = deca.formsemestre_impair
formsemestre_2 = deca.formsemestre_pair
# Ordonne selon les dates des 2 semestres considérés (pour les redoublants à cheval):
if deca.formsemestre_pair.date_debut < deca.formsemestre_impair.date_debut:
formsemestre_1, formsemestre_2 = formsemestre_2, formsemestre_1
H.append( H.append(
f""" f"""
<div class="titre_niveaux"><b>Niveaux de compétences et unités d'enseignement du BUT{deca.annee_but}</b></div> <div class="titre_niveaux"><b>Niveaux de compétences et unités d'enseignement du BUT{deca.annee_but}</b></div>
<div class="but_annee"> <div class="but_annee">
<div class="titre"></div> <div class="titre"></div>
<div class="titre">S{deca.formsemestre_impair.semestre_id <div class="titre">S{formsemestre_1.semestre_id
if deca.formsemestre_impair else "-"}</div> if formsemestre_1 else "-"}
<div class="titre">S{deca.formsemestre_pair.semestre_id <span class="avertissement_redoublement">{formsemestre_1.annee_scolaire_str() if formsemestre_1 else ""}</span>
if deca.formsemestre_pair else "-"} </div>
<span class="avertissement_redoublement">{avertissement_redoublement}</span></div> <div class="titre">S{formsemestre_2.semestre_id
if formsemestre_2 else "-"}
<span class="avertissement_redoublement">{formsemestre_2.annee_scolaire_str() if formsemestre_2 else ""}</span>
</div>
<div class="titre">RCUE</div> <div class="titre">RCUE</div>
""" """
) )
@ -91,29 +99,40 @@ def show_etud(deca: DecisionsProposeesAnnee, read_only: bool = True) -> str:
<div title="{niveau.competence.titre_long}">{niveau.competence.titre}</div> <div title="{niveau.competence.titre_long}">{niveau.competence.titre}</div>
</div>""" </div>"""
) )
dec_rcue = deca.decisions_rcue_by_niveau.get(niveau.id) dec_rcue = deca.decisions_rcue_by_niveau.get(niveau.id) # peut être None
if dec_rcue is None: ues = [ue for ue in deca.ues_impair if ue.niveau_competence.id == niveau.id]
H.append( ue_impair = ues[0] if ues else None
"""<div class="niveau_vide"></div><div class="niveau_vide"></div><div class="niveau_vide"></div>""" ues = [ue for ue in deca.ues_pair if ue.niveau_competence.id == niveau.id]
) ue_pair = ues[0] if ues else None
continue # Les UEs à afficher, toujours en readonly sur le formsemestre de l'année précédente du redoublant
# Semestre impair ues_ro = [
(
ue_impair,
(deca.a_cheval and deca.formsemestre_id != deca.formsemestre_impair.id),
),
(
ue_pair,
deca.a_cheval and deca.formsemestre_id != deca.formsemestre_pair.id,
),
]
# Ordonne selon les dates des 2 semestres considérés:
if deca.formsemestre_pair.date_debut < deca.formsemestre_impair.date_debut:
ues_ro[0], ues_ro[1] = ues_ro[1], ues_ro[0]
# Colonnes d'UE:
for ue, ue_read_only in ues_ro:
H.append( H.append(
_gen_but_niveau_ue( _gen_but_niveau_ue(
dec_rcue.rcue.ue_1, ue,
deca.decisions_ues[dec_rcue.rcue.ue_1.id], deca.decisions_ues[ue.id],
disabled=read_only, disabled=read_only or ue_read_only,
) annee_prec=ue_read_only,
)
# Semestre pair
H.append(
_gen_but_niveau_ue(
dec_rcue.rcue.ue_2,
deca.decisions_ues[dec_rcue.rcue.ue_2.id],
disabled=read_only,
) )
) )
# RCUE # RCUE
if dec_rcue is None:
H.append("""<div class="niveau_vide"></div>""")
else:
H.append( H.append(
f"""<div class="but_niveau_rcue f"""<div class="but_niveau_rcue
{'recorded' if dec_rcue.code_valide is not None else ''} {'recorded' if dec_rcue.code_valide is not None else ''}
@ -140,6 +159,8 @@ def _gen_but_select(
klass: str = "", klass: str = "",
) -> str: ) -> str:
"Le menu html select avec les codes" "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>"""
h = "\n".join( h = "\n".join(
[ [
f"""<option value="{code}" f"""<option value="{code}"
@ -157,7 +178,12 @@ def _gen_but_select(
""" """
def _gen_but_niveau_ue(ue: UniteEns, dec_ue: DecisionsProposeesUE, disabled=False): def _gen_but_niveau_ue(
ue: UniteEns,
dec_ue: DecisionsProposeesUE,
disabled: bool = False,
annee_prec: bool = False,
):
if dec_ue.ue_status and dec_ue.ue_status["is_capitalized"]: if dec_ue.ue_status and dec_ue.ue_status["is_capitalized"]:
moy_ue_str = f"""<span class="ue_cap">{ moy_ue_str = f"""<span class="ue_cap">{
scu.fmt_note(dec_ue.moy_ue_with_cap)}</span>""" scu.fmt_note(dec_ue.moy_ue_with_cap)}</span>"""
@ -178,6 +204,7 @@ def _gen_but_niveau_ue(ue: UniteEns, dec_ue: DecisionsProposeesUE, disabled=Fals
return f"""<div class="but_niveau_ue { return f"""<div class="but_niveau_ue {
'recorded' if dec_ue.code_valide is not None else ''} 'recorded' if dec_ue.code_valide is not None else ''}
{'annee_prec' if annee_prec else ''}
"> ">
<div title="{ue.titre}">{ue.acronyme}</div> <div title="{ue.titre}">{ue.acronyme}</div>
<div class="but_note with_scoplement"> <div class="but_note with_scoplement">

View File

@ -58,7 +58,7 @@ class ScolarFormSemestreValidation(db.Model):
) )
def __repr__(self): def __repr__(self):
return f"{self.__class__.__name__}({self.formsemestre_id}, {self.etudid}, code={self.code}, ue={self.ue}, moy_ue={self.moy_ue})" return f"{self.__class__.__name__}(sem={self.formsemestre_id}, etuid={self.etudid}, code={self.code}, ue={self.ue}, moy_ue={self.moy_ue})"
def __str__(self): def __str__(self):
if self.ue_id: if self.ue_id:

View File

@ -122,17 +122,29 @@ select:invalid {
background: red; background: red;
} }
select.but_code option.recorded { select.but_code option.recorded,
div.but_code recorded {
color: rgb(3, 157, 3); color: rgb(3, 157, 3);
font-weight: bold; font-weight: bold;
} }
div.but_code {
font-weight: bold;
color: blue;
margin-left: 4px;
margin-right: 4px;
}
div.but_niveau_ue.recorded, div.but_niveau_ue.recorded,
div.but_niveau_rcue.recorded { div.but_niveau_rcue.recorded {
border-color: rgb(136, 252, 136); border-color: rgb(136, 252, 136);
border-width: 2px; border-width: 2px;
} }
div.but_niveau_ue.annee_prec {
background-color: rgb(167, 167, 0);
}
div.but_niveau_ue.modified { div.but_niveau_ue.modified {
background-color: rgb(255, 214, 254); background-color: rgb(255, 214, 254);
} }

View File

@ -60,7 +60,9 @@ def test_but_jury_GMP_lm(test_client):
# puis lance les tests de jury # puis lance les tests de jury
doc = yaml_setup.setup_from_yaml("tests/unit/cursus_but_gmp_iutlm.yaml") doc = yaml_setup.setup_from_yaml("tests/unit/cursus_but_gmp_iutlm.yaml")
formsemestres = FormSemestre.query.order_by(FormSemestre.semestre_id).all() formsemestres = FormSemestre.query.order_by(
FormSemestre.date_debut, FormSemestre.semestre_id
).all()
# Vérifie les deca de tous les semestres: # Vérifie les deca de tous les semestres:
for formsemestre in formsemestres: for formsemestre in formsemestres:
_check_deca(formsemestre) _check_deca(formsemestre)
@ -68,7 +70,6 @@ def test_but_jury_GMP_lm(test_client):
# Saisie de toutes les décisions de jury # Saisie de toutes les décisions de jury
for formsemestre in formsemestres: for formsemestre in formsemestres:
formsemestre_validation_auto_but(formsemestre, only_adm=False) formsemestre_validation_auto_but(formsemestre, only_adm=False)
# Vérifie résultats attendus: # Vérifie résultats attendus:
for formsemestre in formsemestres: for formsemestre in formsemestres:
_test_but_jury(formsemestre, doc) _test_but_jury(formsemestre, doc)