diff --git a/app/but/jury_but.py b/app/but/jury_but.py index 244989edd..af1eed4ff 100644 --- a/app/but/jury_but.py +++ b/app/but/jury_but.py @@ -630,19 +630,38 @@ class DecisionsProposeesAnnee(DecisionsProposees): d[dec_rcue.rcue.ue_2.id] = dec_rcue return d - def next_annee_semestre_id(self, code: str) -> int: - """L'indice du semestre dans lequel l'étudiant est autorisé à - poursuivre l'année suivante. None si aucun.""" - if self.formsemestre_pair is None: - return None # seulement sur année - if code == RED: - return self.formsemestre_pair.semestre_id - 1 - elif ( - code in sco_codes.BUT_CODES_PASSAGE + 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. + """ + ids = set() + # La poursuite d'études dans un semestre pair d’une même année + # est de droit pour tout étudiant: + if (self.formsemestre.semestre_id % 2) and sco_codes.ParcoursBUT.NB_SEM: + ids.add(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 d’UE ; + # - et une moyenne égale ou supérieure à 8 sur 20 à chaque RCUE. + # + # La condition a paru trop stricte à de nombreux collègues. + # ScoDoc ne contraint donc pas à la respecter strictement. + # Si le code est dans BUT_CODES_PASSAGE (ADM, ADJ, PASD, PAS1NCI, ATJ), + # autorise à passer dans le semestre suivant + if ( + self.jury_annuel + and code in sco_codes.BUT_CODES_PASSAGE and self.formsemestre_pair.semestre_id < sco_codes.ParcoursBUT.NB_SEM ): - return self.formsemestre_pair.semestre_id + 1 - return None + ids.add(self.formsemestre.semestre_id + 1) + + if code == RED: + ids.add( + self.formsemestre.semestre_id - (self.formsemestre.semestre_id + 1) % 2 + ) + + return ids def record_form(self, form: dict): """Enregistre les codes de jury en base @@ -704,47 +723,43 @@ class DecisionsProposeesAnnee(DecisionsProposees): raise ScoValueError( f"code annee {html.escape(code)} invalide pour formsemestre {html.escape(self.formsemestre)}" ) - if code == self.code_valide or (self.code_valide is not None and no_overwrite): - self.recorded = True - return False # no change - if self.validation: - db.session.delete(self.validation) - db.session.commit() - if code is None: - self.validation = None - else: - self.validation = ApcValidationAnnee( - etudid=self.etud.id, - formsemestre=self.formsemestre_impair, - ordre=self.annee_but, - annee_scolaire=self.annee_scolaire(), - code=code, - ) - db.session.add(self.validation) - db.session.commit() - log(f"Recording {self}: {code}") - Scolog.logdb( - method="jury_but", - etudid=self.etud.id, - msg=f"Validation année BUT{self.annee_but}: {code}", - ) + + if code != self.code_valide and (self.code_valide is None or not no_overwrite): + # Enregistrement du code annuel BUT + if self.validation: + db.session.delete(self.validation) + db.session.commit() + if code is None: + self.validation = None + else: + self.validation = ApcValidationAnnee( + etudid=self.etud.id, + formsemestre=self.formsemestre_impair, + ordre=self.annee_but, + annee_scolaire=self.annee_scolaire(), + code=code, + ) + db.session.add(self.validation) + db.session.commit() + log(f"Recording {self}: {code}") + Scolog.logdb( + method="jury_but", + etudid=self.etud.id, + msg=f"Validation année BUT{self.annee_but}: {code}", + ) # --- Autorisation d'inscription dans semestre suivant ? - if self.formsemestre_pair is not None: - if code is None: - ScolarAutorisationInscription.delete_autorisation_etud( - etudid=self.etud.id, - origin_formsemestre_id=self.formsemestre_pair.id, - ) - else: - next_semestre_id = self.next_annee_semestre_id(code) - if next_semestre_id is not None: - ScolarAutorisationInscription.autorise_etud( - self.etud.id, - self.formsemestre_pair.formation.formation_code, - self.formsemestre_pair.id, - next_semestre_id, - ) + ScolarAutorisationInscription.delete_autorisation_etud( + etudid=self.etud.id, + origin_formsemestre_id=self.formsemestre.id, + ) + for next_semestre_id in self.next_semestre_ids(code): + ScolarAutorisationInscription.autorise_etud( + self.etud.id, + self.formsemestre.formation.formation_code, + self.formsemestre.id, + next_semestre_id, + ) db.session.commit() self.recorded = True @@ -872,18 +887,18 @@ class DecisionsProposeesAnnee(DecisionsProposees): self.invalidate_formsemestre_cache() def get_autorisations_passage(self) -> list[int]: - """Les liste des indices de semestres auxquels on est autorisé à - s'inscrire depuis cette année""" - formsemestre = self.formsemestre_pair or self.formsemestre_impair - if not formsemestre: - return [] - return [ - a.semestre_id - for a in ScolarAutorisationInscription.query.filter_by( - etudid=self.etud.id, - origin_formsemestre_id=formsemestre.id, - ) - ] + """Liste des indices de semestres auxquels on est autorisé à + s'inscrire depuis le semestre courant. + """ + return sorted( + [ + a.semestre_id + for a in ScolarAutorisationInscription.query.filter_by( + etudid=self.etud.id, + origin_formsemestre_id=self.formsemestre.id, + ) + ] + ) def descr_niveaux_validation(self, line_sep: str = "\n") -> str: """Description textuelle des niveaux validés (enregistrés) diff --git a/app/but/jury_but_view.py b/app/but/jury_but_view.py index acb6b695b..a58f41e2c 100644 --- a/app/but/jury_but_view.py +++ b/app/but/jury_but_view.py @@ -11,7 +11,7 @@ import re import numpy as np import flask -from flask import flash, url_for +from flask import flash, render_template, url_for from flask import g, request from app import db @@ -32,8 +32,10 @@ from app.models import ( ScolarAutorisationInscription, ScolarFormSemestreValidation, ) +from app.models.config import ScoDocSiteConfig from app.scodoc import html_sco_header from app.scodoc.sco_exceptions import ScoValueError +from app.scodoc import sco_preferences from app.scodoc import sco_utils as scu @@ -43,21 +45,20 @@ def show_etud(deca: DecisionsProposeesAnnee, read_only: bool = True) -> str: """ H = [] - H.append("""
""") - H.append( - f""" -
- Décision de jury pour l'année : { - _gen_but_select("code_annee", deca.codes, deca.code_valide, - disabled=True, klass="manual") - } - ({deca.code_valide or 'non'} enregistrée) + if deca.jury_annuel: + H.append( + f""" +
+
+ Décision de jury pour l'année : { + _gen_but_select("code_annee", deca.codes, deca.code_valide, + disabled=True, klass="manual") + } + ({deca.code_valide or 'non'} enregistrée) +
""" - ) - div_explanation = f"""
{deca.explanation}
""" - - H.append("""
""") + ) formsemestre_1 = deca.formsemestre_impair formsemestre_2 = deca.formsemestre_pair @@ -74,7 +75,7 @@ def show_etud(deca: DecisionsProposeesAnnee, read_only: bool = True) -> str:
Niveaux de compétences et unités d'enseignement du BUT{deca.annee_but}
- {div_explanation} +
{deca.explanation}
{"S" +str(formsemestre_1.semestre_id) @@ -285,7 +286,7 @@ def jury_but_semestriel( read_only: bool, navigation_div: str = "", ) -> str: - """Formulaire saisie décision d'UE d'un semestre BUT isolé (pas jury annuel)""" + """Page: formulaire saisie décision d'UE d'un semestre BUT isolé (pas jury annuel).""" res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre) parcour, ues = jury_but.list_ue_parcour_etud(formsemestre, etud, res) inscription_etat = etud.inscription_etat(formsemestre.id) @@ -374,20 +375,20 @@ def jury_but_semestriel( f"""
-
-
-
Jury BUT S{formsemestre.id} - - Parcours {(parcour.libelle if parcour else False) or "non spécifié"} +
+
+
Jury BUT S{formsemestre.id} + - Parcours {(parcour.libelle if parcour else False) or "non spécifié"} +
+
{etud.nomprenom}
+
+
-
{etud.nomprenom}
-
- -
-

Jury sur un semestre BUT isolé (ne concerne que les UEs)

- {warning} +

Jury sur un semestre BUT isolé (ne concerne que les UEs)

+ {warning}
@@ -450,24 +451,37 @@ def jury_but_semestriel( ) H.append("
") # but_annee + div_autorisations_passage = ( + f""" +
+ Autorisé à passer en : + { ", ".join( ["S" + str(a.semestre_id or '') for a in autorisations_passage ] )} +
+ """ + if autorisations_passage + else """
pas d'autorisations de passage enregistrées.
""" + ) + H.append(div_autorisations_passage) + if read_only: H.append( - """
- Vous n'avez pas la permission de modifier ces décisions. - Les champs entourés en vert sont enregistrés.
""" + f"""
+ {"Vous n'avez pas la permission de modifier ces décisions." + if formsemestre.etat + else "Semestre verrouillé."} + Les champs entourés en vert sont enregistrés. +
+ """ ) else: if formsemestre.semestre_id < formsemestre.formation.get_parcours().NB_SEM: H.append( f"""
- - autoriser à passer dans le semestre S{formsemestre.semestre_id+1} - {("(autorisations enregistrées: " + ' '.join( - 'S' + str(a.semestre_id or '') for a in autorisations_passage) + ")" - ) if autorisations_passage else ""} - + + autoriser à passer dans le semestre S{formsemestre.semestre_id+1} +
""" ) @@ -481,7 +495,19 @@ def jury_but_semestriel(
""" ) - H.append(navigation_div) + + H.append(navigation_div) + H.append("
") + H.append( + render_template( + "but/documentation_codes_jury.html", + nom_univ=f"""Export {sco_preferences.get_preference("InstituteName") + or sco_preferences.get_preference("UnivName") + or "Apogée"}""", + codes=ScoDocSiteConfig.get_codes_apo_dict(), + ) + ) + return "\n".join(H) diff --git a/app/comp/res_common.py b/app/comp/res_common.py index 7632ec28b..35f2a637e 100644 --- a/app/comp/res_common.py +++ b/app/comp/res_common.py @@ -733,7 +733,8 @@ class ResultatsSemestre(ResultatsCache): f"""{"saisir" if not jury_code_sem else "modifier"} décision""", + }">{("saisir" if not jury_code_sem else "modifier") + if self.formsemestre.etat else "voir"} décisions""", "col_jury_link", idx, ) diff --git a/app/scodoc/sco_formsemestre_validation.py b/app/scodoc/sco_formsemestre_validation.py index 25b47df3f..3d2a7835a 100644 --- a/app/scodoc/sco_formsemestre_validation.py +++ b/app/scodoc/sco_formsemestre_validation.py @@ -602,9 +602,21 @@ def formsemestre_recap_parcours_table( {sem['mois_debut']} {formsemestre.titre_annee()}{parcours_name} + title="Bulletin de notes">{formsemestre.titre_annee()}{parcours_name} """ ) + if nt.is_apc: + H.append( + f"""jury""" + ) + H.append("""""") + if nt.is_apc: H.append('BUT') elif decision_sem: diff --git a/app/scodoc/sco_moduleimpl_status.py b/app/scodoc/sco_moduleimpl_status.py index d0fdd05b3..9b92a7dfe 100644 --- a/app/scodoc/sco_moduleimpl_status.py +++ b/app/scodoc/sco_moduleimpl_status.py @@ -544,7 +544,9 @@ def _ligne_evaluation( if not first_eval: H.append(""" """) tr_class_1 += " mievr_spaced" - H.append(f"""""") + H.append( + f"""""" + ) coef = evaluation.coefficient if is_apc: if not evaluation.get_ue_poids_dict(): @@ -588,7 +590,9 @@ def _ligne_evaluation( ) # H.append( - f"""
+ f""" + + { eval_index:2} @@ -612,20 +616,6 @@ def _ligne_evaluation( else: H.append(arrow_none) - H.append( - f""" -
- - -   - Durée - Coef. - Notes - Abs - N - Moyenne """ - ) - if etat["evalcomplete"]: etat_txt = """(prise en compte)""" etat_descr = "notes utilisées dans les moyennes" @@ -648,9 +638,19 @@ def _ligne_evaluation( }" title="{etat_descr}">{etat_txt}""" H.append( - f"""{etat_txt} - - """ + f""" + + +   + Durée + Coef. + Notes + Abs + N + Moyenne {etat_txt} + + + """ ) if can_edit_evals: H.append( @@ -726,7 +726,7 @@ def _ligne_evaluation( {etat["nb_notes"]} / {etat["nb_inscrits"]} {etat["nb_abs"]} {etat["nb_neutre"]} - """ + """ % etat ) if etat["moy"]: @@ -750,11 +750,11 @@ def _ligne_evaluation( H.append(f"""""") if modimpl.module.is_apc(): H.append( - f"""{ + f"""{ evaluation.get_ue_poids_str()}""" ) else: - H.append('') + H.append('') H.append("""""") else: # il y a deja des notes saisies gr_moyennes = etat["gr_moyennes"] @@ -773,7 +773,10 @@ def _ligne_evaluation( name = "Tous" # tous else: name = f"""Groupe {gr_moyenne["group_name"]}""" - H.append(f"""{name}  """) + H.append( + f"""{name}   + """ + ) if gr_moyenne["gr_nb_notes"] > 0: H.append( f"""{gr_moyenne["gr_moy"]}  (Textes de référence:
diff --git a/app/views/notes.py b/app/views/notes.py index 5ebc60c09..3bdb9f6e6 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -2499,11 +2499,31 @@ def formsemestre_validation_but( H.append(jury_but_view.show_etud(deca, read_only=read_only)) + autorisations_idx = deca.get_autorisations_passage() + div_autorisations_passage = ( + f""" +
+ Autorisé à passer en : + { ", ".join( ["S" + str(i) for i in autorisations_idx ] )} +
+ """ + if autorisations_idx + else """
+ pas d'autorisations de passage enregistrées. +
+ """ + ) + H.append(div_autorisations_passage) + if read_only: H.append( - """
- Vous n'avez pas la permission de modifier ces décisions. - Les champs entourés en vert sont enregistrés.
""" + f""" +
+ {"Vous n'avez pas la permission de modifier ces décisions." + if formsemestre.etat + else "Semestre verrouillé."} + Les champs entourés en vert sont enregistrés. +
""" ) else: erase_span = f""" - permettre la saisie manuelles des codes d'année et de niveaux. - Dans ce cas, il vous revient de vous assurer de la cohérence entre - vos codes d'UE/RCUE/Année ! + permettre la saisie manuelles des codes + {"d'année et " if deca.jury_annuel else ""} + de niveaux. + Dans ce cas, assurez-vous de la cohérence entre les codes d'UE/RCUE/Année ! +
diff --git a/scodoc.py b/scodoc.py index 25ca43156..12556c2c2 100755 --- a/scodoc.py +++ b/scodoc.py @@ -32,7 +32,7 @@ from app.models import GroupDescr from app.models import Identite from app.models import ModuleImpl, ModuleImplInscription from app.models import Partition -from app.models import ScolarFormSemestreValidation +from app.models import ScolarAutorisationInscription, ScolarFormSemestreValidation from app.models.but_refcomp import ( ApcCompetence, ApcNiveau, @@ -101,6 +101,7 @@ def make_shell_context(): "ResultatsSemestreBUT": ResultatsSemestreBUT, "Role": Role, "scolar": scolar, + "ScolarAutorisationInscription": ScolarAutorisationInscription, "ScolarFormSemestreValidation": ScolarFormSemestreValidation, "ScolarNews": models.ScolarNews, "scu": scu, diff --git a/tests/unit/cursus_but_geii_lyon.yaml b/tests/unit/cursus_but_geii_lyon.yaml index 7a38f3bb3..8d64c976a 100644 --- a/tests/unit/cursus_but_geii_lyon.yaml +++ b/tests/unit/cursus_but_geii_lyon.yaml @@ -1022,3 +1022,84 @@ Etudiants: moy_ue: 9.5000 moy_ue_with_cap: 12.7600 decision_annee: AJ + geii1000: + prenom: etugeii1000 + civilite: M + formsemestres: + S1: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S1.1": 12.0000 + "S1.2": 9.0000 + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: False + nb_competences: 2 + nb_rcue_annee: 0 + decisions_ues: + "UE11": + codes: [ "ADM", "..." ] + code_valide: ADM + moy_ue: 12.0000 + "UE12": + codes: [ "AJ", "..." ] + code_valide: AJ + decision_jury: AJ + moy_ue: 9.0000 + S2: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S2.1": 12.0000 + "S2.2": 10.50 # capitalise mais ne compense pas + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: False + nb_competences: 2 + nb_rcue_annee: 2 + valide_moitie_rcue: False + codes: [ "RED", "..." ] + decisions_ues: + "UE21": + codes: [ "ADM", "..." ] + code_valide: ADM + moy_ue: 12.0000 + "UE22": + code_valide: ADM + moy_ue: 10.5 + decisions_rcues: # on repère ici les RCUE par l'acronyme de leur 1ere UE (donc du S1) + "UE11": + code_valide: ADM + decision_jury: ADM + rcue: + moy_rcue: 12.0000 + est_compensable: False + "UE12": + code_valide: AJ + decision_jury: AJ + rcue: + moy_rcue: 9.75 + est_compensable: False + decision_annee: RED + S1-red: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S1.1": 5.0000 + "S1.2": 12.0000 + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: False + nb_competences: 2 + nb_rcue_annee: 0 + decisions_ues: + "UE11": + code_valide: AJ + moy_ue: 5. # LA MOYENNE COURANTE + moy_ue_with_cap: 12.0000 + "UE12": + code_valide: ADM + decision_jury: ADM + moy_ue: 12.0000 + # RCUE inter-annuel + decisions_rcues: # on repère ici les RCUE par l'acronyme de leur 1ere UE (donc du S1) + "UE12": + code_valide: ADM + rcue: + moy_rcue: 11.25 + est_compensable: False diff --git a/tests/unit/test_but_jury.py b/tests/unit/test_but_jury.py index 5ee80a684..fec4976c6 100644 --- a/tests/unit/test_but_jury.py +++ b/tests/unit/test_but_jury.py @@ -33,7 +33,7 @@ DEPT = TestConfig.DEPT_TEST @pytest.mark.but_gb def test_but_jury_GB(test_client): """Tests sur un cursus GB - - construction des semestres et de leurs étudianst à partir du yaml + - construction des semestres et de leurs étudiants à partir du yaml - vérification jury de S1 - vérification jury de S2 - vérification jury de S3 @@ -96,7 +96,6 @@ def test_but_jury_GEII_lyon(test_client): # Construit la base de test GB une seule fois # puis lance les tests de jury doc = yaml_setup.setup_from_yaml("tests/unit/cursus_but_geii_lyon.yaml") - formsemestres = FormSemestre.query.order_by( FormSemestre.date_debut, FormSemestre.semestre_id ).all()