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 + 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)