From 438caf105238d170fbbd65cba44b6d7b88d088b8 Mon Sep 17 00:00:00 2001
From: Emmanuel Viennet
Date: Thu, 22 Jun 2023 19:00:56 +0200
Subject: [PATCH] =?UTF-8?q?Jury=20BUT:=20-=20Modification=20gestion=20de?=
=?UTF-8?q?=20l'enregistrement=20des=20codes.=20-=20Signale=20quand=20un?=
=?UTF-8?q?=20RCUE=20change=20de=20code.=20-=20Calcul=20auto=20du=20jury:?=
=?UTF-8?q?=20peut=20modifier=20les=20d=C3=A9cisions=20RCUE.?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
app/but/jury_but.py | 97 +++++++++++--------
app/but/jury_but_validation_auto.py | 8 +-
app/but/jury_but_view.py | 13 ++-
app/scodoc/codes_cursus.py | 11 ++-
app/scodoc/sco_apogee_csv.py | 4 +-
app/static/css/jury_but.css | 1 +
app/static/css/scodoc.css | 3 +-
app/templates/but/documentation_codes_jury.j2 | 9 +-
.../but/formsemestre_validation_auto_but.j2 | 17 +++-
tests/unit/test_but_jury.py | 4 +-
10 files changed, 103 insertions(+), 64 deletions(-)
diff --git a/app/but/jury_but.py b/app/but/jury_but.py
index dedb522b9..d9f8031a2 100644
--- a/app/but/jury_but.py
+++ b/app/but/jury_but.py
@@ -278,11 +278,15 @@ class DecisionsProposeesAnnee(DecisionsProposees):
)
if self.formsemestre_impair is not None:
- self.validation = ApcValidationAnnee.query.filter_by(
- etudid=self.etud.id,
- formation_id=self.formsemestre.formation_id,
- ordre=self.annee_but,
- ).first()
+ self.validation = (
+ ApcValidationAnnee.query.filter_by(
+ etudid=self.etud.id,
+ ordre=self.annee_but,
+ )
+ .join(Formation)
+ .filter_by(formation_code=self.formsemestre.formation.formation_code)
+ .first()
+ )
else:
self.validation = None
if self.validation is not None:
@@ -721,7 +725,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
et qu'il n'y en a pas déjà, enregistre ceux par défaut.
"""
log("jury_but.DecisionsProposeesAnnee.record_form")
- code_annee = None
+ code_annee = self.codes[0] # si pas dans le form, valeur par defaut
codes_rcues = [] # [ (dec_rcue, code), ... ]
codes_ues = [] # [ (dec_ue, code), ... ]
for key in form:
@@ -753,16 +757,15 @@ class DecisionsProposeesAnnee(DecisionsProposees):
dec_ue.record(code)
for dec_rcue, code in codes_rcues:
dec_rcue.record(code)
- self.record(code_annee) # XXX , mark_recorded=False)
+ self.record(code_annee)
self.record_autorisation_inscription(code_annee)
self.record_all()
self.recorded = True
db.session.commit()
- def record(self, code: str, no_overwrite=False, mark_recorded: bool = True) -> bool:
+ def record(self, code: str, mark_recorded: bool = True) -> bool:
"""Enregistre le code de l'année, et au besoin l'autorisation d'inscription.
- Si no_overwrite, ne fait rien si un code est déjà enregistré.
Si l'étudiant est DEM ou DEF, ne fait rien.
Si mark_recorded est vrai, positionne self.recorded
"""
@@ -773,23 +776,34 @@ class DecisionsProposeesAnnee(DecisionsProposees):
f"code annee {html.escape(code)} invalide pour formsemestre {html.escape(self.formsemestre)}"
)
- if code != self.code_valide and (self.code_valide is None or not no_overwrite):
+ if code != self.code_valide:
# Enregistrement du code annuel BUT
- if self.validation:
- db.session.delete(self.validation)
- db.session.commit()
if code is None:
- self.validation = None
+ if self.validation:
+ db.session.delete(self.validation)
+ self.validation = None
+ db.session.commit()
else:
- self.validation = ApcValidationAnnee(
- etudid=self.etud.id,
- formsemestre=self.formsemestre_impair,
- formation_id=self.formsemestre.formation_id,
- ordre=self.annee_but,
- annee_scolaire=self.annee_scolaire(),
- code=code,
- )
+ if self.validation is None:
+ self.validation = ApcValidationAnnee(
+ etudid=self.etud.id,
+ formsemestre=self.formsemestre_impair,
+ formation_id=self.formsemestre.formation_id,
+ ordre=self.annee_but,
+ annee_scolaire=self.annee_scolaire(),
+ code=code,
+ )
+ else: # Update validation année BUT
+ self.validation.etud = self.etud
+ self.validation.formsemestre = self.formsemestre_impair
+ self.validation.formation_id = self.formsemestre.formation_id
+ self.validation.ordre = self.annee_but
+ self.validation.annee_scolaire = self.annee_scolaire()
+ self.validation.code = code
+ self.validation.date = datetime.now()
+
db.session.add(self.validation)
+ db.session.commit()
log(f"Recording {self}: {code}")
Scolog.logdb(
method="jury_but",
@@ -840,9 +854,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
)
return res and self.etud.id in res.get_etudids_attente()
- def record_all(
- self, no_overwrite: bool = True, only_validantes: bool = False
- ) -> bool:
+ def record_all(self, only_validantes: bool = False) -> bool:
"""Enregistre les codes qui n'ont pas été spécifiés par le formulaire,
et sont donc en mode "automatique".
- Si "à cheval", ne modifie pas les codes UE de l'année scolaire précédente.
@@ -868,9 +880,8 @@ class DecisionsProposeesAnnee(DecisionsProposees):
# rappel: le code par défaut est en tête
code = dec_ue.codes[0] if dec_ue.codes else None
if (not only_validantes) or code in sco_codes.CODES_UE_VALIDES_DE_DROIT:
- # enregistre le code jury seulement s'il n'y a pas déjà de code
- # (no_overwrite=True) sauf en mode test yaml
- modif |= dec_ue.record(code, no_overwrite=no_overwrite)
+ # enregistre le code jury
+ modif |= dec_ue.record(code)
# RCUE :
for dec_rcue in self.decisions_rcue_by_niveau.values():
code = dec_rcue.codes[0] if dec_rcue.codes else None
@@ -888,17 +899,15 @@ class DecisionsProposeesAnnee(DecisionsProposees):
)
)
):
- modif |= dec_rcue.record(code, no_overwrite=no_overwrite)
+ modif |= dec_rcue.record(code)
# Année:
if not self.recorded:
# rappel: le code par défaut est en tête
code = self.codes[0] if self.codes else None
- # enregistre le code jury seulement s'il n'y a pas déjà de code
- # (no_overwrite=True) sauf en mode test yaml
if (
not only_validantes
) or code in sco_codes.CODES_ANNEE_BUT_VALIDES_DE_DROIT:
- modif |= self.record(code, no_overwrite=no_overwrite)
+ modif |= self.record(code)
self.record_autorisation_inscription(code)
return modif
@@ -1133,7 +1142,7 @@ class DecisionsProposeesRCUE(DecisionsProposees):
return f"""<{self.__class__.__name__} rcue={self.rcue} valid={self.code_valide
} codes={self.codes} explanation={self.explanation}"""
- def record(self, code: str, no_overwrite=False) -> bool:
+ def record(self, code: str) -> bool:
"""Enregistre le code RCUE.
Note:
- si le RCUE est ADJ, les UE non validées sont passées à ADJ
@@ -1147,7 +1156,7 @@ class DecisionsProposeesRCUE(DecisionsProposees):
raise ScoValueError(
f"code UE invalide pour ue_id={self.ue.id}: {html.escape(code)}"
)
- if code == self.code_valide or (self.code_valide is not None and no_overwrite):
+ if code == self.code_valide:
self.recorded = True
return False # no change
parcours_id = self.parcour.id if self.parcour is not None else None
@@ -1322,11 +1331,15 @@ class DecisionsProposeesRCUE(DecisionsProposees):
if annee_inferieure < 1:
return
# Garde-fou: Année déjà validée ?
- validations_annee: ApcValidationAnnee = ApcValidationAnnee.query.filter_by(
- etudid=self.etud.id,
- ordre=annee_inferieure,
- formation_id=self.rcue.formsemestre_1.formation_id,
- ).all()
+ validations_annee: ApcValidationAnnee = (
+ ApcValidationAnnee.query.filter_by(
+ etudid=self.etud.id,
+ ordre=annee_inferieure,
+ )
+ .join(Formation)
+ .filter_by(formation_code=self.rcue.formsemestre_1.formation.code)
+ .all()
+ )
if len(validations_annee) > 1:
log(
f"warning: {len(validations_annee)} validations d'année\n{validations_annee}"
@@ -1519,16 +1532,15 @@ class DecisionsProposeesUE(DecisionsProposees):
self.codes = [sco_codes.AJ, sco_codes.ADJ] + self.codes
self.explanation = "notes insuffisantes"
- def record(self, code: str, no_overwrite=False) -> bool:
+ def record(self, code: str) -> bool:
"""Enregistre le code jury pour cette UE.
- Si no_overwrite, n'enregistre pas s'il y a déjà un code.
Return: True si code enregistré (modifié)
"""
if code and not code in self.codes:
raise ScoValueError(
f"code UE invalide pour ue_id={self.ue.id}: {html.escape(code)}"
)
- if code == self.code_valide or (self.code_valide is not None and no_overwrite):
+ if code == self.code_valide:
self.recorded = True
return False # no change
self.erase()
@@ -1627,7 +1639,6 @@ class BUTCursusEtud: # WIP TODO
ApcValidationAnnee.query.filter_by(
etudid=self.etud.id,
ordre=ordre,
- formation_id=self.formsemestre.formation_id,
)
.join(Formation)
.filter(
diff --git a/app/but/jury_but_validation_auto.py b/app/but/jury_but_validation_auto.py
index deae9b593..e698b5dd4 100644
--- a/app/but/jury_but_validation_auto.py
+++ b/app/but/jury_but_validation_auto.py
@@ -16,14 +16,12 @@ from app.scodoc.sco_exceptions import ScoValueError
def formsemestre_validation_auto_but(
- formsemestre: FormSemestre, only_adm: bool = True, no_overwrite: bool = True
+ formsemestre: FormSemestre, only_adm: bool = True
) -> int:
"""Calcul automatique des décisions de jury sur une "année" BUT.
- N'enregistre jamais de décisions de l'année scolaire précédente, même
si on a des RCUE "à cheval".
- - Ne modifie jamais de décisions déjà enregistrées (sauf si no_overwrite est faux,
- ce qui est utilisé pour certains tests unitaires).
- Normalement, only_adm est True et on n'enregistre que les décisions validantes
de droit: ADM ou CMP.
En revanche, si only_adm est faux, on enregistre la première décision proposée par ScoDoc
@@ -38,9 +36,7 @@ def formsemestre_validation_auto_but(
for etudid in formsemestre.etuds_inscriptions:
etud = Identite.get_etud(etudid)
deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre)
- nb_etud_modif += deca.record_all(
- no_overwrite=no_overwrite, only_validantes=only_adm
- )
+ nb_etud_modif += deca.record_all(only_validantes=only_adm)
db.session.commit()
return nb_etud_modif
diff --git a/app/but/jury_but_view.py b/app/but/jury_but_view.py
index bbe992cbf..87321684e 100644
--- a/app/but/jury_but_view.py
+++ b/app/but/jury_but_view.py
@@ -155,6 +155,7 @@ def _gen_but_select(
disabled: bool = False,
klass: str = "",
data: dict = {},
+ code_valide_label: str = "",
) -> str:
"Le menu html select avec les codes"
# if disabled: # mauvaise idée car le disabled est traité en JS
@@ -164,7 +165,10 @@ def _gen_but_select(
f"""{code} """
+ >{code
+ if ((code != code_valide) or not code_valide_label)
+ else code_valide_label
+ }"""
for code in codes
]
)
@@ -246,6 +250,7 @@ def _gen_but_rcue(dec_rcue: DecisionsProposeesRCUE, niveau: ApcNiveau) -> str:
"""
code_propose_menu = dec_rcue.code_valide # le code enregistré
+ code_valide_label = code_propose_menu
if dec_rcue.validation:
if dec_rcue.code_valide == dec_rcue.codes[0]:
descr_validation = dec_rcue.validation.html()
@@ -257,6 +262,9 @@ def _gen_but_rcue(dec_rcue: DecisionsProposeesRCUE, niveau: ApcNiveau) -> str:
> sco_codes.BUT_CODES_ORDER[dec_rcue.code_valide]
):
code_propose_menu = dec_rcue.codes[0]
+ code_valide_label = (
+ f"{dec_rcue.codes[0]} (actuel {dec_rcue.code_valide})"
+ )
scoplement = f"""{descr_validation}
"""
else:
scoplement = "" # "pas de validation"
@@ -282,7 +290,8 @@ def _gen_but_rcue(dec_rcue: DecisionsProposeesRCUE, niveau: ApcNiveau) -> str:
code_propose_menu,
disabled=True,
klass="manual code_rcue",
- data = { "niveau_id" : str(niveau.id)}
+ data = { "niveau_id" : str(niveau.id)},
+ code_valide_label = code_valide_label,
)}
diff --git a/app/scodoc/codes_cursus.py b/app/scodoc/codes_cursus.py
index f5027eee1..85d14b957 100644
--- a/app/scodoc/codes_cursus.py
+++ b/app/scodoc/codes_cursus.py
@@ -223,8 +223,15 @@ BUT_CODES_PASSAGE = {
# les codes, du plus "défavorable" à l'étudiant au plus favorable:
# (valeur par défaut 0)
BUT_CODES_ORDER = {
- NAR: 0,
+ ABAN: 0,
+ ABL: 0,
+ DEM: 0,
DEF: 0,
+ EXCLU: 0,
+ NAR: 0,
+ UEBSL: 0,
+ RAT: 5,
+ RED: 6,
AJ: 10,
ATJ: 20,
CMP: 50,
@@ -233,7 +240,7 @@ BUT_CODES_ORDER = {
PASD: 60,
ADJR: 90,
ADSUP: 90,
- ADJ: 100,
+ ADJ: 90,
ADM: 100,
}
diff --git a/app/scodoc/sco_apogee_csv.py b/app/scodoc/sco_apogee_csv.py
index 1c02c3c16..1348cbe8c 100644
--- a/app/scodoc/sco_apogee_csv.py
+++ b/app/scodoc/sco_apogee_csv.py
@@ -495,7 +495,9 @@ class ApoEtud(dict):
ApcValidationAnnee.query.filter_by(
formsemestre_id=formsemestre.id,
etudid=self.etud["etudid"],
- formation_id=self.cur_sem["formation_id"],
+ formation_id=self.cur_sem[
+ "formation_id"
+ ], # XXX utiliser formation_code
).first()
)
self.is_nar = (
diff --git a/app/static/css/jury_but.css b/app/static/css/jury_but.css
index 61f8db77b..bf4be05e3 100644
--- a/app/static/css/jury_but.css
+++ b/app/static/css/jury_but.css
@@ -168,6 +168,7 @@ div.but_niveau_ue.recorded_different,
div.but_niveau_rcue.recorded_different {
box-shadow: 0 0 0 3px red;
outline: dashed 3px var(--color-recorded);
+ background-color: yellow;
}
div.but_niveau_ue.annee_prec {
diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css
index 172e90081..0173a14e5 100644
--- a/app/static/css/scodoc.css
+++ b/app/static/css/scodoc.css
@@ -1128,7 +1128,8 @@ div.sco_help {
padding: 8px;
border-radius: 4px;
font-style: italic;
- background-color: rgb(200, 200, 220);
+ max-width: 800px;
+ background-color: rgb(209, 255, 214);
}
div.vertical_spacing_but {
diff --git a/app/templates/but/documentation_codes_jury.j2 b/app/templates/but/documentation_codes_jury.j2
index 9cd90fd1a..3ea9c80de 100644
--- a/app/templates/but/documentation_codes_jury.j2
+++ b/app/templates/but/documentation_codes_jury.j2
@@ -8,6 +8,8 @@
(transcription paramétrable par votre administrateur ScoDoc).
Codes d'année
+ Les codes d'année BUT sont associés à la formation et non au semestre: on ne valide
+ qu'une seule fois BUT1, BUT2 puis BUT3.
@@ -100,7 +102,8 @@
Codes RCUE (niveaux de compétences annuels)
-
+ Les codes de RCUE sont associés à la formation: chaque niveau de compétence
+ est validé une fois au maximum. En cas de redoublement, le code RCUE peut changer.
@@ -161,7 +164,9 @@
Codes des Unités d'Enseignement (UE)
-
+ Les codes d'UE sont associés aux UE d'un semestre. En cas de redoublement,
+ l'UE antérieure garde son code, non écrasé par le redoublement. Chaque UE suivie a son code.
+
diff --git a/app/templates/but/formsemestre_validation_auto_but.j2 b/app/templates/but/formsemestre_validation_auto_but.j2
index 27334aac5..5bf69b397 100644
--- a/app/templates/but/formsemestre_validation_auto_but.j2
+++ b/app/templates/but/formsemestre_validation_auto_but.j2
@@ -8,19 +8,27 @@
{% block app_content %}
+
Calcul automatique des décisions de jury du BUT
N'enregistre jamais de décisions de l'année scolaire précédente, même
si on a des RCUE "à cheval" sur deux années.
- Ne modifie jamais de décisions déjà enregistrées.
+
+ Attention: peut modifier des décisions déjà enregistrées , si la
+ validation de droit est calculée. Par exemple, vous aviez saisi RAT
+ pour un étudiant dont les moyennes d'UE dépassent 10 mais qui pour une
+ raison particulière ne valide pas son année. Le calcul automatique peut
+ remplacer ce RAT par un ADM , ScoDoc considérant que les
+ conditions sont satisfaites. On peut éviter cela en laissant une note de
+ l'étudiant en ATTente.
+
N'enregistre que les décisions validantes de droit: ADM ou CMP .
N'enregistre pas de décision si l'étudiant a une ou plusieurs notes en ATTente.
- L'assiduité n'est pas prise en compte.
-
+ L'assiduité n'est pas prise en compte.
En conséquence, saisir ensuite manuellement les décisions manquantes ,
@@ -34,9 +42,10 @@
Il est nécessaire de relire soigneusement les décisions à l'issue de cette procédure !
+
-
+
{{ wtf.quick_form(form) }}
diff --git a/tests/unit/test_but_jury.py b/tests/unit/test_but_jury.py
index db0b9aa27..acb5c67da 100644
--- a/tests/unit/test_but_jury.py
+++ b/tests/unit/test_but_jury.py
@@ -108,9 +108,7 @@ def test_but_jury_GEII_lyon(test_client):
# Saisie de toutes les décisions de jury "automatiques"
# et vérification des résultats attendus:
for formsemestre in formsemestres:
- formsemestre_validation_auto_but(
- formsemestre, only_adm=False, no_overwrite=False
- )
+ formsemestre_validation_auto_but(formsemestre, only_adm=False)
yaml_setup_but.but_test_jury(formsemestre, doc)