diff --git a/app/but/jury_but.py b/app/but/jury_but.py index 244989edda..af1eed4ff8 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 acb6b695bc..ac454195eb 100644 --- a/app/but/jury_but_view.py +++ b/app/but/jury_but_view.py @@ -43,21 +43,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 +73,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 +284,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 +373,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 +449,35 @@ 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.
""" + 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} +
""" ) diff --git a/app/scodoc/sco_moduleimpl_status.py b/app/scodoc/sco_moduleimpl_status.py index d0fdd05b37..9b92a7dfee 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"]}  ( + 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.
""" + 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 25ca43156f..12556c2c20 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 7a38f3bb3d..8d64c976a6 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 5ee80a684e..fec4976c68 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()