forked from ScoDoc/ScoDoc
mise à jour master
This commit is contained in:
parent
cdcb4a2468
commit
98e709bfd0
@ -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()
|
||||
|
@ -650,10 +650,13 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
à poursuivre après le semestre courant.
|
||||
"""
|
||||
# La poursuite d'études dans un semestre pair d’une même année
|
||||
# est de droit pour tout étudiant:
|
||||
if (self.formsemestre.semestre_id % 2) and sco_codes.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 d’UE ;
|
||||
@ -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)
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
où 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
|
@ -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))
|
||||
|
@ -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,
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
),
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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() ]
|
||||
|
||||
|
@ -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
|
||||
|
@ -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"]
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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"]
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
"""
|
||||
)
|
||||
|
@ -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']}
|
||||
<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>
|
||||
"""
|
||||
|
@ -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"],
|
||||
|
@ -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
|
@ -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
|
||||
|
@ -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":
|
||||
|
@ -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()
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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 %}
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user