diff --git a/app/models/ues.py b/app/models/ues.py
index caa46ce41b..8627fea167 100644
--- a/app/models/ues.py
+++ b/app/models/ues.py
@@ -260,8 +260,10 @@ class UniteEns(db.Model):
class DispenseUE(db.Model):
"""Dispense d'UE
- Utilisé en APC (BUT) pour indiquer les étudiants redoublants avec une UE capitalisée
- qu'ils ne refont pas.
+ Utilisé en APC (BUT) pour indiquer
+ - les étudiants redoublants avec une UE capitalisée qu'ils ne refont pas.
+ - les étudiants "non inscrit" à une UE car elle ne fait pas partie de leur Parcours.
+
La dispense d'UE n'est PAS une validation:
- elle n'est pas affectée par les décisions de jury (pas effacée)
- elle est associée à un formsemestre
diff --git a/app/scodoc/sco_moduleimpl_inscriptions.py b/app/scodoc/sco_moduleimpl_inscriptions.py
index 4c593c06d0..c693c9228f 100644
--- a/app/scodoc/sco_moduleimpl_inscriptions.py
+++ b/app/scodoc/sco_moduleimpl_inscriptions.py
@@ -613,11 +613,16 @@ def _list_but_ue_inscriptions(res: NotesTableCompat, read_only: bool = True) ->
- L'inscription ou désinscription aux UEs du BUT n'affecte pas les inscriptions aux modules
+
L'inscription ou désinscription aux UEs du BUT n'affecte pas les inscriptions aux modules
mais permet de "dispenser" un étudiant de suivre certaines UEs de son parcours.
- Il peut s'agit d'étudiants redoublants ayant déjà acquis l'UE, ou d'autres cas particuliers.
- La dispense d'UE est réversible à tout moment (avant le jury de fin de semestre)
+
+
Il peut s'agit d'étudiants redoublants ayant déjà acquis l'UE, ou d'une UE
+ présente dans le semestre mais pas dans le parcours de l'étudiant, ou bien d'autres
+ cas particuliers.
+
+
La dispense d'UE est réversible à tout moment (avant le jury de fin de semestre)
et n'affecte pas les notes saisies.
+
"""
diff --git a/app/scodoc/sco_recapcomplet.py b/app/scodoc/sco_recapcomplet.py
index af91313ff5..549b375d1c 100644
--- a/app/scodoc/sco_recapcomplet.py
+++ b/app/scodoc/sco_recapcomplet.py
@@ -242,7 +242,19 @@ def formsemestre_recapcomplet(
"""
)
-
+ # Légende
+ H.append(
+ """
+
+
Codes utilisés dans cette table:
+
+
~
valeur manquante
+
=
UE dispensée
+
nan
valeur non disponible
+
+
+ """
+ )
H.append(html_sco_header.sco_footer())
# HTML or binary data ?
if len(H) > 1:
diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css
index 1129225d71..34c9d05e34 100644
--- a/app/static/css/scodoc.css
+++ b/app/static/css/scodoc.css
@@ -2672,6 +2672,30 @@ table.notes_recapcomplet a:hover {
text-decoration: underline;
}
+div.table_recap_caption {
+ width: fit-content;
+ padding: 8px;
+ border-radius: 8px;
+ background-color: rgb(202, 255, 180);
+}
+
+div.table_recap_caption div.title {
+ font-weight: bold;
+}
+
+div.table_recap_caption div.captions {
+ display: grid;
+ grid-template-columns: 48px 200px;
+}
+
+div.table_recap_caption div.captions div:nth-child(odd) {
+ text-align: center;
+}
+
+div.table_recap_caption div.captions div:nth-child(even) {
+ font-style: italic;
+}
+
/* bulletin */
div.notes_bulletin {
margin-right: 5px;
diff --git a/app/tables/jury_recap.py b/app/tables/jury_recap.py
index 2be036a68b..5d9e5b8858 100644
--- a/app/tables/jury_recap.py
+++ b/app/tables/jury_recap.py
@@ -154,7 +154,7 @@ class TableJury(TableRecap):
niveau: ApcNiveau = validation_rcue.niveau()
titre = f"C{niveau.competence.numero}" # à voir (nommer les compétences...)
row.add_cell(
- f"c_{competence_id}_annee",
+ f"c_{competence_id}_{annee}",
titre,
validation_rcue.code,
group="cursus_" + annee,
diff --git a/app/tables/recap.py b/app/tables/recap.py
index 989629ae19..ca55a10196 100644
--- a/app/tables/recap.py
+++ b/app/tables/recap.py
@@ -285,9 +285,9 @@ class TableRecap(tb.Table):
notes = res.modimpl_notes(modimpl.id, ue.id)
if np.isnan(notes).all():
# aucune note valide
- row_min.add_cell(col_id, None, np.nan)
- row_max.add_cell(col_id, None, np.nan)
- moy = np.nan
+ row_min.add_cell(col_id, None, "")
+ row_max.add_cell(col_id, None, "")
+ moy = ""
else:
row_min.add_cell(col_id, None, self.fmt_note(np.nanmin(notes)))
row_max.add_cell(col_id, None, self.fmt_note(np.nanmax(notes)))
@@ -297,7 +297,7 @@ class TableRecap(tb.Table):
None,
self.fmt_note(moy),
# aucune note dans ce module ?
- classes=["col_empty" if np.isnan(moy) else ""],
+ classes=["col_empty" if (moy == "" or np.isnan(moy)) else ""],
)
row_apo.add_cell(col_id, None, modimpl.module.code_apogee or "")
@@ -618,7 +618,7 @@ class RowRecap(tb.Row):
):
"""Ajoute cols moy_gen moy_ue et tous les modules..."""
etud = self.etud
- table = self.table
+ table: TableRecap = self.table
res = table.res
# --- Si DEM ou DEF, ne montre aucun résultat d'UE ni moy. gen.
if res.get_etud_etat(etud.id) != scu.INSCRIT:
@@ -701,13 +701,17 @@ class RowRecap(tb.Row):
def add_ue_cols(self, ue: UniteEns, ue_status: dict, col_group: str = None):
"Ajoute résultat UE au row (colonne col_ue)"
# sous-classé par JuryRow pour ajouter les codes
- table = self.table
+ table: TableRecap = self.table
formsemestre: FormSemestre = table.res.formsemestre
table.group_titles[
"col_ue"
] = f"UEs du S{formsemestre.semestre_id} {formsemestre.annee_scolaire()}"
col_id = f"moy_ue_{ue.id}"
- val = ue_status["moy"]
+ val = (
+ ue_status["moy"]
+ if (self.etud.id, ue.id) not in table.res.dispense_ues
+ else "="
+ )
note_classes = []
if isinstance(val, float):
if val < table.barre_moy:
diff --git a/tests/ressources/yaml/cursus_but_gccd_cy.yaml b/tests/ressources/yaml/cursus_but_gccd_cy.yaml
index 614c21e382..fe52f10b1a 100644
--- a/tests/ressources/yaml/cursus_but_gccd_cy.yaml
+++ b/tests/ressources/yaml/cursus_but_gccd_cy.yaml
@@ -134,7 +134,7 @@ FormSemestres:
codes_parcours: ['BAT', 'TP']
Etudiants:
- A_ok: # Etudiant qui va tout valider directement
+ A_ok: # Etudiant parcours BAT qui va tout valider directement
prenom: Étudiant_BAT
civilite: M
formsemestres:
@@ -157,9 +157,41 @@ Etudiants:
S5:
parcours: BAT
+ dispense_ues: ['UE5.2']
notes_modules:
"R5.01": 15 # toutes UE
"SAÉ 5.BAT.01": 10 # UE5.1
"SAÉ 5.BAT.02": 11 # UE5.4
S6:
parcours: BAT
+
+ B_ok: # Etudiant parcours TP qui va tout valider directement
+ prenom: Étudiant_TP
+ civilite: M
+ formsemestres:
+ S1:
+ parcours: TP
+ notes_modules:
+ "R1.01": 11 # toutes UEs
+ S2:
+ parcours: TP
+ notes_modules:
+ "R2.01": 12 # toutes UEs
+ S3:
+ parcours: TP
+ notes_modules:
+ "R3.01": 13 # toutes UEs
+ S4:
+ parcours: TP
+ notes_modules:
+ "R4.01": 14 # toutes UE
+
+ S5:
+ parcours: TP
+ dispense_ues: ['UE5.1']
+ notes_modules:
+ "R5.01": 15 # toutes UE
+ "SAÉ 5.BAT.01": 10 # UE5.1
+ "SAÉ 5.BAT.02": 11 # UE5.4
+ S6:
+ parcours: TP
diff --git a/tests/unit/yaml_setup.py b/tests/unit/yaml_setup.py
index 0f8544ff99..19696acd67 100644
--- a/tests/unit/yaml_setup.py
+++ b/tests/unit/yaml_setup.py
@@ -53,12 +53,14 @@ from app.auth.models import User
from app.models import (
ApcParcours,
+ DispenseUE,
Evaluation,
Formation,
FormSemestre,
Identite,
Module,
ModuleImpl,
+ UniteEns,
)
from app.scodoc import sco_formations
@@ -263,6 +265,9 @@ def inscrit_les_etudiants(formation: Formation, doc: dict):
group_ids = [group.id]
else:
group_ids = []
+ # Génère des dispenses d'UEs
+ if "dispense_ues" in sem_infos:
+ etud_dispense_ues(formsemestre, etud, sem_infos["dispense_ues"])
sco_formsemestre_inscriptions.do_formsemestre_inscription_with_modules(
formsemestre.id,
etud.id,
@@ -275,6 +280,19 @@ def inscrit_les_etudiants(formation: Formation, doc: dict):
formsemestre.update_inscriptions_parcours_from_groups()
+def etud_dispense_ues(
+ formsemestre: FormSemestre, etud: Identite, ue_acronymes: list[str]
+):
+ """Génère des dispenses d'UE"""
+ for ue_acronyme in set(ue_acronymes):
+ ue: UniteEns = formsemestre.formation.ues.filter_by(
+ acronyme=ue_acronyme
+ ).first()
+ assert ue
+ disp = DispenseUE(formsemestre_id=formsemestre.id, ue_id=ue.id, etudid=etud.id)
+ db.session.add(disp)
+
+
def setup_from_yaml(filename: str) -> dict:
"""Lit le fichier yaml et construit l'ensemble des objets"""
with open(filename, encoding="utf-8") as f: