mise à jour master

This commit is contained in:
iziram 2023-03-22 10:47:06 +01:00
parent cdcb4a2468
commit 98e709bfd0
41 changed files with 361 additions and 4361 deletions

View File

@ -477,11 +477,8 @@ def formsemestre_resultat(formsemestre_id: int):
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
app.set_sco_dept(formsemestre.departement.acronym)
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
table = res.get_table_recap(
convert_values=convert_values,
include_evaluations=False,
mode_jury=False,
allow_html=False,
table = TableRecap(
res, convert_values=convert_values, include_evaluations=False, mode_jury=False
)
# Supprime les champs inutiles (mise en forme)
rows = table.to_list()

View File

@ -650,10 +650,13 @@ class DecisionsProposeesAnnee(DecisionsProposees):
à poursuivre après le semestre courant.
"""
# La poursuite d'études dans un semestre pair dune même année
# est de droit pour tout étudiant:
if (self.formsemestre.semestre_id % 2) and sco_codes.CursusBUT.NB_SEM:
ids.add(self.formsemestre.semestre_id + 1)
# est de droit pour tout étudiant.
# Pas de redoublements directs de S_impair vers S_impair
# (pourront être traités manuellement)
if (
self.formsemestre.semestre_id % 2
) and self.formsemestre.semestre_id < sco_codes.CursusBUT.NB_SEM:
return {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 dUE ;
@ -667,7 +670,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
if (
self.jury_annuel
and code in sco_codes.BUT_CODES_PASSAGE
and self.formsemestre_pair.semestre_id < sco_codes.CursusBUT.NB_SEM
and self.formsemestre.semestre_id < sco_codes.CursusBUT.NB_SEM
):
ids.add(self.formsemestre.semestre_id + 1)

View File

@ -97,12 +97,17 @@ def pvjury_page_but(formsemestre_id: int, fmt="html"):
def pvjury_table_but(
formsemestre: FormSemestre, etudids: list[int] = None, line_sep: str = "\n"
formsemestre: FormSemestre,
etudids: list[int] = None,
line_sep: str = "\n",
only_diplome=False,
anonymous=False,
with_paragraph_nom=False,
) -> tuple[list[dict], dict]:
"""Table avec résultats jury BUT pour PV.
Si etudids est None, prend tous les étudiants inscrits.
"""
# remplace pour le BUT la fonction sco_pvjury.pvjury_table
# remplace pour le BUT la fonction sco_pv_forms.pvjury_table
annee_but = (formsemestre.semestre_id + 1) // 2
titles = {
"nom": "Code" if anonymous else "Nom",

View File

@ -1,565 +0,0 @@
##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
"""Jury BUT: table recap annuelle et liens saisie
"""
import collections
import time
import numpy as np
from flask import g, url_for
from app.but import jury_but
from app.but.jury_but import (
DecisionsProposeesAnnee,
DecisionsProposeesRCUE,
DecisionsProposeesUE,
)
from app.comp.res_but import ResultatsSemestreBUT
from app.comp import res_sem
from app.models.etudiants import Identite
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences
from app.models.formsemestre import FormSemestre
from app.scodoc import html_sco_header
from app.scodoc.sco_codes_parcours import (
BUT_BARRE_RCUE,
BUT_BARRE_UE,
BUT_BARRE_UE8,
BUT_RCUE_SUFFISANT,
)
from app.scodoc import sco_formsemestre_status
from app.scodoc import sco_pvjury
from app.scodoc import sco_utils as scu
from app.scodoc import table_builder as tb
class TableJury(tb.Table):
pass
class RowJury(tb.Row):
"Ligne de la table saisie jury"
def add_nb_rcues_cell(self, deca: DecisionsProposeesAnnee):
"cell avec nb niveaux validables / total"
classes = ["col_rcue", "col_rcues_validables"]
if deca.nb_rcues_under_8 > 0:
classes.append("moy_ue_warning")
elif deca.nb_validables < deca.nb_competences:
classes.append("moy_ue_inf")
else:
classes.append("moy_ue_valid")
if len(deca.rcues_annee) > 0:
# permet un tri par nb de niveaux validables + moyenne gen indicative S_pair
if deca.res_pair and deca.etud.id in deca.res_pair.etud_moy_gen:
moy = deca.res_pair.etud_moy_gen[deca.etud.id]
if np.isnan(moy):
moy_gen_d = "x"
else:
moy_gen_d = f"{int(moy*1000):05}"
else:
moy_gen_d = "x"
order = f"{deca.nb_validables:04d}-{moy_gen_d}"
else:
# étudiants sans RCUE: pas de semestre impair, ...
# les classe à la fin
order = f"{deca.nb_validables:04d}-00000-{deca.etud.sort_key}"
self.add_cell(
"rcues_validables",
"RCUEs",
f"""{deca.nb_validables}/{deca.nb_competences}"""
+ ((" " + scu.EMO_WARNING) if deca.nb_rcues_under_8 > 0 else ""),
group="rcues_validables",
classes=classes,
data={"order": order},
)
def add_ue_cells(self, dec_ue: DecisionsProposeesUE):
"cell de moyenne d'UE"
col_id = f"moy_ue_{dec_ue.ue.id}"
note_class = ""
val = dec_ue.moy_ue
if isinstance(val, float):
if val < BUT_BARRE_UE:
note_class = "moy_inf"
elif val >= BUT_BARRE_UE:
note_class = "moy_ue_valid"
if val < BUT_BARRE_UE8:
note_class = "moy_ue_warning" # notes très basses
self.add_cell(
col_id,
dec_ue.ue.acronyme,
self.fmt_note(val),
group="col_ue",
"col_ue" + note_class,
column_class="col_ue",
)
self.add_cell(
col_id + "_code",
dec_ue.ue.acronyme,
dec_ue.code_valide or "",
"col_ue_code recorded_code",
column_class="col_ue",
)
def formsemestre_saisie_jury_but(
formsemestre2: FormSemestre,
read_only: bool = False,
selected_etudid: int = None,
mode="jury",
) -> str:
"""formsemestre est un semestre PAIR
Si readonly, ne montre pas le lien "saisir la décision"
=> page html complète
Si mode == "recap", table recap des codes, sans liens de saisie.
"""
# Quick & Dirty
# pour chaque etud de res2 trié
# S1: UE1, ..., UEn
# S2: UE1, ..., UEn
#
# UE1_s1, UE1_s2, moy_rcue, UE2... , Nbrcue_validables, Nbrcue<8, passage_de_droit, valide_moitie_rcue
#
# Pour chaque etud de res2 trié
# DecisionsProposeesAnnee(etud, formsemestre2)
# Pour le 1er etud, faire un check_ues_ready_jury(self) -> page d'erreur
# -> rcue .ue_1, .ue_2 -> stroe moy ues, rcue.moy_rcue, etc
if formsemestre2.formation.referentiel_competence is None:
raise ScoNoReferentielCompetences(formation=formsemestre2.formation)
rows, titles, column_ids, jury_stats = get_jury_but_table(
formsemestre2, read_only=read_only, mode=mode
)
if not rows:
return (
'<div class="table_recap"><div class="message">aucun étudiant !</div></div>'
)
filename = scu.sanitize_filename(
f"""jury-but-{formsemestre2.titre_num()}-{time.strftime("%Y-%m-%d")}"""
)
klass = "table_jury_but_bilan" if mode == "recap" else ""
table_html = build_table_jury_but_html(
filename, rows, titles, column_ids, selected_etudid=selected_etudid, klass=klass
)
H = [
html_sco_header.sco_header(
page_title=f"{formsemestre2.sem_modalite()}: jury BUT annuel",
no_side_bar=True,
init_qtip=True,
javascripts=["js/etud_info.js", "js/table_recap.js"],
),
sco_formsemestre_status.formsemestre_status_head(
formsemestre_id=formsemestre2.id
),
]
if mode == "recap":
H.append(
f"""<h3>Décisions de jury enregistrées pour les étudiants de ce semestre</h3>
<div class="table_jury_but_links">
<div>
<ul>
<li><a href="{url_for(
"notes.pvjury_table_but",
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre2.id)
}" class="stdlink">Tableau PV de jury</a>
</li>
<li><a href="{url_for(
"notes.formsemestre_lettres_individuelles",
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre2.id)
}" class="stdlink">Courriers individuels (classeur pdf)</a>
</li>
</div>
</div>
"""
)
H.append(
f"""
{table_html}
<div class="table_jury_but_links">
"""
)
if (mode == "recap") and not read_only:
H.append(
f"""
<p><a class="stdlink" href="{url_for(
"notes.formsemestre_saisie_jury",
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre2.id)
}">Saisie des décisions du jury</a>
</p>"""
)
else:
H.append(
f"""
<p><a class="stdlink" href="{url_for(
"notes.formsemestre_validation_auto_but",
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre2.id)
}">Calcul automatique des décisions du jury</a>
</p>
<p><a class="stdlink" href="{url_for(
"notes.formsemestre_jury_but_recap",
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre2.id)
}">Tableau récapitulatif des décisions du jury</a>
</p>
"""
)
H.append(
f"""
</div>
<div class="jury_stats">
<div>Nb d'étudiants avec décision annuelle:
{sum(jury_stats["codes_annuels"].values())} / {jury_stats["nb_etuds"]}
</div>
<div><b>Codes annuels octroyés:</b></div>
<table class="jury_stats_codes">
"""
)
for code in sorted(jury_stats["codes_annuels"].keys()):
H.append(
f"""<tr>
<td>{code}</td>
<td style="text-align:right">{jury_stats["codes_annuels"][code]}</td>
<td style="text-align:right">{
(100*jury_stats["codes_annuels"][code] / jury_stats["nb_etuds"]):2.1f}%
</td>
</tr>"""
)
H.append(
f"""
</table>
</div>
{html_sco_header.sco_footer()}
"""
)
return "\n".join(H)
def build_table_jury_but_html(
filename: str, rows, titles, column_ids, selected_etudid: int = None, klass=""
) -> str:
"""assemble la table html"""
footer_rows = [] # inutilisé pour l'instant
H = [
f"""<div class="table_recap"><table class="table_recap apc jury table_jury_but {klass}"
data-filename="{filename}">"""
]
# header
H.append(
f"""
<thead>
{scu.gen_row(column_ids, titles, "th")}
</thead>
"""
)
# body
H.append("<tbody>")
for row in rows:
H.append(f"{scu.gen_row(column_ids, row, selected_etudid=selected_etudid)}\n")
H.append("</tbody>\n")
# footer
H.append("<tfoot>")
idx_last = len(footer_rows) - 1
for i, row in enumerate(footer_rows):
H.append(f'{scu.gen_row(column_ids, row, "th" if i == idx_last else "td")}\n')
H.append(
"""
</tfoot>
</table>
</div>
"""
)
return "".join(H)
class RowCollector:
"""Une ligne de la table"""
def __init__(
self,
cells: dict = None,
titles: dict = None,
convert_values=True,
column_classes: dict = None,
):
self.titles = titles
self.row = cells or {} # col_id : str
self.column_classes = column_classes # col_id : str, css class
self.idx = 0
self.last_etud_cell_idx = 0
if convert_values:
self.fmt_note = scu.fmt_note
else:
self.fmt_note = lambda x: x
def __setitem__(self, key, value):
self.row[key] = value
def __getitem__(self, key):
return self.row[key]
def get_row_dict(self):
"La ligne, comme un dict"
# create empty cells
for col_id in self.titles:
if col_id not in self.row:
self.row[col_id] = ""
klass = self.column_classes.get(col_id)
if klass:
self.row[f"_{col_id}_class"] = klass
return self.row
def add_cell(
self,
col_id: str,
title: str,
content: str,
classes: str = "",
idx: int = None,
column_class="",
):
"""Add a row to our table. classes is a list of css class names"""
self.idx = idx if idx is not None else self.idx
self.row[col_id] = content
if classes:
self.row[f"_{col_id}_class"] = classes + f" c{self.idx}"
if not col_id in self.titles:
self.titles[col_id] = title
self.titles[f"_{col_id}_col_order"] = self.idx
if classes:
self.titles[f"_{col_id}_class"] = classes
self.column_classes[col_id] = column_class
self.idx += 1
# def add_etud_cells(
# self, etud: Identite, formsemestre: FormSemestre, with_links=True
# ):
# "Les cells code, nom, prénom etc."
# # --- Codes (seront cachés, mais exportés en excel)
# self.add_cell("etudid", "etudid", etud.id, "codes")
# self.add_cell("code_nip", "code_nip", etud.code_nip or "", "codes")
# # --- Identité étudiant (adapté de res_common/get_table_recap, à factoriser XXX TODO)
# self.add_cell("civilite_str", "Civ.", etud.civilite_str, "identite_detail")
# self.add_cell("nom_disp", "Nom", etud.nom_disp(), "identite_detail")
# self["_nom_disp_order"] = etud.sort_key
# self.add_cell("prenom", "Prénom", etud.prenom, "identite_detail")
# self.add_cell("nom_short", "Nom", etud.nom_short, "identite_court")
# self["_nom_short_data"] = {
# "etudid": etud.id,
# "nomprenom": etud.nomprenom,
# }
# if with_links:
# self["_nom_short_order"] = etud.sort_key
# self["_nom_short_target"] = url_for(
# "notes.formsemestre_bulletinetud",
# scodoc_dept=g.scodoc_dept,
# formsemestre_id=formsemestre.id,
# etudid=etud.id,
# )
# self["_nom_short_target_attrs"] = f'class="etudinfo" id="{etud.id}"'
# self["_nom_disp_target"] = self["_nom_short_target"]
# self["_nom_disp_target_attrs"] = self["_nom_short_target_attrs"]
# self.last_etud_cell_idx = self.idx
def add_rcue_cells(self, dec_rcue: DecisionsProposeesRCUE):
"2 cells: moyenne du RCUE, code enregistré"
rcue = dec_rcue.rcue
col_id = f"moy_rcue_{rcue.ue_1.niveau_competence_id}" # le niveau_id
note_class = ""
val = rcue.moy_rcue
if isinstance(val, float):
if val < BUT_BARRE_RCUE:
note_class = " moy_ue_inf"
elif val >= BUT_BARRE_RCUE:
note_class = " moy_ue_valid"
if val < BUT_RCUE_SUFFISANT:
note_class = " moy_ue_warning" # notes très basses
self.add_cell(
col_id,
f"<div>{rcue.ue_1.acronyme}</div><div>{rcue.ue_2.acronyme}</div>",
self.fmt_note(val),
"col_rcue" + note_class,
column_class="col_rcue",
)
self.add_cell(
col_id + "_code",
f"<div>{rcue.ue_1.acronyme}</div><div>{rcue.ue_2.acronyme}</div>",
dec_rcue.code_valide or "",
"col_rcue_code recorded_code",
column_class="col_rcue",
)
def get_jury_but_table(
formsemestre2: FormSemestre, read_only: bool = False, mode="jury", with_links=True
) -> tuple[list[dict], list[str], list[str], dict]:
"""Construit la table des résultats annuels pour le jury BUT
=> rows_dict, titles, column_ids, jury_stats
jury_stats est un dict donnant des comptages sur le jury.
"""
res2: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre2)
titles = {} # column_id : title
jury_stats = {
"nb_etuds": len(formsemestre2.etuds_inscriptions),
"codes_annuels": collections.Counter(),
}
table = TableJury(res2, mode_jury=True)
for etudid in formsemestre2.etuds_inscriptions:
etud: Identite = Identite.query.get(etudid)
deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre2)
# XXX row = RowCollector(titles=titles, column_classes=column_classes)
row = RowJury(table, etudid)
table.add_row(row)
row.add_etud(etud)
# --- Nombre de niveaux
row.add_nb_rcues_cell(deca)
# --- Les RCUEs
for rcue in deca.rcues_annee:
dec_rcue = deca.dec_rcue_by_ue.get(rcue.ue_1.id)
if dec_rcue is not None: # None si l'UE n'est pas associée à un niveau
row.add_ue_cells(deca.decisions_ues[rcue.ue_1.id])
row.add_ue_cells(deca.decisions_ues[rcue.ue_2.id])
row.add_rcue_cells(dec_rcue)
# --- Les ECTS validés
ects_valides = 0.0
if deca.res_impair:
ects_valides += deca.res_impair.get_etud_ects_valides(etudid)
if deca.res_pair:
ects_valides += deca.res_pair.get_etud_ects_valides(etudid)
row.add_cell(
"ects_annee",
"ECTS",
f"""{int(ects_valides)}""",
"col_code_annee",
)
# --- Le code annuel existant
row.add_cell(
"code_annee",
"Année",
f"""{deca.code_valide or ''}""",
"col_code_annee",
)
if deca.code_valide:
jury_stats["codes_annuels"][deca.code_valide] += 1
# --- Le lien de saisie
if mode != "recap" and with_links:
row.add_cell(
"lien_saisie",
"",
f"""
<a href="{url_for(
'notes.formsemestre_validation_but',
scodoc_dept=g.scodoc_dept,
etudid=etud.id,
formsemestre_id=formsemestre2.id,
)}" class="stdlink">
{"voir" if read_only else ("modif." if deca.code_valide else "saisie")}
décision</a>
"""
if deca.inscription_etat == scu.INSCRIT
else deca.inscription_etat,
"col_lien_saisie_but",
)
rows.append(row)
rows_dict = [row.get_row_dict() for row in rows]
if len(rows_dict) > 0:
col_idx = res2.recap_add_partitions(
rows_dict, titles, col_idx=row.last_etud_cell_idx + 1
)
res2.recap_add_cursus(rows_dict, titles, col_idx=col_idx + 1)
column_ids = [title for title in titles if not title.startswith("_")]
column_ids.sort(key=lambda col_id: titles.get("_" + col_id + "_col_order", 1000))
rows_dict.sort(key=lambda row: row["_nom_disp_order"])
return rows_dict, titles, column_ids, jury_stats
def get_jury_but_results(formsemestre: FormSemestre) -> list[dict]:
"""Liste des résultats jury BUT sous forme de dict, pour API"""
if formsemestre.formation.referentiel_competence is None:
# pas de ref. comp., donc pas de decisions de jury (ne lance pas d'exception)
return []
dpv = sco_pvjury.dict_pvjury(formsemestre.id)
rows = []
for etudid in formsemestre.etuds_inscriptions:
rows.append(get_jury_but_etud_result(formsemestre, dpv, etudid))
return rows
def get_jury_but_etud_result(
formsemestre: FormSemestre, dpv: dict, etudid: int
) -> dict:
"""Résultats de jury d'un étudiant sur un semestre pair de BUT"""
etud: Identite = Identite.query.get(etudid)
dec_etud = dpv["decisions_dict"][etudid]
if formsemestre.formation.is_apc():
deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre)
else:
deca = None
row = {
"etudid": etud.id,
"code_nip": etud.code_nip,
"code_ine": etud.code_ine,
"is_apc": dpv["is_apc"], # BUT ou classic ?
"etat": dec_etud["etat"], # I ou D ou DEF
"nb_competences": deca.nb_competences if deca else 0,
}
# --- Les RCUEs
rcue_list = []
if deca:
for rcue in deca.rcues_annee:
dec_rcue = deca.dec_rcue_by_ue.get(rcue.ue_1.id)
if dec_rcue is not None: # None si l'UE n'est pas associée à un niveau
dec_ue1 = deca.decisions_ues[rcue.ue_1.id]
dec_ue2 = deca.decisions_ues[rcue.ue_2.id]
rcue_dict = {
"ue_1": {
"ue_id": rcue.ue_1.id,
"moy": None
if (dec_ue1.moy_ue is None or np.isnan(dec_ue1.moy_ue))
else dec_ue1.moy_ue,
"code": dec_ue1.code_valide,
},
"ue_2": {
"ue_id": rcue.ue_2.id,
"moy": None
if (dec_ue2.moy_ue is None or np.isnan(dec_ue2.moy_ue))
else dec_ue2.moy_ue,
"code": dec_ue2.code_valide,
},
"moy": rcue.moy_rcue,
"code": dec_rcue.code_valide,
}
rcue_list.append(rcue_dict)
row["rcues"] = rcue_list
# --- Les UEs
ue_list = []
if dec_etud["decisions_ue"]:
for ue_id, ue_dec in dec_etud["decisions_ue"].items():
ue_dict = {
"ue_id": ue_id,
"code": ue_dec["code"],
"ects": ue_dec["ects"],
}
ue_list.append(ue_dict)
row["ues"] = ue_list
# --- Le semestre (pour les formations classiques)
if dec_etud["decision_sem"]:
row["semestre"] = {"code": dec_etud["decision_sem"].get("code")}
else:
row["semestre"] = {} # APC, ...
# --- Autorisations
row["autorisations"] = dec_etud["autorisations"]
return row

View File

@ -12,7 +12,7 @@ import numpy as np
from app.but import jury_but
from app.models.etudiants import Identite
from app.models.formsemestre import FormSemestre
from app.scodoc import sco_dict_pv_jury
from app.scodoc import sco_pv_dict
def get_jury_but_results(formsemestre: FormSemestre) -> list[dict]:
@ -20,7 +20,7 @@ def get_jury_but_results(formsemestre: FormSemestre) -> list[dict]:
if formsemestre.formation.referentiel_competence is None:
# pas de ref. comp., donc pas de decisions de jury (ne lance pas d'exception)
return []
dpv = sco_dict_pv_jury.dict_pvjury(formsemestre.id)
dpv = sco_pv_dict.dict_pvjury(formsemestre.id)
rows = []
for etudid in formsemestre.etuds_inscriptions:
rows.append(_get_jury_but_etud_result(formsemestre, dpv, etudid))

View File

@ -33,10 +33,7 @@ import pandas as pd
from app import db
from app import models
from app.models import (
DispenseUE,
FormSemestre,
FormSemestreInscription,
Identite,
Module,
ModuleImpl,
ModuleUECoef,
@ -215,31 +212,6 @@ def notes_sem_load_cube(formsemestre: FormSemestre) -> tuple:
)
def load_dispense_ues(
formsemestre: FormSemestre, etudids: pd.Index, ues: list[UniteEns]
) -> set[tuple[int, int]]:
"""Construit l'ensemble des
etudids = modimpl_inscr_df.index, # les etudids
ue_ids : modimpl_coefs_df.index, # les UE du formsemestre sans les UE bonus sport
Résultat: set de (etudid, ue_id).
"""
dispense_ues = set()
ue_sem_by_code = {ue.ue_code: ue for ue in ues}
# Prend toutes les dispenses obtenues par des étudiants de ce formsemestre,
# puis filtre sur inscrits et code d'UE UE
for dispense_ue in DispenseUE.query.join(
Identite, FormSemestreInscription
).filter_by(formsemestre_id=formsemestre.id):
if dispense_ue.etudid in etudids:
# UE dans le semestre avec même code ?
ue = ue_sem_by_code.get(dispense_ue.ue.ue_code)
if ue is not None:
dispense_ues.add((dispense_ue.etudid, ue.id))
return dispense_ues
def compute_ue_moys_apc(
sem_cube: np.array,
etuds: list,

View File

@ -74,7 +74,7 @@ class ResultatsSemestreBUT(NotesTableCompat):
modimpl.module.ue.type != UE_SPORT
for modimpl in self.formsemestre.modimpls_sorted
]
self.dispense_ues = moy_ue.load_dispense_ues(
self.dispense_ues = DispenseUE.load_formsemestre_dispense_ues_set(
self.formsemestre, self.modimpl_inscr_df.index, self.ues
)
self.etud_moy_ue = moy_ue.compute_ue_moys_apc(

View File

@ -28,7 +28,6 @@ from app.scodoc.sco_cache import ResultatsSemestreCache
from app.scodoc.codes_cursus import UE_SPORT
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc import sco_utils as scu
from app.scodoc import table_builder as tb
# Il faut bien distinguer
# - ce qui est caché de façon persistente (via redis):
@ -454,713 +453,3 @@ class ResultatsSemestre(ResultatsCache):
# ici si l'étudiant est inscrit dans le semestre courant,
# somme des coefs des modules de l'UE auxquels il est inscrit
return self.compute_etud_ue_coef(etudid, ue)
# --- TABLEAU RECAP
class TableRecap(tb.Table): # was get_table_recap
"""Table récap. des résultats.
allow_html: si vrai, peut mettre du HTML dans les valeurs
Result: Table, le contenu étant une ligne par étudiant.
Si convert_values, transforme les notes en chaines ("12.34").
Les colonnes générées sont:
etudid
rang : rang indicatif (basé sur moy gen)
moy_gen : moy gen indicative
moy_ue_<ue_id>, ..., les moyennes d'UE
moy_res_<modimpl_id>_<ue_id>, ... les moyennes de ressources dans l'UE
moy_sae_<modimpl_id>_<ue_id>, ... les moyennes de SAE dans l'UE
On ajoute aussi des classes:
- pour les lignes:
selected_row pour l'étudiant sélectionné
- les colonnes:
- la moyenne générale a la classe col_moy_gen
- les colonnes SAE ont la classe col_sae
- les colonnes Resources ont la classe col_res
- les colonnes d'UE ont la classe col_ue
- les colonnes de modules (SAE ou res.) d'une UE ont la classe mod_ue_<ue_id>
"""
def __init__(
self,
res: ResultatsSemestre,
convert_values=False,
include_evaluations=False,
mode_jury=False,
):
self.res = res
self.include_evaluations = include_evaluations
self.mode_jury = mode_jury
parcours = self.formsemestre.formation.get_parcours()
self.barre_moy = parcours.BARRE_MOY - scu.NOTES_TOLERANCE
self.barre_valid_ue = parcours.NOTES_BARRE_VALID_UE
self.barre_warning_ue = parcours.BARRE_UE_DISPLAY_WARNING
self.cache_nomcomplet = {} # cache uid : nomcomplet
if convert_values:
self.fmt_note = scu.fmt_note
else:
self.fmt_note = lambda x: x
# couples (modimpl, ue) effectivement présents dans la table:
self.modimpl_ue_ids = set()
etuds_inscriptions = self.formsemestre.etuds_inscriptions
ues = self.formsemestre.query_ues(with_sport=True) # avec bonus
ues_sans_bonus = [ue for ue in ues if ue.type != UE_SPORT]
for etudid in etuds_inscriptions:
etud = Identite.query.get(etudid)
row = RowRecap(self, etudid)
self.add_row(row)
self.recap_add_etud(row, etud)
self._recap_add_moyennes(row, etud, ues_sans_bonus)
self.recap_add_partitions()
self.recap_add_cursus()
self._recap_add_admissions()
# tri par rang croissant
if not self.formsemestre.block_moyenne_generale:
self.sort_rows(key=lambda row: row.rang_order)
else:
self.sort_rows(key=lambda row: row.nb_ues_validables, reverse=True)
# Lignes footer (min, max, ects, apo, ...)
self.add_bottom_rows(ues_sans_bonus)
# Evaluations:
if include_evaluations:
self.add_evaluations()
self.mark_empty_cols()
self.add_type_row()
def mark_empty_cols(self):
"""Ajoute style "col_empty" aux colonnes de modules vides"""
# identifie les col. vides par la classe sur leur moyenne
row_moy = self.get_row_by_id("moy")
for col_id in self.column_ids:
cell: tb.Cell = row_moy.cells.get(col_id)
if cell and "col_empty" in cell.classes:
self.column_classes[col_id].append("col_empty")
def add_type_row(self):
"""Ligne avec la classe de chaque colonne recap."""
# récupère le type à partir des classes css (hack...)
row_type = tb.BottomRow(
self,
"type_col",
left_title="Type col.",
left_title_col_ids=["prenom", "nom_short"],
category="bottom_infos",
classes=["bottom_info"],
)
for col_id in self.column_ids:
group_name = self.column_group.get(col_id, "")
if group_name.startswith("col_"):
group_name = group_name[4:]
row_type.add_cell(col_id, None, group_name)
def add_bottom_rows(self, ues):
"""Les informations à mettre en bas de la table recap:
min, max, moy, ECTS, Apo."""
res = self.res
# Ordre des lignes: Min, Max, Moy, Coef, ECTS, Apo
row_min = tb.BottomRow(
self,
"min",
left_title="Min.",
left_title_col_ids=["prenom", "nom_short"],
category="bottom_infos",
classes=["bottom_info"],
)
row_max = tb.BottomRow(
self,
"max",
left_title="Max.",
left_title_col_ids=["prenom", "nom_short"],
category="bottom_infos",
classes=["bottom_info"],
)
row_moy = tb.BottomRow(
self,
"moy",
left_title="Moy.",
left_title_col_ids=["prenom", "nom_short"],
category="bottom_infos",
classes=["bottom_info"],
)
row_coef = tb.BottomRow(
self,
"coef",
left_title="Coef.",
left_title_col_ids=["prenom", "nom_short"],
category="bottom_infos",
classes=["bottom_info"],
)
row_ects = tb.BottomRow(
self,
"ects",
left_title="ECTS",
left_title_col_ids=["prenom", "nom_short"],
category="bottom_infos",
classes=["bottom_info"],
)
row_apo = tb.BottomRow(
self,
"apo",
left_title="Code Apogée",
left_title_col_ids=["prenom", "nom_short"],
category="bottom_infos",
classes=["bottom_info"],
)
# --- ECTS
# titre (à gauche) sur 2 colonnes pour s'adapter à l'affichage des noms/prenoms
for ue in ues:
col_id = f"moy_ue_{ue.id}"
row_ects.add_cell(col_id, None, ue.ects)
# ajoute cell UE vides sur ligne coef pour borders verticales
# XXX TODO classes dans table sur colonne ajoutées à tous les TD
row_coef.add_cell(col_id, None, "")
row_ects.add_cell(
"moy_gen",
None,
sum([ue.ects or 0 for ue in ues if ue.type != UE_SPORT]),
)
# --- MIN, MAX, MOY, APO
row_min.add_cell("moy_gen", None, self.fmt_note(res.etud_moy_gen.min()))
row_max.add_cell("moy_gen", None, self.fmt_note(res.etud_moy_gen.max()))
row_moy.add_cell("moy_gen", None, self.fmt_note(res.etud_moy_gen.mean()))
for ue in ues:
col_id = f"moy_ue_{ue.id}"
row_min.add_cell(col_id, None, self.fmt_note(res.etud_moy_ue[ue.id].min()))
row_max.add_cell(col_id, None, self.fmt_note(res.etud_moy_ue[ue.id].max()))
row_moy.add_cell(col_id, None, self.fmt_note(res.etud_moy_ue[ue.id].mean()))
row_apo.add_cell(col_id, None, ue.code_apogee or "")
for modimpl in res.formsemestre.modimpls_sorted:
if (modimpl.id, ue.id) in self.modimpl_ue_ids:
col_id = f"moy_{modimpl.module.type_abbrv()}_{modimpl.id}_{ue.id}"
if res.is_apc:
coef = res.modimpl_coefs_df[modimpl.id][ue.id]
else:
coef = modimpl.module.coefficient or 0
row_coef.add_cell(
col_id,
None,
self.fmt_note(coef),
group=f"col_ue_{ue.id}_modules",
)
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
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)))
moy = np.nanmean(notes)
row_moy.add_cell(
col_id,
None,
self.fmt_note(moy),
# aucune note dans ce module ?
classes=["col_empty" if np.isnan(moy) else ""],
)
row_apo.add_cell(col_id, None, modimpl.module.code_apogee or "")
class RowRecap(tb.Row):
"Ligne de la table recap"
def add_etud(self, etud: Identite):
"""Ajoute colonnes étudiant: codes, noms"""
res = self.table.res
# --- Codes (seront cachés, mais exportés en excel)
self.add_cell("etudid", "etudid", etud.id, "etud_codes")
self.add_cell(
"code_nip",
"code_nip",
etud.code_nip or "",
"etud_codes",
)
# --- Rang
if not res.formsemestre.block_moyenne_generale:
self.rang_order = res.etud_moy_gen_ranks_int[etud.id]
res.add_cell(
"rang",
"Rg",
self.etud_moy_gen_ranks[etud.id],
"rang",
data={"order": f"{self.rang_order:05d}"},
)
else:
self.rang_order = -1
# --- Identité étudiant
url_bulletin = url_for(
"notes.formsemestre_bulletinetud",
scodoc_dept=g.scodoc_dept,
formsemestre_id=res.formsemestre.id,
etudid=etud.id,
)
self.add_cell("civilite_str", "Civ.", etud.civilite_str, "identite_detail")
self.add_cell(
"nom_disp",
"Nom",
etud.nom_disp(),
"identite_detail",
data={"order": etud.sort_key},
target=url_bulletin,
target_attrs={"class": "etudinfo", "id": str(etud.id)},
)
self.add_cell("prenom", "Prénom", etud.prenom, "identite_detail")
self.add_cell(
"nom_short",
"Nom",
etud.nom_short,
"identite_court",
data={
"order": etud.sort_key,
"etudid": etud.id,
"nomprenom": etud.nomprenom,
},
target=url_bulletin,
target_attrs={"class": "etudinfo", "id": str(etud.id)},
)
def add_moyennes( # XXX was _recap_add_moyennes
self,
row: tb.Row,
etud: Identite,
ues_sans_bonus: list[UniteEns],
):
"""Ajoute cols moy_gen moy_ue et tous les modules..."""
table = self.table
res = table.res
# --- Moyenne générale
if not res.formsemestre.block_moyenne_generale:
moy_gen = res.etud_moy_gen.get(etud.id, False)
note_class = ""
if moy_gen is False:
moy_gen = scu.NO_NOTE_STR
elif isinstance(moy_gen, float) and moy_gen < table.barre_moy:
note_class = "moy_ue_warning" # en rouge
row.add_cell(
"moy_gen",
"Moy",
table.fmt_note(moy_gen),
"col_moy_gen",
classes=[note_class],
)
# Ajoute bulle sur titre du pied de table:
if res.is_apc:
table.foot_title_row.cells["moy_gen"].target_attrs[
"title"
] = "moyenne indicative"
# --- Moyenne d'UE
self.nb_ues_validables, self.nb_ues_warning = 0, 0
for ue in ues_sans_bonus:
ue_status = res.get_etud_ue_status(etud.id, ue.id)
if ue_status is not None:
self.add_ue(ue, ue_status)
if table.mode_jury:
# pas d'autre colonnes de résultats
continue
# Bonus (sport) dans cette UE ?
# Le bonus sport appliqué sur cette UE
if (res.bonus_ues is not None) and (ue.id in res.bonus_ues):
val = res.bonus_ues[ue.id][etud.id] or ""
val_fmt = val_fmt_html = table.fmt_note(val)
if val:
val_fmt_html = f"""<span class="green-arrow-up"></span><span class="sp2l">{
val_fmt
}</span>"""
row.add_cell(
f"bonus_ue_{ue.id}",
f"Bonus {ue.acronyme}",
val_fmt_html,
raw_content=val_fmt,
group=f"col_ue_{ue.id}",
classes=["col_ue_bonus"],
)
# Les moyennes des modules (ou ressources et SAÉs) dans cette UE
self.add_ue_modimpls(
ue, etud, ue_status["is_capitalized"]
) # XXX _recap_add_ue_modimpls
self.nb_ues_etud_parcours = len(res.etud_ues_ids(etud.id))
ue_valid_txt = (
ue_valid_txt_html
) = f"{self.nb_ues_validables}/{self.nb_ues_etud_parcours}"
if self.nb_ues_warning:
ue_valid_txt_html += " " + scu.EMO_WARNING
# place juste avant moy. gen.
table.insert_group("col_ues_validables", before="col_moy_gen")
classes = ["col_ue"]
if self.nb_ues_warning:
classes.append("moy_ue_warning")
elif self.nb_ues_validables < len(ues_sans_bonus):
classes.append("moy_inf")
row.add_cell(
"ues_validables",
"UEs",
ue_valid_txt_html,
"col_ues_validables",
classes=classes,
raw_content=ue_valid_txt,
data={"order": self.nb_ues_validables}, # tri
)
if table.mode_jury and res.validations:
if res.is_apc:
# formations BUT: pas de code semestre, concatene ceux des UEs
dec_ues = res.validations.decisions_jury_ues.get(etud.id)
if dec_ues:
jury_code_sem = ",".join(
[dec_ues[ue_id].get("code", "") for ue_id in dec_ues]
)
else:
jury_code_sem = ""
else:
# formations classiques: code semestre
dec_sem = res.validations.decisions_jury.get(etud.id)
jury_code_sem = dec_sem["code"] if dec_sem else ""
self.add_cell("jury_code_sem", "Jury", jury_code_sem, "jury_code_sem")
self.add_cell(
"jury_link",
"",
f"""<a href="{url_for('notes.formsemestre_validation_etud_form',
scodoc_dept=g.scodoc_dept, formsemestre_id=res.formsemestre.id, etudid=etud.id
)
}">{("saisir" if not jury_code_sem else "modifier")
if res.formsemestre.etat else "voir"} décisions</a>""",
"col_jury_link",
)
def add_ue(self, ue: UniteEns, ue_status: dict):
"Ajoute résultat UE au row (colonne col_ue)"
table = self.table
col_id = f"moy_ue_{ue.id}"
val = ue_status["moy"]
note_class = ""
if isinstance(val, float):
if val < table.barre_moy:
note_class = "moy_inf"
elif val >= table.barre_valid_ue:
note_class = "moy_ue_valid"
self.nb_ues_validables += 1
if val < table.barre_warning_ue:
note_class = "moy_ue_warning" # notes très basses
self.nb_ues_warning += 1
self.add_cell(
col_id,
ue.acronyme,
table.fmt_note(val),
group=f"col_ue_{ue.id}",
classes=["col_ue", note_class],
)
table.foot_title_row.cells[col_id].target_attrs[
"title"
] = f"""{ue.titre} S{ue.semestre_idx or '?'}"""
def add_ue_modimpls(
self, row: tb.Row, ue: UniteEns, etud: Identite, is_capitalized: bool
):
"""Ajoute à row les moyennes des modules (ou ressources et SAÉs) dans l'UE"""
# pylint: disable=invalid-unary-operand-type
table = row.table
res = table.res
for modimpl in res.modimpls_in_ue(ue, etud.id, with_bonus=False):
if is_capitalized:
val = "-c-"
else:
modimpl_results = res.modimpls_results.get(modimpl.id)
if modimpl_results: # pas bonus
if res.is_apc: # BUT
moys_vers_ue = modimpl_results.etuds_moy_module.get(ue.id)
val = (
moys_vers_ue.get(etud.id, "?")
if moys_vers_ue is not None
else ""
)
else: # classique: Series indépendante de l'UE
val = modimpl_results.etuds_moy_module.get(etud.id, "?")
else:
val = ""
col_id = f"moy_{modimpl.module.type_abbrv()}_{modimpl.id}_{ue.id}"
val_fmt = val_fmt_html = table.fmt_note(val)
if modimpl.module.module_type == scu.ModuleType.MALUS:
if val and not isinstance(val, str) and not np.isnan(val):
if val >= 0:
val_fmt_html = f"""<span class="red-arself-down"></span><span class="sp2l">+{
val_fmt
}</span>"""
else:
# val_fmt_html = (scu.EMO_RED_TRIANGLE_DOWN + val_fmt)
val_fmt_html = f"""<span class="green-arrow-up"></span><span class="sp2l malus_negatif">-{
table.fmt_note(-val)}</span>"""
else:
val_fmt = val_fmt_html = "" # inscrit à ce malus, mais sans note
cell = self.add_cell(
col_id,
modimpl.module.code,
val_fmt_html,
raw_content=val_fmt,
group=f"col_ue_{ue.id}_modules",
classes=[
f"col_{modimpl.module.type_abbrv()}",
f"mod_ue_{ue.id}",
],
)
if modimpl.module.module_type == scu.ModuleType.MALUS:
# positionne la colonne à droite de l'UE
cell.group = f"col_ue_{ue.id}_malus"
table.insert_group(cell.group, after=f"col_ue_{ue.id}")
table.foot_title_row.cells[col_id].target = url_for(
"notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept,
moduleimpl_id=modimpl.id,
)
nom_resp = table.cache_nomcomplet.get(modimpl.responsable_id)
if nom_resp is None:
user = User.query.get(modimpl.responsable_id)
nom_resp = user.get_nomcomplet() if user else ""
table.cache_nomcomplet[modimpl.responsable_id] = nom_resp
table.foot_title_row.cells[col_id].target_attrs[
"title"
] = f"{modimpl.module.titre} ({nom_resp})"
table.modimpl_ue_ids.add((modimpl.id, ue.id))
def _recap_etud_groups_infos(
self, etudid: int, row: dict, titles: dict
): # XXX non utilisé
"""Table recap: ajoute à row les colonnes sur les groupes pour cet etud"""
# dec = self.get_etud_decision_sem(etudid)
# if dec:
# codes_nb[dec["code"]] += 1
row_class = ""
etud_etat = self.get_etud_etat(etudid)
if etud_etat == DEM:
gr_name = "Dém."
row_class = "dem"
elif etud_etat == DEF:
gr_name = "Déf."
row_class = "def"
else:
# XXX probablement à revoir pour utiliser données cachées,
# via get_etud_groups_in_partition ou autre
group = sco_groups.get_etud_main_group(etudid, self.formsemestre.id)
gr_name = group["group_name"] or ""
row["group"] = gr_name
row["_group_class"] = "group"
if row_class:
row["_tr_class"] = " ".join([row.get("_tr_class", ""), row_class])
titles["group"] = "Gr"
def _recap_add_admissions(self, table: tb.Table):
"""Ajoute les colonnes "admission"
Les colonnes ont la classe css "admission"
"""
fields = {
"bac": "Bac",
"specialite": "Spécialité",
"type_admission": "Type Adm.",
"classement": "Rg. Adm.",
}
first = True
for cid, title in fields.items():
cell_head, cell_foot = table.add_title(cid, title)
cell_head.classes.append("admission")
cell_foot.classes.append("admission")
if first:
cell_head.classes.append("admission_first")
cell_foot.classes.append("admission_first")
first = False
for row in table.rows:
etud = Identite.query.get(row.id) # TODO XXX
admission = etud.admission.first()
first = True
for cid, title in fields.items():
cell = row.add_cell(
cid,
title,
getattr(admission, cid) or "",
"admission",
)
if first:
cell.classes.append("admission_first")
first = False
def recap_add_cursus(self, table: tb.Table):
"""Ajoute colonne avec code cursus, eg 'S1 S2 S1'"""
table.insert_group("cursus", before="col_ues_validables")
cid = "code_cursus"
formation_code = self.formsemestre.formation.formation_code
for row in table.rows:
etud = Identite.query.get(row.id) # TODO XXX à optimiser: etud dans row
row.add_cell(
cid,
"Cursus",
" ".join(
[
f"S{ins.formsemestre.semestre_id}"
for ins in reversed(etud.inscriptions())
if ins.formsemestre.formation.formation_code == formation_code
]
),
"cursus",
)
def recap_add_partitions(self, table: tb.Table):
"""Ajoute les colonnes indiquant les groupes
La table contient des rows avec la clé etudid.
Les colonnes ont la classe css "partition"
"""
table.insert_group("partition", after="identite_court")
partitions, partitions_etud_groups = sco_groups.get_formsemestre_groups(
self.formsemestre.id
)
first_partition = True
for partition in partitions:
col_classes = [] # la classe "partition" sera ajoutée par la table
if not first_partition:
col_classes.append("partition_aux")
first_partition = False
cid = f"part_{partition['partition_id']}"
cell_head, cell_foot = table.add_title(cid, partition["partition_name"])
cell_head.classes += col_classes
cell_foot.classes += col_classes
if partition["bul_show_rank"]:
rg_cid = cid + "_rg" # rang dans la partition
cell_head, cell_foot = table.add_title(
cid, f"Rg {partition['partition_name']}"
)
cell_head.classes.append("partition_rangs")
cell_foot.classes.append("partition_rangs")
partition_etud_groups = partitions_etud_groups[partition["partition_id"]]
for row in table.rows:
etudid = row.id
group = None # group (dict) de l'étudiant dans cette partition
# dans NotesTableCompat, à revoir
etud_etat = self.get_etud_etat(row.id) # row.id == etudid
tr_classes = []
if etud_etat == scu.DEMISSION:
gr_name = "Dém."
tr_classes.append("dem")
elif etud_etat == DEF:
gr_name = "Déf."
tr_classes.append("def")
else:
group = partition_etud_groups.get(etudid)
gr_name = group["group_name"] if group else ""
if gr_name:
row.add_cell(
cid,
partition["partition_name"],
gr_name,
"partition",
classes=col_classes,
)
# Rangs dans groupe
if (
partition["bul_show_rank"]
and (group is not None)
and (group["id"] in self.moy_gen_rangs_by_group)
):
rang = self.moy_gen_rangs_by_group[group["id"]][0]
row.add_cell(rg_cid, None, rang.get(etudid, ""), "partition")
def _recap_add_evaluations(self, table: tb.Table):
"""Ajoute les colonnes avec les notes aux évaluations
rows est une liste de dict avec une clé "etudid"
Les colonnes ont la classe css "evaluation"
"""
# nouvelle ligne pour description évaluations:
row_descr_eval = tb.BottomRow(
table,
"evaluations",
left_title="Description évaluations",
left_title_col_ids=["prenom", "nom_short"],
category="bottom_infos",
classes=["bottom_info"],
)
first_eval = True
for modimpl in self.formsemestre.modimpls_sorted:
evals = self.modimpls_results[modimpl.id].get_evaluations_completes(modimpl)
eval_index = len(evals) - 1
inscrits = {i.etudid for i in modimpl.inscriptions}
first_eval_of_mod = True
for e in evals:
col_id = f"eval_{e.id}"
title = f'{modimpl.module.code} {eval_index} {e.jour.isoformat() if e.jour else ""}'
col_classes = ["evaluation"]
if first_eval:
col_classes.append("first")
elif first_eval_of_mod:
col_classes.append("first_of_mod")
first_eval_of_mod = first_eval = False
eval_index -= 1
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(
e.evaluation_id
)
for row in table.rows:
etudid = row.id
if etudid in inscrits:
if etudid in notes_db:
val = notes_db[etudid]["value"]
else:
# Note manquante mais prise en compte immédiate: affiche ATT
val = scu.NOTES_ATTENTE
content = table.fmt_note(val)
classes = col_classes + [
{
"ABS": "abs",
"ATT": "att",
"EXC": "exc",
}.get(content, "")
]
row.add_cell(col_id, title, content, "", classes=classes)
else:
row.add_cell(
col_id,
title,
"ni",
"",
classes=col_classes + ["non_inscrit"],
)
table.get_row_by_id("coef").row[col_id] = e.coefficient
table.get_row_by_id("min").row[col_id] = "0"
table.get_row_by_id("max").row[col_id] = table.fmt_note(e.note_max)
row_descr_eval.add_cell(
col_id,
None,
e.description or "",
target=url_for(
"notes.evaluation_listenotes",
scodoc_dept=g.scodoc_dept,
evaluation_id=e.id,
),
)

View File

@ -260,12 +260,23 @@ class UniteEns(db.Model):
class DispenseUE(db.Model):
"""Dispense d'UE
Utilisé en PCC (BUT) pour indiquer les étudiants redoublants avec une UE capitalisée
Utilisé en APC (BUT) pour indiquer les étudiants redoublants avec une UE capitalisée
qu'ils ne refont pas.
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
- elle ne permet pas la délivrance d'ECTS ou du diplôme.
On utilise cette dispense et non une "inscription" par souci d'efficacité:
en général, la grande majorité des étudiants suivront toutes les UEs de leur parcours,
la dispense étant une exception.
"""
__table_args__ = (db.UniqueConstraint("ue_id", "etudid"),)
__table_args__ = (db.UniqueConstraint("formsemestre_id", "ue_id", "etudid"),)
id = db.Column(db.Integer, primary_key=True)
formsemestre_id = formsemestre_id = db.Column(
db.Integer, db.ForeignKey("notes_formsemestre.id"), index=True, nullable=True
)
ue_id = db.Column(
db.Integer,
db.ForeignKey(UniteEns.id, ondelete="CASCADE"),
@ -284,3 +295,25 @@ class DispenseUE(db.Model):
def __repr__(self) -> str:
return f"""<{self.__class__.__name__} {self.id} etud={
repr(self.etud)} ue={repr(self.ue)}>"""
@classmethod
def load_formsemestre_dispense_ues_set(
cls, formsemestre: "FormSemestre", etudids: pd.Index, ues: list[UniteEns]
) -> set[tuple[int, int]]:
"""Construit l'ensemble des
etudids = modimpl_inscr_df.index, # les etudids
ue_ids : modimpl_coefs_df.index, # les UE du formsemestre sans les UE bonus sport
Résultat: set de (etudid, ue_id).
"""
# Prend toutes les dispenses obtenues par des étudiants de ce formsemestre,
# puis filtre sur inscrits et ues
ue_ids = {ue.id for ue in ues}
dispense_ues = {
(dispense_ue.etudid, dispense_ue.ue_id)
for dispense_ue in DispenseUE.query.filter_by(
formsemestre_id=formsemestre.id
)
if dispense_ue.etudid in etudids and dispense_ue.ue_id in ue_ids
}
return dispense_ues

View File

@ -65,10 +65,8 @@ def comp_nom_semestre_dans_parcours(sem):
"""Le nom a afficher pour titrer un semestre
par exemple: "semestre 2 FI 2015"
"""
from app.scodoc import sco_formations
F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0]
parcours = codes_cursus.get_cursus_from_code(F["type_parcours"])
formation: Formation = Formation.query.get_or_404(sem["formation_id"])
parcours = codes_cursus.get_cursus_from_code(formation.type_parcours)
return "%s %s %s %s" % (
parcours.SESSION_NAME, # eg "semestre"
sem["semestre_id"], # eg 2

View File

@ -824,7 +824,8 @@ FORMATION_CURSUS_DESCRS = [p[1].__doc__ for p in _tp] # intitulés (eg pour men
FORMATION_CURSUS_TYPES = [p[0] for p in _tp] # codes numeriques (TYPE_CURSUS)
def get_cursus_from_code(code_cursus):
def get_cursus_from_code(code_cursus: int) -> TypeCursus:
"renvoie le cursus de code indiqué"
cursus = SCO_CURSUS.get(code_cursus)
if cursus is None:
log(f"Warning: invalid code_cursus: {code_cursus}")

File diff suppressed because it is too large Load Diff

View File

@ -76,9 +76,9 @@ from app.scodoc import html_sco_header
from app.scodoc import sco_bulletins_pdf
from app.scodoc import sco_groups
from app.scodoc import sco_groups_view
from app.scodoc import sco_pvjury
from app.scodoc import sco_dict_pv_jury
from app.scodoc import sco_pvpdf
from app.scodoc import sco_pv_forms
from app.scodoc import sco_pv_lettres_inviduelles
from app.scodoc import sco_pv_pdf
from app.scodoc.sco_exceptions import ScoValueError
@ -398,27 +398,23 @@ def do_formsemestre_archive(
signature=signature,
)
if data:
PVArchive.store(archive_id, "CourriersDecisions%s.pdf" % groups_filename, data)
PVArchive.store(archive_id, f"CourriersDecisions{groups_filename}.pdf", data)
# PV de jury (PDF): disponible seulement en classique
# en BUT, le PV est sous forme excel (Decisions_Jury.xlsx ci-dessus)
if not formsemestre.formation.is_apc():
dpv = sco_dict_pv_jury.dict_pvjury(
formsemestre_id, etudids=etudids, with_prev=True
)
data = sco_pvpdf.pvjury_pdf(
dpv,
date_commission=date_commission,
date_jury=date_jury,
numero_arrete=numero_arrete,
code_vdi=code_vdi,
show_title=show_title,
pv_title=pv_title,
with_paragraph_nom=with_paragraph_nom,
anonymous=anonymous,
)
if data:
PVArchive.store(archive_id, "PV_Jury%s.pdf" % groups_filename, data)
# PV de jury (PDF):
data = sco_pv_pdf.pvjury_pdf(
formsemestre,
etudids=etudids,
date_commission=date_commission,
date_jury=date_jury,
numero_arrete=numero_arrete,
code_vdi=code_vdi,
show_title=show_title,
pv_title=pv_title,
with_paragraph_nom=with_paragraph_nom,
anonymous=anonymous,
)
if data:
PVArchive.store(archive_id, f"PV_Jury{groups_filename}.pdf", data)
def formsemestre_archive(formsemestre_id, group_ids: list[int] = None):
@ -474,7 +470,7 @@ enregistrés et non modifiables, on peut les retrouver ultérieurement.
),
("sep", {"input_type": "separator", "title": "Informations sur PV de jury"}),
]
descr += sco_pvjury.descrform_pvjury(formsemestre)
descr += sco_pv_forms.descrform_pvjury(formsemestre)
descr += [
(
"signature",

View File

@ -67,7 +67,7 @@ from app.scodoc import sco_evaluation_db
from app.scodoc import sco_formsemestre
from app.scodoc import sco_groups
from app.scodoc import sco_preferences
from app.scodoc import sco_dict_pv_jury
from app.scodoc import sco_pv_dict
from app.scodoc import sco_users
import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import ModuleType, fmt_note

View File

@ -49,7 +49,6 @@ from app.models import (
)
from app.models import ApcValidationRCUE, ScolarFormSemestreValidation, ScolarEvent
from app.models import ScolarNews
from app.models.formations import Matiere
import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import ModuleType
@ -66,7 +65,6 @@ from app.scodoc import codes_cursus
from app.scodoc import sco_edit_apc
from app.scodoc import sco_edit_matiere
from app.scodoc import sco_edit_module
from app.scodoc import sco_formsemestre
from app.scodoc import sco_groups
from app.scodoc import sco_moduleimpl
from app.scodoc import sco_tag_module
@ -844,7 +842,8 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
H.append(
f"""</li>
<li> <a class="stdlink" href="{
url_for('notes.edit_modules_ue_coefs', scodoc_dept=g.scodoc_dept, formation_id=formation_id, semestre_idx=semestre_idx)
url_for('notes.edit_modules_ue_coefs',
scodoc_dept=g.scodoc_dept, formation_id=formation_id, semestre_idx=semestre_idx)
}">{'Visualiser' if locked else 'Éditer'} les coefficients des ressources et SAÉs</a>
</li>
</ul>

View File

@ -36,7 +36,7 @@ from flask_login import current_user
from app import db, log
from app.models import ModuleImpl, ScolarNews
from app.models import Evaluation, ModuleImpl, ScolarNews
from app.models.evaluations import evaluation_enrich_dict, check_evaluation_args
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb

View File

@ -40,7 +40,7 @@ from app.scodoc import html_sco_header
from app.scodoc import sco_bac
from app.scodoc import codes_cursus
from app.scodoc import sco_preferences
from app.scodoc import sco_dict_pv_jury
from app.scodoc import sco_pv_dict
from app.scodoc import sco_etud
import sco_version
from app.scodoc.gen_tables import GenTable
@ -57,16 +57,14 @@ def _build_results_table(start_date=None, end_date=None, types_parcours=[]):
# Décisions de jury de tous les semestres:
dpv_by_sem = {}
for formsemestre_id in formsemestre_ids:
dpv_by_sem[formsemestre_id] = sco_dict_pv_jury.dict_pvjury(
dpv_by_sem[formsemestre_id] = sco_pv_dict.dict_pvjury(
formsemestre_id, with_parcours_decisions=True
)
semlist = [dpv["formsemestre"] for dpv in dpv_by_sem.values() if dpv]
semlist_parcours = []
for sem in semlist:
sem["formation"] = sco_formations.formation_list(
args={"formation_id": sem["formation_id"]}
)[0]
sem["formation"] = Formation.query.get_or_404(sem["formation_id"]).to_dict()
sem["parcours"] = codes_cursus.get_cursus_from_code(
sem["formation"]["type_parcours"]
)
@ -350,7 +348,7 @@ end_date='2017-08-31'
formsemestre_ids = get_set_formsemestre_id_dates( start_date, end_date)
dpv_by_sem = {}
for formsemestre_id in formsemestre_ids:
dpv_by_sem[formsemestre_id] = sco_dict_pv_jury.dict_pvjury( formsemestre_id, with_parcours_decisions=True)
dpv_by_sem[formsemestre_id] = sco_pv_dict.dict_pvjury( formsemestre_id, with_parcours_decisions=True)
semlist = [ dpv['formsemestre'] for dpv in dpv_by_sem.values() ]

View File

@ -482,20 +482,24 @@ def formation_list_table() -> GenTable:
editable = current_user.has_permission(Permission.ScoChangeFormation)
# Traduit/ajoute des champs à afficher:
for f in formations:
try:
f["parcours_name"] = codes_cursus.get_cursus_from_code(
f["type_parcours"]
).NAME
except:
f["parcours_name"] = ""
f["_titre_target"] = url_for(
"notes.ue_table",
scodoc_dept=g.scodoc_dept,
formation_id=str(f["formation_id"]),
)
f["_titre_link_class"] = "stdlink"
f["_titre_id"] = "titre-%s" % f["acronyme"].lower().replace(" ", "-")
rows = []
for formation in formations:
acronyme_no_spaces = formation.acronyme.lower().replace(" ", "-")
row = {
"acronyme": formation.acronyme,
"parcours_name": codes_cursus.get_cursus_from_code(
formation.type_parcours
).NAME,
"titre": formation.titre,
"_titre_target": url_for(
"notes.ue_table",
scodoc_dept=g.scodoc_dept,
formation_id=formation.id,
),
"_titre_link_class": "stdlink",
"_titre_id": f"""titre-{acronyme_no_spaces}""",
"version": formation.version or 0,
}
# Ajoute les semestres associés à chaque formation:
row["formsemestres"] = formation.formsemestres.order_by(
FormSemestre.date_debut

View File

@ -38,7 +38,7 @@ import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu
from app import log
from app.models import Departement
from app.models import FormSemestre
from app.models import Formation, FormSemestre
from app.scodoc import sco_cache, codes_cursus, sco_formations, sco_preferences
from app.scodoc.gen_tables import GenTable
from app.scodoc.codes_cursus import NO_SEMESTRE_ID
@ -145,13 +145,8 @@ def _formsemestre_enrich(sem):
# imports ici pour eviter refs circulaires
from app.scodoc import sco_formsemestre_edit
formations = sco_formations.formation_list(
args={"formation_id": sem["formation_id"]}
)
if not formations:
raise ScoValueError("pas de formation pour ce semestre !")
F = formations[0]
parcours = codes_cursus.get_cursus_from_code(F["type_parcours"])
formation: Formation = Formation.query.get_or_404(sem["formation_id"])
parcours = codes_cursus.get_cursus_from_code(formation.type_parcours)
# 'S1', 'S2', ... ou '' pour les monosemestres
if sem["semestre_id"] != NO_SEMESTRE_ID:
sem["sem_id_txt"] = "S%s" % sem["semestre_id"]

View File

@ -55,7 +55,6 @@ from app.scodoc import sco_formations
from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_formsemestre_validation
from app.scodoc import sco_etud
from app.scodoc.codes_cursus import UE_SPORT

View File

@ -431,6 +431,12 @@ def formsemestre_status_menubar(formsemestre: FormSemestre) -> str:
},
"enabled": formsemestre.can_edit_jury(),
},
{
"title": "Générer feuille préparation Jury (non BUT)",
"endpoint": "notes.feuille_preparation_jury",
"args": {"formsemestre_id": formsemestre_id},
"enabled": not formsemestre.formation.is_apc(),
},
{
"title": "Éditer les PV et archiver les résultats",
"endpoint": "notes.formsemestre_archive",
@ -589,10 +595,7 @@ def formsemestre_description_table(
).first_or_404()
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
use_ue_coefs = sco_preferences.get_preference("use_ue_coefs", formsemestre_id)
F = sco_formations.formation_list(args={"formation_id": formsemestre.formation_id})[
0
]
parcours = codes_cursus.get_cursus_from_code(F["type_parcours"])
parcours = codes_cursus.get_cursus_from_code(formsemestre.formation.type_parcours)
# --- Colonnes à proposer:
columns_ids = ["UE", "Code", "Module"]
if with_parcours:

View File

@ -64,7 +64,7 @@ from app.scodoc import sco_cursus_dut
from app.scodoc.sco_cursus_dut import etud_est_inscrit_ue
from app.scodoc import sco_photos
from app.scodoc import sco_preferences
from app.scodoc import sco_dict_pv_jury
from app.scodoc import sco_pv_dict
# ------------------------------------------------------------------------------------
def formsemestre_validation_etud_form(
@ -561,7 +561,7 @@ def formsemestre_recap_parcours_table(
is_cur = Se.formsemestre_id == sem["formsemestre_id"]
num_sem += 1
dpv = sco_dict_pv_jury.dict_pvjury(sem["formsemestre_id"], etudids=[etudid])
dpv = sco_pv_dict.dict_pvjury(sem["formsemestre_id"], etudids=[etudid])
pv = dpv["decisions"][0]
decision_sem = pv["decision_sem"]
decisions_ue = pv["decisions_ue"]

View File

@ -795,8 +795,7 @@ def groups_table(
)
m["parcours"] = Se.get_cursus_descr()
m["code_cursus"], _ = sco_report.get_code_cursus_etud(etud)
L = [[m.get(k, "") for k in keys] for m in groups_infos.members]
rows = [[m.get(k, "") for k in keys] for m in groups_infos.members]
title = "etudiants_%s" % groups_infos.groups_filename
xls = sco_excel.excel_simple_table(titles=titles, lines=rows, sheet_name=title)
filename = title

View File

@ -47,7 +47,7 @@ from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_groups
from app.scodoc import sco_preferences
from app.scodoc import sco_dict_pv_jury
from app.scodoc import sco_pv_dict
from app.scodoc.sco_exceptions import ScoValueError
@ -137,7 +137,7 @@ def list_inscrits(formsemestre_id, with_dems=False):
def list_etuds_from_sem(src, dst) -> list[dict]:
"""Liste des etudiants du semestre src qui sont autorisés à passer dans le semestre dst."""
target = dst["semestre_id"]
dpv = sco_dict_pv_jury.dict_pvjury(src["formsemestre_id"])
dpv = sco_pv_dict.dict_pvjury(src["formsemestre_id"])
if not dpv:
return []
etuds = [
@ -261,8 +261,8 @@ def list_source_sems(sem, delai=None) -> list[dict]:
if s["semestre_id"] == codes_cursus.NO_SEMESTRE_ID:
continue
#
F = sco_formations.formation_list(args={"formation_id": s["formation_id"]})[0]
parcours = codes_cursus.get_cursus_from_code(F["type_parcours"])
formation: Formation = Formation.query.get_or_404(s["formation_id"])
parcours = codes_cursus.get_cursus_from_code(formation.type_parcours)
if not parcours.ALLOW_SEM_SKIP:
if s["semestre_id"] < (sem["semestre_id"] - 1):
continue

View File

@ -402,7 +402,9 @@ def moduleimpl_inscriptions_stats(formsemestre_id):
# Etudiants "dispensés" d'une UE (capitalisée)
ues_cap_info = get_etuds_with_capitalized_ue(formsemestre_id)
if ues_cap_info:
H.append('<h3>Étudiants avec UEs capitalisées:</h3><ul class="ue_inscr_list">')
H.append(
'<h3>Étudiants avec UEs capitalisées (ADM):</h3><ul class="ue_inscr_list">'
)
ues = [
sco_edit_ue.ue_list({"ue_id": ue_id})[0] for ue_id in ues_cap_info.keys()
]
@ -470,8 +472,9 @@ def moduleimpl_inscriptions_stats(formsemestre_id):
if can_change:
H.append(
f"""<div><a class="stdlink" href="{
url_for("notes.etud_inscrit_ue", scodoc_dept=g.scodoc_dept, etudid=etud["etudid"],
formsemestre_id=formsemestre_id, ue_id=ue["ue_id"])
url_for("notes.etud_inscrit_ue",
scodoc_dept=g.scodoc_dept, etudid=etud["etudid"],
formsemestre_id=formsemestre_id, ue_id=ue["ue_id"])
}">inscrire à {"" if is_apc else "tous les modules de"} cette UE</a></div>
"""
)

View File

@ -190,14 +190,9 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
if not isinstance(moduleimpl_id, int):
raise ScoInvalidIdType("moduleimpl_id must be an integer !")
modimpl: ModuleImpl = ModuleImpl.query.get_or_404(moduleimpl_id)
mi_dict = modimpl.to_dict()
module: Module = modimpl.module
formsemestre_id = modimpl.formsemestre_id
formsemestre: FormSemestre = modimpl.formsemestre
mod_dict = sco_edit_module.module_list(args={"module_id": modimpl.module_id})[0]
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
formation_dict = sco_formations.formation_list(
args={"formation_id": sem["formation_id"]}
)[0]
mod_inscrits = sco_moduleimpl.do_moduleimpl_inscription_list(
moduleimpl_id=moduleimpl_id
)
@ -232,22 +227,22 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
arrow_up, arrow_down, arrow_none = sco_groups.get_arrow_icons_tags()
#
module_resp = User.query.get(modimpl.responsable_id)
mod_type_name = scu.MODULE_TYPE_NAMES[mod_dict["module_type"]]
mod_type_name = scu.MODULE_TYPE_NAMES[module.module_type]
H = [
html_sco_header.sco_header(
page_title=f"{mod_type_name} {mod_dict['code']} {mod_dict['titre']}",
page_title=f"{mod_type_name} {module.code} {module.titre}",
javascripts=["js/etud_info.js"],
init_qtip=True,
),
f"""<h2 class="formsemestre">{mod_type_name}
<tt>{mod_dict['code']}</tt> {mod_dict['titre']}
{"dans l'UE " + modimpl.module.ue.acronyme
if modimpl.module.module_type == scu.ModuleType.MALUS
f"""<h2 class="formsemestre">{mod_type_name}
<tt>{module.code}</tt> {module.titre}
{"dans l'UE " + modimpl.module.ue.acronyme
if modimpl.module.module_type == scu.ModuleType.MALUS
else ""
}
</h2>
<div class="moduleimpl_tableaubord moduleimpl_type_{
scu.ModuleType(mod_dict['module_type']).name.lower()}">
scu.ModuleType(module.module_type).name.lower()}">
<table>
<tr>
<td class="fichetitre2">Responsable: </td><td class="redboldtext">
@ -294,8 +289,10 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
H.append("""</td><td></td></tr>""")
# 3ieme ligne: Formation
H.append(
"""<tr><td class="fichetitre2">Formation: </td><td>%(titre)s</td></tr>"""
% formation_dict
f"""<tr>
<td class="fichetitre2">Formation: </td><td>{formsemestre.formation.titre}</td>
</tr>
"""
)
# Ligne: Inscrits
H.append(
@ -303,18 +300,17 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
)
if current_user.has_permission(Permission.ScoEtudInscrit):
H.append(
"""<a class="stdlink" style="margin-left:2em;" href="moduleimpl_inscriptions_edit?moduleimpl_id=%s">modifier</a>"""
% mi_dict["moduleimpl_id"]
f"""<a class="stdlink" style="margin-left:2em;" href="moduleimpl_inscriptions_edit?moduleimpl_id={modimpl.id}">modifier</a>"""
)
H.append("</td></tr>")
# Ligne: règle de calcul
has_expression = sco_compute_moy.moduleimpl_has_expression(mi_dict)
has_expression = sco_compute_moy.moduleimpl_has_expression(modimpl)
if has_expression:
H.append(
f"""<tr>
<td class="fichetitre2" colspan="4">Règle de calcul:
<span class="formula" title="mode de calcul de la moyenne du module"
>moyenne=<tt>{mi_dict["computation_expr"]}</tt>
>moyenne=<tt>{modimpl.computation_expr}</tt>
</span>"""
)
H.append("""<span class="warning">inutilisée dans cette version de ScoDoc""")
@ -421,21 +417,20 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
</form>
</p>
"""
% mi_dict
)
# -------- Tableau des evaluations
top_table_links = ""
if can_edit_evals:
top_table_links = f"""<a class="stdlink" href="{
url_for("notes.evaluation_create", scodoc_dept=g.scodoc_dept, moduleimpl_id=mi_dict['moduleimpl_id'])
url_for("notes.evaluation_create", scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl.id)
}">Créer nouvelle évaluation</a>
"""
if nb_evaluations > 0:
top_table_links += f"""
<a class="stdlink" style="margin-left:2em;" href="{
url_for("notes.moduleimpl_evaluation_renumber",
scodoc_dept=g.scodoc_dept, moduleimpl_id=mi_dict['moduleimpl_id'],
scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl.id,
redirect=1)
}">Trier par date</a>
"""

View File

@ -38,18 +38,13 @@ import flask
from flask import flash, redirect, url_for
from flask import g, request
from app.models import (
Formation,
FormSemestre,
ScolarAutorisationInscription,
)
from app.models.etudiants import Identite
from app.models import FormSemestre, Identite
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
from app.scodoc import html_sco_header
from app.scodoc import codes_cursus
from app.scodoc import sco_dict_pv_jury
from app.scodoc import sco_pv_dict
from app.scodoc import sco_etud
from app.scodoc import sco_groups
from app.scodoc import sco_groups_view
@ -230,7 +225,7 @@ def formsemestre_pvjury(formsemestre_id, format="html", publish=True):
footer = html_sco_header.sco_footer()
dpv = sco_dict_pv_jury.dict_pvjury(formsemestre_id, with_prev=True)
dpv = sco_pv_dict.dict_pvjury(formsemestre_id, with_prev=True)
if not dpv:
if format == "html":
return (
@ -411,7 +406,7 @@ def formsemestre_pvjury_pdf(formsemestre_id, group_ids: list[int] = None, etudid
tf[2]["anonymous"] = bool(tf[2]["anonymous"])
try:
PDFLOCK.acquire()
pdfdoc = sco_pvpdf.pvjury_pdf(
pdfdoc = sco_pv_pdf.pvjury_pdf(
formsemestre,
etudids,
numero_arrete=tf[2]["numero_arrete"],

View File

@ -1,937 +0,0 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Emmanuel Viennet emmanuel.viennet@viennet.net
#
##############################################################################
"""Edition des PV de jury
"""
import io
import re
from PIL import Image as PILImage
from PIL import UnidentifiedImageError
import reportlab
from reportlab.lib.units import cm, mm
from reportlab.lib.enums import TA_LEFT, TA_RIGHT, TA_JUSTIFY
from reportlab.platypus import Paragraph, Spacer, Frame, PageBreak
from reportlab.platypus import Table, TableStyle, Image
from reportlab.platypus.doctemplate import PageTemplate, BaseDocTemplate
from reportlab.lib.pagesizes import A4, landscape
from reportlab.lib import styles
from reportlab.lib.colors import Color
from flask import g
from app.models import FormSemestre, Identite
import app.scodoc.sco_utils as scu
from app.scodoc import sco_bulletins_pdf
from app.scodoc import codes_cursus
from app.scodoc import sco_dict_pv_jury
from app.scodoc import sco_etud
from app.scodoc import sco_pdf
from app.scodoc import sco_preferences
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.sco_logos import find_logo
from app.scodoc.sco_cursus_dut import SituationEtudCursus
from app.scodoc.sco_pdf import SU
import sco_version
LOGO_FOOTER_ASPECT = scu.CONFIG.LOGO_FOOTER_ASPECT # XXX A AUTOMATISER
LOGO_FOOTER_HEIGHT = scu.CONFIG.LOGO_FOOTER_HEIGHT * mm
LOGO_FOOTER_WIDTH = LOGO_FOOTER_HEIGHT * scu.CONFIG.LOGO_FOOTER_ASPECT
LOGO_HEADER_ASPECT = scu.CONFIG.LOGO_HEADER_ASPECT # XXX logo IUTV (A AUTOMATISER)
LOGO_HEADER_HEIGHT = scu.CONFIG.LOGO_HEADER_HEIGHT * mm
LOGO_HEADER_WIDTH = LOGO_HEADER_HEIGHT * scu.CONFIG.LOGO_HEADER_ASPECT
def page_footer(canvas, doc, logo, preferences, with_page_numbers=True):
"Add footer on page"
width = doc.pagesize[0] # - doc.pageTemplate.left_p - doc.pageTemplate.right_p
foot = Frame(
0.1 * mm,
0.2 * cm,
width - 1 * mm,
2 * cm,
leftPadding=0,
rightPadding=0,
topPadding=0,
bottomPadding=0,
id="monfooter",
showBoundary=0,
)
left_foot_style = reportlab.lib.styles.ParagraphStyle({})
left_foot_style.fontName = preferences["SCOLAR_FONT"]
left_foot_style.fontSize = preferences["SCOLAR_FONT_SIZE_FOOT"]
left_foot_style.leftIndent = 0
left_foot_style.firstLineIndent = 0
left_foot_style.alignment = TA_RIGHT
right_foot_style = reportlab.lib.styles.ParagraphStyle({})
right_foot_style.fontName = preferences["SCOLAR_FONT"]
right_foot_style.fontSize = preferences["SCOLAR_FONT_SIZE_FOOT"]
right_foot_style.alignment = TA_RIGHT
p = sco_pdf.make_paras(
f"""<para>{preferences["INSTITUTION_NAME"]}</para><para>{
preferences["INSTITUTION_ADDRESS"]}</para>""",
left_foot_style,
)
np = Paragraph(f'<para fontSize="14">{doc.page}</para>', right_foot_style)
tabstyle = TableStyle(
[
("LEFTPADDING", (0, 0), (-1, -1), 0),
("RIGHTPADDING", (0, 0), (-1, -1), 0),
("ALIGN", (0, 0), (-1, -1), "RIGHT"),
# ('INNERGRID', (0,0), (-1,-1), 0.25, black),#debug
# ('LINEABOVE', (0,0), (-1,0), 0.5, black),
("VALIGN", (1, 0), (1, 0), "MIDDLE"),
("RIGHTPADDING", (-1, 0), (-1, 0), 1 * cm),
]
)
elems = [p]
if logo:
elems.append(logo)
colWidths = [None, LOGO_FOOTER_WIDTH + 2 * mm]
if with_page_numbers:
elems.append(np)
colWidths.append(2 * cm)
else:
elems.append("")
colWidths.append(8 * mm) # force marge droite
tab = Table([elems], style=tabstyle, colWidths=colWidths)
canvas.saveState() # is it necessary ?
foot.addFromList([tab], canvas)
canvas.restoreState()
def page_header(canvas, doc, logo, preferences, only_on_first_page=False):
"Ajoute au canvas le frame avec le logo"
if only_on_first_page and int(doc.page) > 1:
return
height = doc.pagesize[1]
head = Frame(
-22 * mm,
height - 13 * mm - LOGO_HEADER_HEIGHT,
10 * cm,
LOGO_HEADER_HEIGHT + 2 * mm,
leftPadding=0,
rightPadding=0,
topPadding=0,
bottomPadding=0,
id="monheader",
showBoundary=0,
)
if logo:
canvas.saveState() # is it necessary ?
head.addFromList([logo], canvas)
canvas.restoreState()
class CourrierIndividuelTemplate(PageTemplate):
"""Template pour courrier avisant des decisions de jury (1 page par étudiant)"""
def __init__(
self,
document,
pagesbookmarks=None,
author=None,
title=None,
subject=None,
margins=(0, 0, 0, 0), # additional margins in mm (left,top,right, bottom)
preferences=None, # dictionnary with preferences, required
force_header=False,
force_footer=False, # always add a footer (whatever the preferences, use for PV)
template_name="CourrierJuryTemplate",
):
"""Initialise our page template."""
self.pagesbookmarks = pagesbookmarks or {}
self.pdfmeta_author = author
self.pdfmeta_title = title
self.pdfmeta_subject = subject
self.preferences = preferences
self.force_header = force_header
self.force_footer = force_footer
self.with_footer = (
self.force_footer or self.preferences["PV_LETTER_WITH_HEADER"]
)
self.with_header = (
self.force_header or self.preferences["PV_LETTER_WITH_FOOTER"]
)
self.with_page_background = self.preferences["PV_LETTER_WITH_BACKGROUND"]
self.with_page_numbers = False
self.header_only_on_first_page = False
# Our doc is made of a single frame
left, top, right, bottom = margins # marge additionnelle en mm
# marges du Frame principal
self.bot_p = 2 * cm
self.left_p = 2.5 * cm
self.right_p = 2.5 * cm
self.top_p = 0 * cm
# log("margins=%s" % str(margins))
content = Frame(
self.left_p + left * mm,
self.bot_p + bottom * mm,
document.pagesize[0] - self.right_p - self.left_p - left * mm - right * mm,
document.pagesize[1] - self.top_p - self.bot_p - top * mm - bottom * mm,
)
PageTemplate.__init__(self, template_name, [content])
self.background_image_filename = None
self.logo_footer = None
self.logo_header = None
# Search logos in dept specific dir, then in global scu.CONFIG dir
if template_name == "PVJuryTemplate":
background = find_logo(
logoname="pvjury_background",
dept_id=g.scodoc_dept_id,
) or find_logo(
logoname="pvjury_background",
dept_id=g.scodoc_dept_id,
prefix="",
)
else:
background = find_logo(
logoname="letter_background",
dept_id=g.scodoc_dept_id,
) or find_logo(
logoname="letter_background",
dept_id=g.scodoc_dept_id,
prefix="",
)
if not self.background_image_filename and background is not None:
self.background_image_filename = background.filepath
footer = find_logo(logoname="footer", dept_id=g.scodoc_dept_id)
if footer is not None:
self.logo_footer = Image(
footer.filepath,
height=LOGO_FOOTER_HEIGHT,
width=LOGO_FOOTER_WIDTH,
)
header = find_logo(logoname="header", dept_id=g.scodoc_dept_id)
if header is not None:
self.logo_header = Image(
header.filepath,
height=LOGO_HEADER_HEIGHT,
width=LOGO_HEADER_WIDTH,
)
def beforeDrawPage(self, canv, doc):
"""Draws a logo and an contribution message on each page."""
# ---- Add some meta data and bookmarks
if self.pdfmeta_author:
canv.setAuthor(SU(self.pdfmeta_author))
if self.pdfmeta_title:
canv.setTitle(SU(self.pdfmeta_title))
if self.pdfmeta_subject:
canv.setSubject(SU(self.pdfmeta_subject))
bm = self.pagesbookmarks.get(doc.page, None)
if bm != None:
key = bm
txt = SU(bm)
canv.bookmarkPage(key)
canv.addOutlineEntry(txt, bm)
# ---- Background image
if self.background_image_filename and self.with_page_background:
canv.drawImage(
self.background_image_filename, 0, 0, doc.pagesize[0], doc.pagesize[1]
)
# ---- Header/Footer
if self.with_header:
page_header(
canv,
doc,
self.logo_header,
self.preferences,
self.header_only_on_first_page,
)
if self.with_footer:
page_footer(
canv,
doc,
self.logo_footer,
self.preferences,
with_page_numbers=self.with_page_numbers,
)
class PVTemplate(CourrierIndividuelTemplate):
"""Template pour les pages des PV de jury"""
def __init__(
self,
document,
author=None,
title=None,
subject=None,
margins=None, # additional margins in mm (left,top,right, bottom)
preferences=None, # dictionnary with preferences, required
):
if margins is None:
margins = (
preferences["pv_left_margin"],
preferences["pv_top_margin"],
preferences["pv_right_margin"],
preferences["pv_bottom_margin"],
)
CourrierIndividuelTemplate.__init__(
self,
document,
author=author,
title=title,
subject=subject,
margins=margins,
preferences=preferences,
force_header=True,
force_footer=True,
template_name="PVJuryTemplate",
)
self.with_page_numbers = True
self.header_only_on_first_page = True
self.with_header = self.preferences["PV_WITH_HEADER"]
self.with_footer = self.preferences["PV_WITH_FOOTER"]
self.with_page_background = self.preferences["PV_WITH_BACKGROUND"]
def afterDrawPage(self, canv, doc):
"""Called after all flowables have been drawn on a page"""
pass
def beforeDrawPage(self, canv, doc):
"""Called before any flowables are drawn on a page"""
# If the page number is even, force a page break
CourrierIndividuelTemplate.beforeDrawPage(self, canv, doc)
# Note: on cherche un moyen de generer un saut de page double
# (redémarrer sur page impaire, nouvelle feuille en recto/verso). Pas trouvé en Platypus.
#
# if self.__pageNum % 2 == 0:
# canvas.showPage()
# # Increment pageNum again since we've added a blank page
# self.__pageNum += 1
def _simulate_br(paragraph_txt: str, para="<para>") -> str:
"""Reportlab bug turnaround (could be removed in a future version).
p is a string with Reportlab intra-paragraph XML tags.
Replaces <br> (currently ignored by Reportlab) by </para><para>
Also replaces <br> by <br/>
"""
return ("</para>" + para).join(
re.split(r"<.*?br.*?/>", paragraph_txt.replace("<br>", "<br/>"))
)
def _make_signature_image(signature, leftindent, formsemestre_id) -> Table:
"crée un paragraphe avec l'image signature"
# cree une image PIL pour avoir la taille (W,H)
f = io.BytesIO(signature)
img = PILImage.open(f)
width, height = img.size
pdfheight = (
1.0
* sco_preferences.get_preference("pv_sig_image_height", formsemestre_id)
* mm
)
f.seek(0, 0)
style = styles.ParagraphStyle({})
style.leading = 1.0 * sco_preferences.get_preference(
"SCOLAR_FONT_SIZE", formsemestre_id
) # vertical space
style.leftIndent = leftindent
return Table(
[("", Image(f, width=width * pdfheight / float(height), height=pdfheight))],
colWidths=(9 * cm, 7 * cm),
)
def pdf_lettres_individuelles(
formsemestre_id,
etudids=None,
date_jury="",
date_commission="",
signature=None,
):
"""Document PDF avec les lettres d'avis pour les etudiants mentionnés
(tous ceux du semestre, ou la liste indiquée par etudids)
Renvoie pdf data ou chaine vide si aucun etudiant avec décision de jury.
"""
dpv = sco_dict_pv_jury.dict_pvjury(formsemestre_id, etudids=etudids, with_prev=True)
if not dpv:
return ""
# Ajoute infos sur etudiants
etuds = [x["identite"] for x in dpv["decisions"]]
sco_etud.fill_etuds_info(etuds)
#
formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id)
prefs = sco_preferences.SemPreferences(formsemestre_id)
params = {
"date_jury": date_jury,
"date_commission": date_commission,
"titre_formation": dpv["formation"]["titre_officiel"],
"htab1": "8cm", # lignes à droite (entete, signature)
"htab2": "1cm",
}
# copie preferences
for name in sco_preferences.get_base_preferences().prefs_name:
params[name] = sco_preferences.get_preference(name, formsemestre_id)
bookmarks = {}
objects = [] # list of PLATYPUS objects
npages = 0
for decision in dpv["decisions"]:
if (
decision["decision_sem"]
or decision.get("decision_annee")
or decision.get("decision_rcue")
): # decision prise
etud: Identite = Identite.query.get(decision["identite"]["etudid"])
params["nomEtud"] = etud.nomprenom
bookmarks[npages + 1] = scu.suppress_accents(etud.nomprenom)
try:
objects += pdf_lettre_individuelle(
dpv["formsemestre"], decision, etud, params, signature
)
except UnidentifiedImageError as exc:
raise ScoValueError(
"Fichier image (signature ou logo ?) invalide !"
) from exc
objects.append(PageBreak())
npages += 1
if npages == 0:
return ""
# Paramètres de mise en page
margins = (
prefs["left_margin"],
prefs["top_margin"],
prefs["right_margin"],
prefs["bottom_margin"],
)
# ----- Build PDF
report = io.BytesIO() # in-memory document, no disk file
document = BaseDocTemplate(report)
document.addPageTemplates(
CourrierIndividuelTemplate(
document,
author=f"{sco_version.SCONAME} {sco_version.SCOVERSION} (E. Viennet)",
title=f"Lettres décision {formsemestre.titre_annee()}",
subject="Décision jury",
margins=margins,
pagesbookmarks=bookmarks,
preferences=prefs,
)
)
document.build(objects)
data = report.getvalue()
return data
def _descr_jury(formsemestre: FormSemestre, diplome):
if not diplome:
if formsemestre.formation.is_apc():
t = f"""BUT{(formsemestre.semestre_id+1)//2}"""
s = t
else:
t = f"""passage de Semestre {formsemestre.semestre_id} en Semestre {formsemestre.semestre_id + 1}"""
s = "passage de semestre"
else:
t = "délivrance du diplôme"
s = t
return t, s # titre long, titre court
def pdf_lettre_individuelle(sem, decision, etud: Identite, params, signature=None):
"""
Renvoie une liste d'objets PLATYPUS pour intégration
dans un autre document.
"""
#
formsemestre_id = sem["formsemestre_id"]
formsemestre = FormSemestre.query.get(formsemestre_id)
Se: SituationEtudCursus = decision["Se"]
t, s = _descr_jury(
formsemestre, Se.parcours_validated() or not Se.semestre_non_terminal
)
objects = []
style = reportlab.lib.styles.ParagraphStyle({})
style.fontSize = 14
style.fontName = sco_preferences.get_preference("PV_FONTNAME", formsemestre_id)
style.leading = 18
style.alignment = TA_LEFT
params["semestre_id"] = formsemestre.semestre_id
params["decision_sem_descr"] = decision["decision_sem_descr"]
params["type_jury"] = t # type de jury (passage ou delivrance)
params["type_jury_abbrv"] = s # idem, abbrégé
params["decisions_ue_descr"] = decision["decisions_ue_descr"]
if decision["decisions_ue_nb"] > 1:
params["decisions_ue_descr_plural"] = "s"
else:
params["decisions_ue_descr_plural"] = ""
params["INSTITUTION_CITY"] = (
sco_preferences.get_preference("INSTITUTION_CITY", formsemestre_id) or ""
)
if decision["prev_decision_sem"]:
params["prev_semestre_id"] = decision["prev"]["semestre_id"]
params["prev_decision_sem_txt"] = ""
params["decision_orig"] = ""
params.update(decision["identite"])
# fix domicile
if params["domicile"]:
params["domicile"] = params["domicile"].replace("\\n", "<br/>")
# UE capitalisées:
if decision["decisions_ue"] and decision["decisions_ue_descr"]:
params["decision_ue_txt"] = (
"""<b>Unité%(decisions_ue_descr_plural)s d'Enseignement %(decision_orig)s capitalisée%(decisions_ue_descr_plural)s : %(decisions_ue_descr)s</b>"""
% params
)
else:
params["decision_ue_txt"] = ""
# Mention
params["mention"] = decision["mention"]
# Informations sur compensations
if decision["observation"]:
params["observation_txt"] = (
"""<b>Observation :</b> %(observation)s.""" % decision
)
else:
params["observation_txt"] = ""
# Autorisations de passage
if decision["autorisations"] and not Se.parcours_validated():
if len(decision["autorisations"]) > 1:
s = "s"
else:
s = ""
params[
"autorisations_txt"
] = """Vous êtes autorisé%s à continuer dans le%s semestre%s : <b>%s</b>""" % (
etud.e,
s,
s,
decision["autorisations_descr"],
)
else:
params["autorisations_txt"] = ""
if decision["decision_sem"] and Se.parcours_validated():
params["diplome_txt"] = (
"""Vous avez donc obtenu le diplôme : <b>%(titre_formation)s</b>""" % params
)
else:
params["diplome_txt"] = ""
# Les fonctions ci-dessous ajoutent ou modifient des champs:
if formsemestre.formation.is_apc():
# ajout champs spécifiques PV BUT
add_apc_infos(formsemestre, params, decision)
else:
# ajout champs spécifiques PV DUT
add_classic_infos(formsemestre, params, decision)
# Corps de la lettre:
objects += sco_bulletins_pdf.process_field(
sco_preferences.get_preference("PV_LETTER_TEMPLATE", sem["formsemestre_id"]),
params,
style,
suppress_empty_pars=True,
)
# Signature:
# nota: si semestre terminal, signature par directeur IUT, sinon, signature par
# chef de département.
if Se.semestre_non_terminal:
sig = (
sco_preferences.get_preference(
"PV_LETTER_PASSAGE_SIGNATURE", formsemestre_id
)
or ""
) % params
sig = _simulate_br(sig, '<para leftindent="%(htab1)s">')
objects += sco_pdf.make_paras(
(
"""<para leftindent="%(htab1)s" spaceBefore="25mm">"""
+ sig
+ """</para>"""
)
% params,
style,
)
else:
sig = (
sco_preferences.get_preference(
"PV_LETTER_DIPLOMA_SIGNATURE", formsemestre_id
)
or ""
) % params
sig = _simulate_br(sig, '<para leftindent="%(htab1)s">')
objects += sco_pdf.make_paras(
(
"""<para leftindent="%(htab1)s" spaceBefore="25mm">"""
+ sig
+ """</para>"""
)
% params,
style,
)
if signature:
try:
objects.append(
_make_signature_image(signature, params["htab1"], formsemestre_id)
)
except UnidentifiedImageError as exc:
raise ScoValueError("Image signature invalide !") from exc
return objects
def add_classic_infos(formsemestre: FormSemestre, params: dict, decision: dict):
"""Ajoute les champs pour les formations classiques, donc avec codes semestres"""
if decision["prev_decision_sem"]:
params["prev_code_descr"] = decision["prev_code_descr"]
params[
"prev_decision_sem_txt"
] = f"""<b>Décision du semestre antérieur S{params['prev_semestre_id']} :</b> {params['prev_code_descr']}"""
# Décision semestre courant:
if formsemestre.semestre_id >= 0:
params["decision_orig"] = f"du semestre S{formsemestre.semestre_id}"
else:
params["decision_orig"] = ""
def add_apc_infos(formsemestre: FormSemestre, params: dict, decision: dict):
"""Ajoute les champs pour les formations APC (BUT), donc avec codes RCUE et année"""
annee_but = (formsemestre.semestre_id + 1) // 2
params["decision_orig"] = f"année BUT{annee_but}"
if decision is None:
params["decision_sem_descr"] = ""
params["decision_ue_txt"] = ""
else:
decision_annee = decision.get("decision_annee") or {}
params["decision_sem_descr"] = decision_annee.get("code") or ""
params[
"decision_ue_txt"
] = f"""{params["decision_ue_txt"]}<br/>
<b>Niveaux de compétences:</b><br/> {decision.get("descr_decisions_rcue") or ""}
"""
# ----------------------------------------------
def pvjury_pdf(
formsemestre: FormSemestre,
etudids: list[int],
date_commission=None,
date_jury=None,
numero_arrete=None,
code_vdi=None,
show_title=False,
pv_title=None,
with_paragraph_nom=False,
anonymous=False,
) -> bytes:
"""Doc PDF récapitulant les décisions de jury
(tableau en format paysage)
"""
objects, a_diplome = _pvjury_pdf_type(
formsemestre,
etudids,
only_diplome=False,
date_commission=date_commission,
numero_arrete=numero_arrete,
code_vdi=code_vdi,
date_jury=date_jury,
show_title=show_title,
pv_title=pv_title,
with_paragraph_nom=with_paragraph_nom,
anonymous=anonymous,
)
if not objects:
return b""
jury_de_diplome = formsemestre.est_terminal()
# Si Jury de passage et qu'un étudiant valide le parcours (car il a validé antérieurement le dernier semestre)
# alors on génère aussi un PV de diplome (à la suite dans le même doc PDF)
if not jury_de_diplome and a_diplome:
# au moins un etudiant a validé son diplome:
objects.append(PageBreak())
objects += _pvjury_pdf_type(
formsemestre,
etudids,
only_diplome=True,
date_commission=date_commission,
date_jury=date_jury,
numero_arrete=numero_arrete,
code_vdi=code_vdi,
show_title=show_title,
pv_title=pv_title,
with_paragraph_nom=with_paragraph_nom,
anonymous=anonymous,
)[0]
# ----- Build PDF
report = io.BytesIO() # in-memory document, no disk file
document = BaseDocTemplate(report)
document.pagesize = landscape(A4)
document.addPageTemplates(
PVTemplate(
document,
author=f"{sco_version.SCONAME} {sco_version.SCOVERSION} (E. Viennet)",
title=SU(f"PV du jury de {formsemestre.titre_num()}"),
subject="PV jury",
preferences=sco_preferences.SemPreferences(formsemestre.id),
)
)
document.build(objects)
data = report.getvalue()
return data
def _pvjury_pdf_type(
formsemestre: FormSemestre,
etudids: list[int],
only_diplome=False,
date_commission=None,
date_jury=None,
numeroArrete=None,
VDICode=None,
showTitle=False,
pv_title=None,
anonymous=False,
with_paragraph_nom=False,
) -> tuple[list, bool]:
"""Objets platypus PDF récapitulant les décisions de jury
pour un type de jury (passage ou delivrance).
Ramene: liste d'onj platypus, et un boolen indiquant si au moins un étudiant est diplômé.
"""
from app.scodoc import sco_pvjury
a_diplome = False
# Jury de diplome si sem. terminal OU que l'on demande seulement les diplomés
diplome = formsemestre.est_terminal() or only_diplome
titre_jury, _ = _descr_jury(formsemestre, diplome)
titre_diplome = pv_title or formsemestre.formation.titre_officiel
objects = []
style = reportlab.lib.styles.ParagraphStyle({})
style.fontSize = 12
style.fontName = sco_preferences.get_preference("PV_FONTNAME", formsemestre_id)
style.leading = 18
style.alignment = TA_JUSTIFY
indent = 1 * cm
bulletStyle = reportlab.lib.styles.ParagraphStyle({})
bulletStyle.fontSize = 12
bulletStyle.fontName = sco_preferences.get_preference(
"PV_FONTNAME", formsemestre_id
)
bulletStyle.leading = 12
bulletStyle.alignment = TA_JUSTIFY
bulletStyle.firstLineIndent = 0
bulletStyle.leftIndent = indent
bulletStyle.bulletIndent = indent
bulletStyle.bulletFontName = "Times-Roman"
bulletStyle.bulletFontSize = 11
bulletStyle.spaceBefore = 5 * mm
bulletStyle.spaceAfter = 5 * mm
objects += [Spacer(0, 5 * mm)]
objects += sco_pdf.make_paras(
f"""
<para align="center"><b>Procès-verbal de {titre_jury} du département {
sco_preferences.get_preference("DeptName", formsemestre.id) or "(sans nom)"
} - Session unique {formsemestre.annee_scolaire()}</b></para>
""",
style,
)
objects += sco_pdf.make_paras(
"""
<para align="center"><b><i>%s</i></b></para>
"""
% titre_diplome,
style,
)
if showTitle:
objects += sco_pdf.make_paras(
"""<para align="center"><b>Semestre: %s</b></para>""" % sem["titre"], style
)
if sco_preferences.get_preference("PV_TITLE_WITH_VDI", formsemestre.id):
objects += sco_pdf.make_paras(
"""<para align="center">VDI et Code: %s</para>""" % (VDICode or ""), style
)
if date_jury:
objects += sco_pdf.make_paras(
"""<para align="center">Jury tenu le %s</para>""" % date_jury, style
)
objects += sco_pdf.make_paras(
"<para>"
+ (sco_preferences.get_preference("PV_INTRO", formsemestre.id) or "")
% {
"Decnum": numero_arrete,
"VDICode": code_vdi,
"UnivName": sco_preferences.get_preference("UnivName", formsemestre.id),
"Type": titre_jury,
"Date": date_commission, # deprecated
"date_commission": date_commission,
}
+ "</para>",
bulletStyle,
)
objects += sco_pdf.make_paras(
"""<para>Le jury propose les décisions suivantes :</para>""", style
)
objects += [Spacer(0, 4 * mm)]
if formsemestre.formation.is_apc():
rows, titles = jury_but_pv.pvjury_table_but(
formsemestre, etudids=etudids, line_sep="<br/>"
)
columns_ids = list(titles.keys())
a_diplome = codes_cursus.ADM in [row.get("diplome") for row in rows]
else:
dpv = sco_dict_pv_jury.dict_pvjury(
formsemestre.id, etudids=etudids, with_prev=True
)
if not dpv:
return [], False
rows, titles, columns_ids = sco_pvjury.pvjury_table(
dpv,
only_diplome=only_diplome,
anonymous=anonymous,
with_paragraph_nom=with_paragraph_nom,
)
a_diplome = True in (x["validation_parcours"] for x in dpv["decisions"])
# convert to lists of tuples:
columns_ids = ["etudid"] + columns_ids
lines = [[line.get(x, "") for x in columns_ids] for line in lines]
titles = [titles.get(x, "") for x in columns_ids]
# Make a new cell style and put all cells in paragraphs
cell_style = styles.ParagraphStyle({})
cell_style.fontSize = sco_preferences.get_preference(
"SCOLAR_FONT_SIZE", formsemestre.id
)
cell_style.fontName = sco_preferences.get_preference("PV_FONTNAME", formsemestre.id)
cell_style.leading = 1.0 * sco_preferences.get_preference(
"SCOLAR_FONT_SIZE", formsemestre.id
) # vertical space
LINEWIDTH = 0.5
table_style = [
(
"FONTNAME",
(0, 0),
(-1, 0),
sco_preferences.get_preference("PV_FONTNAME", formsemestre.id),
),
("LINEBELOW", (0, 0), (-1, 0), LINEWIDTH, Color(0, 0, 0)),
("GRID", (0, 0), (-1, -1), LINEWIDTH, Color(0, 0, 0)),
("VALIGN", (0, 0), (-1, -1), "TOP"),
]
titles = ["<para><b>%s</b></para>" % x for x in titles]
def _format_pv_cell(x):
"""convert string to paragraph"""
if isinstance(x, str):
return Paragraph(SU(x), cell_style)
else:
return x
widths_by_id = {
"nom": 5 * cm,
"cursus": 2.8 * cm,
"ects": 1.4 * cm,
"devenir": 1.8 * cm,
"decision_but": 1.8 * cm,
}
table_cells = [[_format_pv_cell(x) for x in line[1:]] for line in ([titles] + rows)]
widths = [widths_by_id.get(col_id) for col_id in columns_ids[1:]]
objects.append(
Table(table_cells, repeatRows=1, colWidths=widths, style=table_style)
)
# Signature du directeur
objects += sco_pdf.make_paras(
f"""<para spaceBefore="10mm" align="right">{
sco_preferences.get_preference("DirectorName", formsemestre.id) or ""
}, {
sco_preferences.get_preference("DirectorTitle", formsemestre.id) or ""
}</para>""",
style,
)
# Légende des codes
codes = list(codes_cursus.CODES_EXPL.keys())
codes.sort()
objects += sco_pdf.make_paras(
"""<para spaceBefore="15mm" fontSize="14">
<b>Codes utilisés :</b></para>""",
style,
)
L = []
for code in codes:
L.append((code, codes_cursus.CODES_EXPL[code]))
TableStyle2 = [
(
"FONTNAME",
(0, 0),
(-1, 0),
sco_preferences.get_preference("PV_FONTNAME", formsemestre.id),
),
("LINEBELOW", (0, 0), (-1, -1), LINEWIDTH, Color(0, 0, 0)),
("LINEABOVE", (0, 0), (-1, -1), LINEWIDTH, Color(0, 0, 0)),
("LINEBEFORE", (0, 0), (0, -1), LINEWIDTH, Color(0, 0, 0)),
("LINEAFTER", (-1, 0), (-1, -1), LINEWIDTH, Color(0, 0, 0)),
]
objects.append(
Table(
[[Paragraph(SU(x), cell_style) for x in line] for line in L],
colWidths=(2 * cm, None),
style=TableStyle2,
)
)
return objects, a_diplome

View File

@ -169,13 +169,14 @@ def formsemestre_recapcomplet(
if len(formsemestre.inscriptions) > 0:
H.append("""<div class="links_under_recap"><ul>""")
H.append(
f"""<li><a class="stdlink" href="{url_for('notes.formsemestre_recapcomplet',
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, mode_jury=1)
}">Décisions du jury</a>
</li>
"""
)
if not mode_jury:
H.append(
f"""<li><a class="stdlink" href="{url_for('notes.formsemestre_recapcomplet',
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, mode_jury=1)
}">Décisions du jury</a>
</li>
"""
)
if formsemestre.can_edit_jury():
if mode_jury:
H.append(
@ -189,6 +190,22 @@ def formsemestre_recapcomplet(
</li>
"""
)
if mode_jury:
H.append(
f"""<li><a class="stdlink" href="{
url_for('notes.formsemestre_lettres_individuelles',
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, mode_jury=1)
}">Courriers individuels (classeur pdf)</a>
</li>
"""
)
H.append(
f"""<li><a class="stdlink" href="{url_for('notes.formsemestre_pvjury_pdf',
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, mode_jury=1)
}">PV officiel (pdf)</a>
</li>
"""
)
H.append("</ul></div>")
if sco_preferences.get_preference("use_ue_coefs", formsemestre_id):
@ -431,13 +448,15 @@ def gen_formsemestre_recapcomplet_html_table(
"""
table = None
table_html = None
if not (mode_jury or selected_etudid):
if include_evaluations:
table_html = sco_cache.TableRecapWithEvalsCache.get(formsemestre.id)
else:
table_html = sco_cache.TableRecapCache.get(formsemestre.id)
# en mode jury ne cache pas la table html
if mode_jury or (table_html is None):
cache_class = {
(True, True): sco_cache.TableJuryWithEvalsCache,
(True, False): sco_cache.TableJuryCache,
(False, True): sco_cache.TableRecapWithEvalsCache,
(False, False): sco_cache.TableRecapCache,
}[(bool(mode_jury), bool(include_evaluations))]
if not selected_etudid:
table_html = cache_class.get(formsemestre.id)
if table_html is None:
table = _gen_formsemestre_recapcomplet_table(
res,
include_evaluations,
@ -446,11 +465,7 @@ def gen_formsemestre_recapcomplet_html_table(
selected_etudid=selected_etudid,
)
table_html = table.html()
if not mode_jury:
if include_evaluations:
sco_cache.TableRecapWithEvalsCache.set(formsemestre.id, table_html)
else:
sco_cache.TableRecapCache.set(formsemestre.id, table_html)
cache_class.set(formsemestre.id, table_html)
return table_html, table
@ -472,6 +487,7 @@ def _gen_formsemestre_recapcomplet_table(
mode_jury=mode_jury,
read_only=not res.formsemestre.can_edit_jury(),
)
table.data["filename"] = filename
table.select_row(selected_etudid)
return table

View File

@ -1574,7 +1574,8 @@ def formsemestre_graph_cursus(
allkeys=False, # unused
):
"""Graphe suivi cohortes"""
annee_bac = str(annee_bac)
annee_bac = str(annee_bac or "")
annee_admission = str(annee_admission or "")
# log("formsemestre_graph_cursus")
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
if format == "pdf":

View File

@ -1,401 +0,0 @@
##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
"""Classes pour aider à construire des tables de résultats
"""
from collections import defaultdict
class Element:
def __init__(
self,
elt: str,
content=None,
classes: list[str] = None,
attrs: dict[str, str] = None,
data: dict = None,
):
self.elt = elt
self.attrs = attrs or {}
self.classes = classes or []
"list of classes for the element"
self.content = content
self.data = data or {}
"data-xxx"
def html(self, extra_classes: list[str] = None) -> str:
"html for element"
classes = [cls for cls in (self.classes + (extra_classes or [])) if cls]
attrs_str = f"""class="{' '.join(classes)}" """ if classes else ""
# Autres attributs:
attrs_str += " " + " ".join([f'{k}="{v}"' for (k, v) in self.attrs.items()])
# et data-x
attrs_str += " " + " ".join([f'data-{k}="{v}"' for k, v in self.data.items()])
return f"""<{self.elt} {attrs_str}>{self.html_content()}</{self.elt}>"""
def html_content(self) -> str:
"Le contenu de l'élément, en html."
return str(self.content or "")
class Table(Element):
"""Construction d'une table de résultats
table = Table()
row = table.new_row(id="xxx", category="yyy")
row.new_cell( col_id, title, content [,classes] [, idx], [group], [keys:dict={}] )
rows = table.get_rows([category="yyy"])
table.sort_rows(key [, reverse])
table.set_titles(titles)
table.update_titles(titles)
table.set_column_groups(groups: list[str])
table.insert_group(group:str, [after=str], [before=str])
Ordre des colonnes: groupées par groupes, et dans chaque groupe par ordre d'insertion
On fixe l'ordre des groupes par ordre d'insertion
ou par insert_group ou par set_column_groups.
"""
def __init__(
self,
selected_row_id: str = None,
classes: list[str] = None,
attrs: dict[str, str] = None,
data: dict = None,
):
super().__init__("table", classes=classes, attrs=attrs, data=data)
self.rows: list["Row"] = []
"ordered list of Rows"
self.row_by_id: dict[str, "Row"] = {}
self.column_ids = []
"ordered list of columns ids"
self.groups = []
"ordered list of column groups names"
self.head = []
self.foot = []
self.column_group = {}
"the group of the column: { col_id : group }"
self.column_classes: defaultdict[str, list[str]] = defaultdict(lambda: [])
"classe ajoutée à toutes les cellules de la colonne: { col_id : class }"
self.selected_row_id = selected_row_id
"l'id de la ligne sélectionnée"
self.titles = {}
"Column title: { col_id : titre }"
self.head_title_row: "Row" = Row(self, "title_head", cell_elt="th")
self.foot_title_row: "Row" = Row(self, "title_foot", cell_elt="th")
self.empty_cell = Cell.empty()
def _prepare(self):
"""Prepare the table before generation:
Sort table columns, add header/footer titles rows
"""
self.sort_columns()
# Titres
self.add_head_row(self.head_title_row)
self.add_foot_row(self.foot_title_row)
def get_row_by_id(self, row_id) -> "Row":
"return the row, or None"
return self.row_by_id.get(row_id)
def is_empty(self) -> bool:
"true if table has no rows"
return len(self.rows) == 0
def select_row(self, row_id):
"mark rows as 'selected'"
self.selected_row_id = row_id
def to_list(self) -> list[dict]:
"""as a list, each row is a dict"""
self._prepare()
return [row.to_dict() for row in self.rows]
def html(self, extra_classes: list[str] = None) -> str:
"""HTML version of the table"""
self._prepare()
return super().html(extra_classes=extra_classes)
def html_content(self) -> str:
"""Le contenu de la table en html."""
newline = "\n"
header = (
f"""
<thead>
{ newline.join(row.html() for row in self.head) }
</thead>
"""
if self.head
else ""
)
footer = (
f"""
<tfoot>
{ newline.join(row.html() for row in self.foot) }
</tfoot>
"""
if self.foot
else ""
)
return f"""
{header}
<tbody>
{
newline.join(row.html() for row in self.rows)
}
</tbody>
{footer}
"""
def add_row(self, row: "Row") -> "Row":
"""Append a new row"""
self.rows.append(row)
self.row_by_id[row.id] = row
return row
def add_head_row(self, row: "Row") -> "Row":
"Add a row to table head"
# row = Row(self, cell_elt="th", category="head")
self.head.append(row)
self.row_by_id[row.id] = row
return row
def add_foot_row(self, row: "Row") -> "Row":
"Add a row to table foot"
self.foot.append(row)
self.row_by_id[row.id] = row
return row
def sort_rows(self, key: callable, reverse: bool = False):
"""Sort table rows"""
self.rows.sort(key=key, reverse=reverse)
def sort_columns(self):
"""Sort columns ids"""
groups_order = {group: i for i, group in enumerate(self.groups)}
cols_order = {col_id: i for i, col_id in enumerate(self.column_ids)}
self.column_ids.sort(
key=lambda col_id: (
groups_order.get(self.column_group.get(col_id), col_id),
cols_order[col_id],
)
)
def insert_group(self, group: str, after: str = None, before: str = None):
"""Déclare un groupe de colonnes et le place avant ou après un autre groupe.
Si pas d'autre groupe indiqué, le place après, à droite du dernier.
Si le group existe déjà, ne fait rien (ne le déplace pas).
"""
if group in self.groups:
return
other = after or before
if other is None:
self.groups.append(group)
else:
if not other in self.groups:
raise ValueError(f"invalid column group '{other}'")
index = self.groups.index(other)
if after:
index += 1
self.groups.insert(index, group)
def set_groups(self, groups: list[str]):
"""Define column groups and set order"""
self.groups = groups
def set_titles(self, titles: dict[str, str]):
"""Set columns titles"""
self.titles = titles
def update_titles(self, titles: dict[str, str]):
"""Set columns titles"""
self.titles.update(titles)
def add_title(
self, col_id, title: str = None, classes: list[str] = None
) -> tuple["Cell", "Cell"]:
"""Record this title,
and create cells for footer and header if they don't already exist.
"""
title = title or ""
if col_id not in self.titles:
self.titles[col_id] = title
self.head_title_row.cells[col_id] = self.head_title_row.add_cell(
col_id, None, title, classes=classes
)
self.foot_title_row.cells[col_id] = self.foot_title_row.add_cell(
col_id, None, title, classes=classes
)
return self.head_title_row.cells.get(col_id), self.foot_title_row.cells[col_id]
class Row(Element):
"""A row."""
def __init__(
self,
table: Table,
row_id=None,
category=None,
cell_elt: str = None,
classes: list[str] = None,
attrs: dict[str, str] = None,
data: dict = None,
):
super().__init__("tr", classes=classes, attrs=attrs, data=data)
self.category = category
self.cells = {}
self.cell_elt = cell_elt
self.classes: list[str] = classes or []
"classes sur le <tr>"
self.id = row_id
self.table = table
def add_cell(
self,
col_id: str,
title: str,
content: str,
group: str = None,
attrs: list[str] = None,
classes: list[str] = None,
data: dict[str, str] = None,
elt: str = None,
raw_content=None,
target_attrs: dict = None,
target: str = None,
) -> "Cell":
"""Create cell and add it to the row.
group: groupe de colonnes
classes is a list of css class names
"""
cell = Cell(
content,
(classes or []) + [group or ""], # ajoute le nom de groupe aux classes
elt=elt or self.cell_elt,
attrs=attrs,
data=data,
raw_content=raw_content,
target=target,
target_attrs=target_attrs,
)
return self.add_cell_instance(col_id, cell, column_group=group, title=title)
def add_cell_instance(
self, col_id: str, cell: "Cell", column_group: str = None, title: str = None
) -> "Cell":
"""Add a cell to the row.
Si title est None, il doit avoir été ajouté avec table.add_title().
"""
cell.data["group"] = column_group
self.cells[col_id] = cell
if col_id not in self.table.column_ids:
self.table.column_ids.append(col_id)
self.table.insert_group(column_group)
if column_group is not None:
self.table.column_group[col_id] = column_group
if title is not None:
self.table.add_title(col_id, title, classes=cell.classes)
return cell
def html(self, extra_classes: list[str] = None) -> str:
"""html for row, with cells"""
if (self.id is not None) and self.id == getattr(self.table, "selected_row_id"):
self.classes.append("row_selected")
return super().html(extra_classes=extra_classes)
def html_content(self) -> str:
"Le contenu du row en html."
return "".join(
[
self.cells.get(col_id, self.table.empty_cell).html(
extra_classes=self.table.column_classes.get(col_id)
)
for col_id in self.table.column_ids
]
)
def to_dict(self) -> dict:
"""row as a dict, with only cell contents"""
return {
col_id: self.cells.get(col_id, self.table.empty_cell).raw_content
for col_id in self.table.column_ids
}
class BottomRow(Row):
"""Une ligne spéciale pour le pied de table
avec un titre à gauche
(répété sur les colonnes indiquées par left_title_col_ids),
et automatiquement ajouté au footer.
"""
def __init__(
self, *args, left_title_col_ids: list[str] = None, left_title=None, **kwargs
):
super().__init__(*args, **kwargs)
self.left_title_col_ids = left_title_col_ids
if left_title is not None:
self.set_left_title(left_title)
self.table.add_foot_row(self)
def set_left_title(self, title: str = ""):
"Fill left title cells"
for col_id in self.left_title_col_ids:
self.add_cell(col_id, None, title)
class Cell(Element):
"""Une cellule de table"""
def __init__(
self,
content,
classes: list[str] = None,
elt="td",
attrs: dict[str, str] = None,
data: dict = None,
raw_content=None,
target: str = None,
target_attrs: dict = None,
):
"""if specified, raw_content will be used for raw exports like xlsx"""
super().__init__(
elt if elt is not None else "td", content, classes, attrs, data
)
if self.elt == "th":
self.attrs["scope"] = "row"
self.data = data or {}
self.raw_content = raw_content or content
self.target = target
self.target_attrs = target_attrs or {}
@classmethod
def empty(cls):
"create a new empty cell"
return cls("")
def __str__(self):
return str(self.content)
def html_content(self) -> str:
"content of the table cell, as html"
# entoure le contenu par un lien ?
if (self.target is not None) or self.target_attrs:
href = f'href="{self.target}"' if self.target else ""
target_attrs_str = " ".join(
[f'{k}="{v}"' for (k, v) in self.target_attrs.items()]
)
return f"<a {href} {target_attrs_str}>{super().html_content()}</a>"
return super().html_content()

View File

@ -4171,10 +4171,29 @@ table.table_recap .cursus {
white-space: nowrap;
}
table.table_recap .col_ue,
table.table_recap .col_ue_code,
table.table_recap .col_moy_gen,
table.table_recap .group {
table.table_recap td.col_rcue,
table.table_recap th.col_rcue,
table.table_recap td.cursus_but.first,
table.table_recap td.cursus_but.first {
border-left: 1px solid rgb(221, 221, 221);
}
table.table_recap td.cursus_BUT1 {
color: #007bff;
}
table.table_recap td.cursus_BUT2 {
color: #d39f00;
}
table.table_recap td.cursus_BUT3 {
color: #7f00ff;
}
table.table_recap td.col_ue,
table.table_recap td.col_ue_code,
table.table_recap td.col_moy_gen,
table.table_recap td.group {
border-left: 1px solid blue;
}

View File

@ -1,80 +1,94 @@
// active les menus des codes "manuels" (année, RCUEs)
function enable_manual_codes(elt) {
$(".jury_but select.manual").prop("disabled", !elt.checked);
$(".jury_but select.manual").prop("disabled", !elt.checked);
}
// changement d'un menu code:
function change_menu_code(elt) {
// Ajuste styles pour visualiser codes enregistrés/modifiés
if (elt.value != elt.dataset.orig_code) {
elt.parentElement.parentElement.classList.add("modified");
} else {
elt.parentElement.parentElement.classList.remove("modified");
}
if (elt.value == elt.dataset.orig_recorded) {
elt.parentElement.parentElement.classList.add("recorded");
} else {
elt.parentElement.parentElement.classList.remove("recorded");
}
// Si RCUE passant en ADJ, change les menus des UEs associées ADJR
if (
elt.classList.contains("code_rcue") &&
elt.dataset.niveau_id &&
elt.value == "ADJ" &&
elt.value != elt.dataset.orig_recorded
) {
let ue_selects =
elt.parentElement.parentElement.parentElement.querySelectorAll(
"select.ue_rcue_" + elt.dataset.niveau_id
);
ue_selects.forEach((select) => {
if (select.value != "ADM") {
select.value = "ADJR";
change_menu_code(select); // pour changer les styles
}
});
}
// Ajuste styles pour visualiser codes enregistrés/modifiés
if (elt.value != elt.dataset.orig_code) {
elt.parentElement.parentElement.classList.add("modified");
} else {
elt.parentElement.parentElement.classList.remove("modified");
}
if (elt.value == elt.dataset.orig_recorded) {
elt.parentElement.parentElement.classList.add("recorded");
} else {
elt.parentElement.parentElement.classList.remove("recorded");
}
// Si RCUE passant en ADJ, change les menus des UEs associées ADJR
if (elt.classList.contains("code_rcue")
&& elt.dataset.niveau_id
&& elt.value == "ADJ"
&& elt.value != elt.dataset.orig_recorded) {
let ue_selects = elt.parentElement.parentElement.parentElement.querySelectorAll(
"select.ue_rcue_" + elt.dataset.niveau_id);
ue_selects.forEach(select => {
if (select.value != "ADM") {
select.value = "ADJR";
change_menu_code(select); // pour changer les styles
}
});
}
}
$(function () {
// Recupère la liste ordonnées des etudids
// pour avoir le "suivant" etr le "précédent"
// (liens de navigation)
const url = new URL(document.URL);
const frags = url.pathname.split("/"); // .../formsemestre_validation_but/formsemestre_id/etudid
const etudid = frags[frags.length - 1];
const formsemestre_id = frags[frags.length - 2];
const etudids_key = JSON.stringify(["etudids", url.origin, formsemestre_id]);
const etudids_str = localStorage.getItem(etudids_key);
const noms_key = JSON.stringify(["noms", url.origin, formsemestre_id]);
const noms_str = localStorage.getItem(noms_key);
if (etudids_str && noms_str) {
const etudids = JSON.parse(etudids_str);
const noms = JSON.parse(noms_str);
const cur_idx = etudids.indexOf(etudid);
let prev_idx = -1;
let next_idx = -1;
if (cur_idx != -1) {
if (cur_idx > 0) {
prev_idx = cur_idx - 1;
}
if (cur_idx < etudids.length - 1) {
next_idx = cur_idx + 1;
}
}
if (prev_idx != -1) {
let elem = document.querySelector("div.prev a");
if (elem) {
elem.href = elem.href.replace("PREV", etudids[prev_idx]);
elem.innerHTML = noms[prev_idx];
}
// Recupère la liste ordonnées des etudids
// pour avoir le "suivant" et le "précédent"
// (liens de navigation)
const url = new URL(document.URL);
const frags = url.pathname.split("/"); // .../formsemestre_validation_but/formsemestre_id/etudid
const etudid = frags[frags.length - 1];
const formsemestre_id = frags[frags.length - 2];
const etudids_key = JSON.stringify(["etudids", url.origin, formsemestre_id]);
const etudids_str = localStorage.getItem(etudids_key);
const noms_key = JSON.stringify(["noms", url.origin, formsemestre_id]);
const noms_str = localStorage.getItem(noms_key);
if (etudids_str && noms_str) {
const etudids = JSON.parse(etudids_str);
const noms = JSON.parse(noms_str);
const cur_idx = etudids.indexOf(etudid);
let prev_idx = -1;
let next_idx = -1
if (cur_idx != -1) {
if (cur_idx > 0) {
prev_idx = cur_idx - 1;
}
if (cur_idx < etudids.length - 1) {
next_idx = cur_idx + 1;
}
}
if (prev_idx != -1) {
let elem = document.querySelector("div.prev a");
if (elem) {
elem.href = elem.href.replace("PREV", etudids[prev_idx]);
elem.innerHTML = noms[prev_idx];
}
} else {
document.querySelector("div.prev").innerHTML = "";
}
if (next_idx != -1) {
let elem = document.querySelector("div.next a");
if (elem) {
elem.href = elem.href.replace("NEXT", etudids[next_idx]);
elem.innerHTML = noms[next_idx];
}
} else {
document.querySelector("div.next").innerHTML = "";
}
} else {
document.querySelector("div.prev").innerHTML = "";
// Supprime les liens de navigation
document.querySelector("div.prev").innerHTML = "";
document.querySelector("div.next").innerHTML = "";
}
});
// ----- Etat du formulaire jury pour éviter sortie sans enregistrer
let FORM_STATE = "";
let IS_SUBMITTING = false;
// Une chaine décrivant l'état du form
function get_form_state() {
let codes = [];
@ -85,13 +99,19 @@ function get_form_state() {
$('document').ready(function () {
FORM_STATE = get_form_state();
document.querySelector("form#jury_but").addEventListener('submit', jury_form_submit);
});
function is_modified() {
return FORM_STATE != get_form_state();
}
function jury_form_submit(event) {
IS_SUBMITTING = true;
}
window.addEventListener("beforeunload", function (e) {
if (is_modified()) {
if ((!IS_SUBMITTING) && is_modified()) {
var confirmationMessage = 'Changements non enregistrés !';
(e || window.event).returnValue = confirmationMessage;
return confirmationMessage;

View File

@ -47,26 +47,30 @@ $(function () {
}
});
}
}
}
// Les colonnes visibles sont mémorisées, il faut initialiser l'état des boutons
function update_buttons_labels(dt) {
// chaque bouton controle une classe stockée dans le data-group du span
document.querySelectorAll("button.dt-button").forEach((but) => {
let g_span = but.querySelector("span > span");
if (g_span) {
let group = g_span.dataset["group"];
if (group) {
// si le group (= la 1ere col.) est visible, but_on
if (dt.columns("." + group).visible()[0]) {
but.classList.add("but_on");
but.classList.remove("but_off");
// Changement visibilité groupes colonnes (boutons)
function toggle_col_but_visibility(e, dt, node, config) {
let group = node.children()[0].firstChild.dataset.group;
toggle_col_group_visibility(dt, group, node.hasClass("but_on"));
}
function toggle_col_ident_visibility(e, dt, node, config) {
let onoff = node.hasClass("but_on");
toggle_col_group_visibility(dt, "identite_detail", onoff);
toggle_col_group_visibility(dt, "identite_court", !onoff);
}
function toggle_col_ressources_visibility(e, dt, node, config) {
let onoff = node.hasClass("but_on");
toggle_col_group_visibility(dt, "col_res", onoff);
toggle_col_group_visibility(dt, "col_ue_bonus", onoff);
toggle_col_group_visibility(dt, "col_malus", onoff);
}
function toggle_col_group_visibility(dt, group, onoff) {
if (onoff) {
dt.columns('.' + group).visible(false);
} else {
but.classList.add("but_off");
but.classList.remove("but_on");
dt.columns('.' + group).visible(true);
}
}
update_buttons_labels(dt);
}
// Definition des boutons au dessus de la table:
let buttons = [
@ -102,11 +106,11 @@ $(function () {
action: toggle_col_ident_visibility,
},
{
text: '<span data-group="partition_aux">Groupes</span>',
text: '<span data-group="partition_aux"><a title="Affichage des groupes secondaires (la première partition est toujours affichée)">Groupes</a></span>',
action: toggle_col_but_visibility,
},
{
text: '<span data-group="partition_rangs">Rg</span>',
text: '<span data-group="partition_rangs"><a title="Rangs dans les groupes (si activés dans les partitions concernées)">Rg</a></span>',
action: toggle_col_but_visibility,
},
]; // fin des boutons communs à toutes les tables recap
@ -156,19 +160,21 @@ $(function () {
action: toggle_col_but_visibility,
});
}
: {
name: "toggle_mod",
text: "Cacher les modules",
action: function (e, dt, node, config) {
let onoff = node.hasClass("but_on");
toggle_col_group_visibility(
dt,
"col_mod:not(.col_empty)",
onoff
// S'il y a des colonnes vides:
if ($('table.table_recap td.col_empty').length > 0) {
buttons.push({ // modules vides
text: '<span data-group="col_empty">Vides</span>',
action: toggle_col_but_visibility,
});
}
// Boutons admission (pas en jury)
if (!$('table.table_recap').hasClass("jury")) {
buttons.push(
{
text: '<span data-group="admission">Admission</span>',
action: toggle_col_but_visibility,
}
);
toggle_col_group_visibility(dt, "col_ue_bonus", onoff);
toggle_col_group_visibility(dt, "col_malus", onoff);
},
}
}
// Boutons évaluations (si présentes)
@ -230,7 +236,8 @@ $(function () {
buttons: buttons,
"drawCallback": function (settings) {
// permet de conserver l'ordre de tri des colonnes
let order_info = JSON.stringify($('table.table_recap').DataTable().order());
let table = $('table.table_recap').DataTable();
let order_info = JSON.stringify(table.order());
if (formsemestre_id) {
localStorage.setItem(order_info_key, order_info);
}
@ -270,114 +277,9 @@ $(function () {
$(function () {
let row_selected = document.querySelector(".row_selected");
if (row_selected) {
/*row_selected.scrollIntoView();
window.scrollBy(0, -50);*/
row_selected.scrollIntoView();
window.scrollBy(0, -125);
row_selected.classList.add("selected");
}
// ------------- LA TABLE ---------
try {
let table = $("table.table_recap").DataTable({
paging: false,
searching: true,
info: false,
autoWidth: false,
fixedHeader: {
header: true,
footer: false,
},
orderCellsTop: true, // cellules ligne 1 pour tri
aaSorting: [], // Prevent initial sorting
colReorder: true,
stateSave: true, // enregistre état de la table (tris, ...)
columnDefs: [
{
// cache les codes, le détail de l'identité, les groupes, les colonnes admission et les vides
targets: hidden_colums,
visible: false,
},
{
// Elimine les 0 à gauche pour les exports excel et les "copy"
targets: [
"col_mod",
"col_moy_gen",
"col_moy_ue",
"col_res",
"col_sae",
"evaluation",
"col_rcue",
],
render: function (data, type, row) {
return type === "export" ? data.replace(/0(\d\..*)/, "$1") : data;
},
},
{
// Elimine les "+"" pour les exports
targets: ["col_ue_bonus", "col_malus"],
render: function (data, type, row) {
return type === "export"
? data
.replace(/.*\+(\d?\d?\.\d\d).*/m, "$1")
.replace(/0(\d\..*)/, "$1")
: data;
},
},
{
// Elimine emoji warning sur UEs
targets: ["col_ues_validables"],
render: function (data, type, row) {
return type === "export"
? data.replace(/(\d+\/\d+).*/, "$1")
: data;
},
},
],
dom: "Bfrtip",
buttons: buttons,
drawCallback: function (settings) {
// permet de conserver l'ordre de tri des colonnes
let table = $("table.table_recap").DataTable();
let order_info = JSON.stringify(table.order());
if (formsemestre_id) {
localStorage.setItem(order_info_key, order_info);
}
let etudids = [];
document.querySelectorAll("td.identite_court").forEach((e) => {
etudids.push(e.dataset.etudid);
});
let noms = [];
document.querySelectorAll("td.identite_court").forEach((e) => {
noms.push(e.dataset.nomprenom);
});
localStorage.setItem(etudids_key, JSON.stringify(etudids));
localStorage.setItem(noms_key, JSON.stringify(noms));
},
order: order_info,
});
update_buttons_labels(table);
} catch (error) {
// l'erreur peut etre causee par un ancien storage:
localStorage.removeItem(etudids_key);
localStorage.removeItem(noms_key);
localStorage.removeItem(order_info_key);
location.reload();
}
});
$("table.table_recap tbody").on("click", "tr", function () {
if ($(this).hasClass("selected")) {
$(this).removeClass("selected");
} else {
$("table.table_recap tr.selected").removeClass("selected");
$(this).addClass("selected");
}
});
// Pour montrer et surligner l'étudiant sélectionné:
$(function () {
let row_selected = document.querySelector(".row_selected");
if (row_selected) {
row_selected.scrollIntoView();
window.scrollBy(0, -125);
row_selected.classList.add("selected");
}
});
});
});

View File

@ -23,7 +23,6 @@ from app.comp.res_compat import NotesTableCompat
from app.models import ApcNiveau, UniteEns
from app.models.etudiants import Identite
from app.models.formsemestre import FormSemestre
from app.scodoc import html_sco_header
from app.scodoc.codes_cursus import (
BUT_BARRE_RCUE,
BUT_RCUE_SUFFISANT,

View File

@ -12,8 +12,7 @@ import numpy as np
from app.auth.models import User
from app.comp.res_common import ResultatsSemestre
from app.models import Identite
from app.models.ues import UniteEns
from app.models import Identite, FormSemestre, UniteEns
from app.scodoc.codes_cursus import UE_SPORT, DEF
from app.scodoc import sco_evaluation_db
from app.scodoc import sco_groups

View File

@ -49,7 +49,7 @@
<span class="formation_module_ue">(<a title="UE de rattachement">{{mod.ue.acronyme}}</a>)</span>,
{% endif %}
- parcours <b>{{ mod.get_cursus()|map(attribute="code")|join("</b>, <b>")|default('tronc commun',
- parcours <b>{{ mod.get_parcours()|map(attribute="code")|join("</b>, <b>")|default('tronc commun',
true)|safe
}}</b>
{% if mod.heures_cours or mod.heures_td or mod.heures_tp %}

View File

@ -1590,10 +1590,24 @@ def etud_desinscrit_ue(etudid, formsemestre_id, ue_id):
ue = UniteEns.query.get_or_404(ue_id)
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
if ue.formation.is_apc():
if DispenseUE.query.filter_by(etudid=etudid, ue_id=ue_id).count() == 0:
disp = DispenseUE(ue_id=ue_id, etudid=etudid)
if (
DispenseUE.query.filter_by(
formsemestre_id=formsemestre_id, etudid=etudid, ue_id=ue_id
).count()
== 0
):
disp = DispenseUE(
formsemestre_id=formsemestre_id, ue_id=ue_id, etudid=etudid
)
db.session.add(disp)
db.session.commit()
log(f"etud_desinscrit_ue {etud} {ue}")
Scolog.logdb(
method="etud_desinscrit_ue",
etudid=etud.id,
msg=f"Désinscription de l'UE {ue.acronyme} de {formsemestre.titre_annee()}",
commit=True,
)
sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre.id)
else:
sco_moduleimpl_inscriptions.do_etud_desinscrit_ue_classic(

View File

@ -11,7 +11,7 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "dbcf2175e87f"
down_revision = "d8288b7f0a3e"
down_revision = "6520faf67508"
branch_labels = None
depends_on = None

View File

@ -19,7 +19,6 @@ from app.auth.models import User
from app.models import Departement, Formation, FormationModalite, Matiere
from app.scodoc import notesdb as ndb
from app.scodoc import codes_cursus
from app.scodoc import sco_edit_formation
from app.scodoc import sco_edit_matiere
from app.scodoc import sco_edit_module
from app.scodoc import sco_edit_ue
@ -154,7 +153,7 @@ class ScoFake(object):
acronyme="test",
titre="Formation test",
titre_officiel="Le titre officiel de la formation test",
type_parcours=codes_cursus.CursusDUT.TYPE_CURSUS,
type_parcours: int = codes_cursus.CursusDUT.TYPE_CURSUS,
formation_code=None,
code_specialite=None,
) -> int:

View File

@ -123,7 +123,13 @@ def test_ue_moy(test_client):
modimpl.module.ue.type != UE_SPORT for modimpl in formsemestre.modimpls_sorted
]
etud_moy_ue = moy_ue.compute_ue_moys_apc(
sem_cube, etuds, modimpls, modimpl_inscr_df, modimpl_coefs_df, modimpl_mask
sem_cube,
etuds,
modimpls,
modimpl_inscr_df,
modimpl_coefs_df,
modimpl_mask,
set(),
)
assert etud_moy_ue[ue1.id][etudid] == n1
assert etud_moy_ue[ue2.id][etudid] == n1