Dispenses d'UE: corrige affichage en table recap. Intègre aux tests unitaires cursus. Légendes.

This commit is contained in:
Emmanuel Viennet 2023-03-27 23:57:10 +02:00
parent 5103f162a7
commit c889ba3d1d
8 changed files with 112 additions and 15 deletions

View File

@ -260,8 +260,10 @@ class UniteEns(db.Model):
class DispenseUE(db.Model): class DispenseUE(db.Model):
"""Dispense d'UE """Dispense d'UE
Utilisé en APC (BUT) pour indiquer les étudiants redoublants avec une UE capitalisée Utilisé en APC (BUT) pour indiquer
qu'ils ne refont pas. - 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: 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 n'est pas affectée par les décisions de jury (pas effacée)
- elle est associée à un formsemestre - elle est associée à un formsemestre

View File

@ -613,11 +613,16 @@ def _list_but_ue_inscriptions(res: NotesTableCompat, read_only: bool = True) ->
</table> </table>
</form> </form>
<div class="help"> <div class="help">
L'inscription ou désinscription aux UEs du BUT n'affecte pas les inscriptions aux modules <p>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. 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. </p>
La dispense d'UE est réversible à tout moment (avant le jury de fin de semestre) <p>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.
</p>
<p>La dispense d'UE est réversible à tout moment (avant le jury de fin de semestre)
et n'affecte pas les notes saisies. et n'affecte pas les notes saisies.
</p>
</div> </div>
</div> </div>
""" """

View File

@ -242,7 +242,19 @@ def formsemestre_recapcomplet(
</div> </div>
""" """
) )
# Légende
H.append(
"""
<div class="table_recap_caption">
<div class="title">Codes utilisés dans cette table:</div>
<div class="captions">
<div><tt>~</tt></div><div>valeur manquante</div>
<div><tt>=</tt></div><div>UE dispensée</div>
<div><tt>nan</tt></div><div>valeur non disponible</div>
</div>
</div>
"""
)
H.append(html_sco_header.sco_footer()) H.append(html_sco_header.sco_footer())
# HTML or binary data ? # HTML or binary data ?
if len(H) > 1: if len(H) > 1:

View File

@ -2672,6 +2672,30 @@ table.notes_recapcomplet a:hover {
text-decoration: underline; 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 */ /* bulletin */
div.notes_bulletin { div.notes_bulletin {
margin-right: 5px; margin-right: 5px;

View File

@ -154,7 +154,7 @@ class TableJury(TableRecap):
niveau: ApcNiveau = validation_rcue.niveau() niveau: ApcNiveau = validation_rcue.niveau()
titre = f"C{niveau.competence.numero}" # à voir (nommer les compétences...) titre = f"C{niveau.competence.numero}" # à voir (nommer les compétences...)
row.add_cell( row.add_cell(
f"c_{competence_id}_annee", f"c_{competence_id}_{annee}",
titre, titre,
validation_rcue.code, validation_rcue.code,
group="cursus_" + annee, group="cursus_" + annee,

View File

@ -285,9 +285,9 @@ class TableRecap(tb.Table):
notes = res.modimpl_notes(modimpl.id, ue.id) notes = res.modimpl_notes(modimpl.id, ue.id)
if np.isnan(notes).all(): if np.isnan(notes).all():
# aucune note valide # aucune note valide
row_min.add_cell(col_id, None, np.nan) row_min.add_cell(col_id, None, "")
row_max.add_cell(col_id, None, np.nan) row_max.add_cell(col_id, None, "")
moy = np.nan moy = ""
else: else:
row_min.add_cell(col_id, None, self.fmt_note(np.nanmin(notes))) 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))) row_max.add_cell(col_id, None, self.fmt_note(np.nanmax(notes)))
@ -297,7 +297,7 @@ class TableRecap(tb.Table):
None, None,
self.fmt_note(moy), self.fmt_note(moy),
# aucune note dans ce module ? # 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 "") 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...""" """Ajoute cols moy_gen moy_ue et tous les modules..."""
etud = self.etud etud = self.etud
table = self.table table: TableRecap = self.table
res = table.res res = table.res
# --- Si DEM ou DEF, ne montre aucun résultat d'UE ni moy. gen. # --- Si DEM ou DEF, ne montre aucun résultat d'UE ni moy. gen.
if res.get_etud_etat(etud.id) != scu.INSCRIT: 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): def add_ue_cols(self, ue: UniteEns, ue_status: dict, col_group: str = None):
"Ajoute résultat UE au row (colonne col_ue)" "Ajoute résultat UE au row (colonne col_ue)"
# sous-classé par JuryRow pour ajouter les codes # sous-classé par JuryRow pour ajouter les codes
table = self.table table: TableRecap = self.table
formsemestre: FormSemestre = table.res.formsemestre formsemestre: FormSemestre = table.res.formsemestre
table.group_titles[ table.group_titles[
"col_ue" "col_ue"
] = f"UEs du S{formsemestre.semestre_id} {formsemestre.annee_scolaire()}" ] = f"UEs du S{formsemestre.semestre_id} {formsemestre.annee_scolaire()}"
col_id = f"moy_ue_{ue.id}" 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 = [] note_classes = []
if isinstance(val, float): if isinstance(val, float):
if val < table.barre_moy: if val < table.barre_moy:

View File

@ -134,7 +134,7 @@ FormSemestres:
codes_parcours: ['BAT', 'TP'] codes_parcours: ['BAT', 'TP']
Etudiants: Etudiants:
A_ok: # Etudiant qui va tout valider directement A_ok: # Etudiant parcours BAT qui va tout valider directement
prenom: Étudiant_BAT prenom: Étudiant_BAT
civilite: M civilite: M
formsemestres: formsemestres:
@ -157,9 +157,41 @@ Etudiants:
S5: S5:
parcours: BAT parcours: BAT
dispense_ues: ['UE5.2']
notes_modules: notes_modules:
"R5.01": 15 # toutes UE "R5.01": 15 # toutes UE
"SAÉ 5.BAT.01": 10 # UE5.1 "SAÉ 5.BAT.01": 10 # UE5.1
"SAÉ 5.BAT.02": 11 # UE5.4 "SAÉ 5.BAT.02": 11 # UE5.4
S6: S6:
parcours: BAT 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

View File

@ -53,12 +53,14 @@ from app.auth.models import User
from app.models import ( from app.models import (
ApcParcours, ApcParcours,
DispenseUE,
Evaluation, Evaluation,
Formation, Formation,
FormSemestre, FormSemestre,
Identite, Identite,
Module, Module,
ModuleImpl, ModuleImpl,
UniteEns,
) )
from app.scodoc import sco_formations from app.scodoc import sco_formations
@ -263,6 +265,9 @@ def inscrit_les_etudiants(formation: Formation, doc: dict):
group_ids = [group.id] group_ids = [group.id]
else: else:
group_ids = [] 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( sco_formsemestre_inscriptions.do_formsemestre_inscription_with_modules(
formsemestre.id, formsemestre.id,
etud.id, etud.id,
@ -275,6 +280,19 @@ def inscrit_les_etudiants(formation: Formation, doc: dict):
formsemestre.update_inscriptions_parcours_from_groups() 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: def setup_from_yaml(filename: str) -> dict:
"""Lit le fichier yaml et construit l'ensemble des objets""" """Lit le fichier yaml et construit l'ensemble des objets"""
with open(filename, encoding="utf-8") as f: with open(filename, encoding="utf-8") as f: