463 lines
17 KiB
Python
463 lines
17 KiB
Python
##############################################################################
|
|
# ScoDoc
|
|
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
|
# See LICENSE
|
|
##############################################################################
|
|
|
|
"""Génération bulletin BUT au format PDF standard
|
|
|
|
La génération du bulletin PDF suit le chemin suivant:
|
|
|
|
- vue formsemestre_bulletinetud -> sco_bulletins.formsemestre_bulletinetud
|
|
|
|
bul_dict = bulletin_but.BulletinBUT(formsemestre).bulletin_etud_complet(etud)
|
|
|
|
- sco_bulletins_generator.make_formsemestre_bulletin_etud()
|
|
- instance de BulletinGeneratorStandardBUT
|
|
- BulletinGeneratorStandardBUT.generate(fmt="pdf")
|
|
sco_bulletins_generator.BulletinGenerator.generate()
|
|
.generate_pdf()
|
|
.bul_table() (ci-dessous)
|
|
|
|
"""
|
|
from reportlab.lib.colors import blue
|
|
from reportlab.lib.units import cm, mm
|
|
from reportlab.platypus import Paragraph, Spacer
|
|
|
|
from app.models import ScoDocSiteConfig
|
|
from app.scodoc.sco_bulletins_standard import BulletinGeneratorStandard
|
|
from app.scodoc import gen_tables
|
|
from app.scodoc.codes_cursus import UE_SPORT
|
|
from app.scodoc import sco_utils as scu
|
|
|
|
|
|
class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
|
|
"""Génération du bulletin de BUT au format PDF.
|
|
|
|
self.infos est le dict issu de BulletinBUT.bulletin_etud_complet()
|
|
"""
|
|
|
|
# spécialisation du BulletinGeneratorStandard, ne pas présenter à l'utilisateur:
|
|
list_in_menu = False
|
|
scale_table_in_page = False # pas de mise à l'échelle pleine page auto
|
|
multi_pages = True # plusieurs pages par bulletins
|
|
small_fontsize = "8"
|
|
|
|
def bul_table(self, fmt="html"):
|
|
"""Génère la table centrale du bulletin de notes
|
|
Renvoie:
|
|
- en HTML: une chaine
|
|
- en PDF: une liste d'objets PLATYPUS (eg instance de Table).
|
|
"""
|
|
if fmt == "pdf" and ScoDocSiteConfig.is_bul_pdf_disabled():
|
|
return [Paragraph("<p>Export des PDF interdit par l'administrateur</p>")]
|
|
tables_infos = [
|
|
# ---- TABLE SYNTHESE UES
|
|
self.but_table_synthese_ues(),
|
|
]
|
|
if self.version != "short":
|
|
tables_infos += [
|
|
# ---- TABLE RESSOURCES
|
|
self.but_table_ressources(),
|
|
# ---- TABLE SAE
|
|
self.but_table_saes(),
|
|
]
|
|
objects = []
|
|
for i, (col_keys, rows, pdf_style, col_widths) in enumerate(tables_infos):
|
|
table = gen_tables.GenTable(
|
|
rows=rows,
|
|
columns_ids=col_keys,
|
|
pdf_table_style=pdf_style,
|
|
pdf_col_widths=[col_widths[k] for k in col_keys],
|
|
preferences=self.preferences,
|
|
html_class="notes_bulletin",
|
|
html_class_ignore_default=True,
|
|
html_with_td_classes=True,
|
|
)
|
|
table_objects = table.gen(fmt=fmt)
|
|
objects += table_objects
|
|
# objects += [KeepInFrame(0, 0, table_objects, mode="shrink")]
|
|
if i != 2:
|
|
objects.append(Spacer(1, 6 * mm))
|
|
|
|
return objects
|
|
|
|
def but_table_synthese_ues(
|
|
self, title_bg=(182, 235, 255), title_ue_cap_bg=(150, 207, 147)
|
|
):
|
|
"""La table de synthèse; pour chaque UE, liste des ressources et SAÉs avec leurs notes
|
|
et leurs coefs.
|
|
Renvoie: colkeys, P, pdf_style, colWidths
|
|
- colkeys: nom des colonnes de la table (clés)
|
|
- P : table (liste de dicts de chaines de caracteres)
|
|
- pdf_style : commandes table Platypus
|
|
- largeurs de colonnes pour PDF
|
|
"""
|
|
# nb: self.infos a ici été donné par BulletinBUT.bulletin_etud_complet()
|
|
col_widths = {
|
|
"titre": None,
|
|
"min": 1.5 * cm,
|
|
"moy": 1.5 * cm,
|
|
"max": 1.5 * cm,
|
|
"moyenne": 2 * cm,
|
|
"coef": 2 * cm,
|
|
}
|
|
with_col_minmax = self.preferences["bul_show_minmax"]
|
|
with_col_moypromo = self.preferences["bul_show_moypromo"]
|
|
# Noms des colonnes à afficher:
|
|
col_keys = ["titre"]
|
|
if with_col_minmax:
|
|
col_keys += ["min"]
|
|
if with_col_moypromo:
|
|
col_keys += ["moy"]
|
|
if with_col_minmax:
|
|
col_keys += ["max"]
|
|
col_keys += ["coef", "moyenne"]
|
|
# Couleur fond:
|
|
title_bg = tuple(x / 255.0 for x in title_bg)
|
|
title_ue_cap_bg = tuple(x / 255.0 for x in title_ue_cap_bg)
|
|
# elems pour générer table avec gen_table (liste de dicts)
|
|
rows = [
|
|
# Ligne de titres
|
|
{
|
|
"titre": "Unités d'enseignement",
|
|
"min": "Promotion",
|
|
"moy": "",
|
|
"max": "",
|
|
"_min_colspan": 2,
|
|
"moyenne": Paragraph("<para align=right><b>Note/20</b></para>"),
|
|
"coef": "Coef.",
|
|
"_coef_pdf": Paragraph("<para align=right><b><i>Coef.</i></b></para>"),
|
|
"_css_row_class": "note_bold",
|
|
"_pdf_row_markup": ["b"],
|
|
"_pdf_style": [
|
|
("BACKGROUND", (0, 0), (-1, 0), title_bg),
|
|
],
|
|
},
|
|
]
|
|
if with_col_moypromo and not with_col_minmax:
|
|
rows[-1]["moy"] = "Promotion"
|
|
# 2eme ligne titres si nécessaire
|
|
if with_col_minmax: # TODO or with_col_abs:
|
|
rows.append(
|
|
{
|
|
"min": "min.",
|
|
"moy": "moy.",
|
|
"max": "max.",
|
|
# "abs": "(Tot. / J.)",
|
|
"_css_row_class": "note_bold",
|
|
"_pdf_row_markup": ["b"],
|
|
"_pdf_style": [
|
|
("BACKGROUND", (0, 0), (-1, 0), title_bg),
|
|
],
|
|
}
|
|
)
|
|
rows[-1]["_pdf_style"] += [
|
|
("BOTTOMPADDING", (0, 0), (-1, 0), 7),
|
|
(
|
|
"LINEBELOW",
|
|
(0, 0),
|
|
(-1, 0),
|
|
self.PDF_LINEWIDTH,
|
|
blue,
|
|
),
|
|
]
|
|
ues = self.infos["ues"]
|
|
ues_capitalisees = self.infos.get("ues_capitalisees", {})
|
|
ues_tup = sorted(
|
|
list(ues.items()) + list(ues_capitalisees.items()),
|
|
key=lambda x: x[1]["numero"],
|
|
)
|
|
for ue_acronym, ue in ues_tup:
|
|
is_capitalized = "date_capitalisation" in ue
|
|
self._ue_rows(
|
|
rows, ue_acronym, ue, title_ue_cap_bg if is_capitalized else title_bg
|
|
)
|
|
|
|
# Global pdf style commands:
|
|
pdf_style = [
|
|
("VALIGN", (0, 0), (-1, -1), "TOP"),
|
|
("BOX", (0, 0), (-1, -1), 0.4, blue), # ajoute cadre extérieur bleu:
|
|
]
|
|
return col_keys, rows, pdf_style, col_widths
|
|
|
|
def _ue_rows(self, rows: list, ue_acronym: str, ue: dict, title_bg: tuple):
|
|
"Décrit une UE dans la table synthèse: titre, sous-titre et liste modules"
|
|
if (ue["type"] == UE_SPORT) and len(ue.get("modules", [])) == 0:
|
|
# ne mentionne l'UE que s'il y a des modules
|
|
return
|
|
# 1er ligne titre UE
|
|
moy_ue = ue.get("moyenne", "-")
|
|
if isinstance(moy_ue, dict):
|
|
moy_ue = moy_ue.get("value", "-") if moy_ue is not None else "-"
|
|
t = {
|
|
"titre": f"{ue_acronym} - {ue['titre']}",
|
|
"moyenne": Paragraph(
|
|
f"""<para align=right><b>{moy_ue or "-"}</b></para>"""
|
|
),
|
|
"_css_row_class": "note_bold",
|
|
"_pdf_row_markup": ["b"],
|
|
"_pdf_style": [
|
|
(
|
|
"LINEABOVE",
|
|
(0, 0),
|
|
(-1, 0),
|
|
self.PDF_LINEWIDTH,
|
|
self.PDF_LINECOLOR,
|
|
),
|
|
("BACKGROUND", (0, 0), (-1, 0), title_bg),
|
|
("BOTTOMPADDING", (0, 0), (-1, 0), 7),
|
|
],
|
|
}
|
|
rows.append(t)
|
|
if ue["type"] == UE_SPORT:
|
|
self.ue_sport_rows(rows, ue, title_bg)
|
|
else:
|
|
self.ue_std_rows(rows, ue, title_bg)
|
|
|
|
@staticmethod
|
|
def affichage_bonus_malus(ue: dict) -> list[str]:
|
|
"liste de chaînes affichant les bonus et malus"
|
|
fields_bmr = []
|
|
# lecture des bonus sport culture et malus (ou bonus autre) (0 si valeur non numérique)
|
|
try:
|
|
bonus_sc = float(ue.get("bonus", 0.0)) or 0
|
|
except ValueError:
|
|
bonus_sc = 0
|
|
try:
|
|
malus = float(ue.get("malus", 0.0)) or 0
|
|
except ValueError:
|
|
malus = 0
|
|
# Calcul de l affichage
|
|
if malus < 0:
|
|
if bonus_sc > 0:
|
|
fields_bmr.append(f"Bonus sport/culture: {bonus_sc}")
|
|
fields_bmr.append(f"Bonus autres: {-malus}")
|
|
else:
|
|
fields_bmr.append(f"Bonus: {-malus}")
|
|
elif malus > 0:
|
|
if bonus_sc > 0:
|
|
fields_bmr.append(f"Bonus: {bonus_sc}")
|
|
fields_bmr.append(f"Malus: {malus}")
|
|
else:
|
|
if bonus_sc > 0:
|
|
fields_bmr.append(f"Bonus: {bonus_sc}")
|
|
return fields_bmr
|
|
|
|
def ue_std_rows(self, rows: list, ue: dict, title_bg: tuple):
|
|
"Lignes décrivant une UE standard dans la table de synthèse"
|
|
# 2eme ligne titre UE (bonus/malus/ects)
|
|
if "ECTS" in ue:
|
|
ects_txt = f'ECTS: {ue["ECTS"]["acquis"]:.3g} / {ue["ECTS"]["total"]:.3g}'
|
|
else:
|
|
ects_txt = ""
|
|
# case Bonus/Malus/Rang "bmr"
|
|
fields_bmr = BulletinGeneratorStandardBUT.affichage_bonus_malus(ue)
|
|
moy_ue = ue.get("moyenne", "-")
|
|
if isinstance(moy_ue, dict): # UE non capitalisées
|
|
if self.preferences["bul_show_ue_rangs"]:
|
|
fields_bmr.append(
|
|
f"Rang: {ue['moyenne']['rang']} / {ue['moyenne']['total']}"
|
|
)
|
|
ue_min, ue_max, ue_moy = (
|
|
ue["moyenne"]["min"],
|
|
ue["moyenne"]["max"],
|
|
ue["moyenne"]["moy"],
|
|
)
|
|
else: # UE capitalisée
|
|
ue_min, ue_max, ue_moy = "", "", moy_ue
|
|
date_capitalisation = ue.get("date_capitalisation")
|
|
if date_capitalisation:
|
|
fields_bmr.append(
|
|
f"""Capitalisée le {date_capitalisation.strftime("%d/%m/%Y")}"""
|
|
)
|
|
t = {
|
|
"titre": " - ".join(fields_bmr),
|
|
"coef": ects_txt,
|
|
"_coef_pdf": Paragraph(f"""<para align=right>{ects_txt}</para>"""),
|
|
"_coef_colspan": 2,
|
|
"_pdf_style": [
|
|
("BACKGROUND", (0, 0), (-1, 0), title_bg),
|
|
("LINEBELOW", (0, 0), (-1, 0), self.PDF_LINEWIDTH, self.PDF_LINECOLOR),
|
|
# ligne au dessus du bonus/malus, gris clair
|
|
("LINEABOVE", (0, 0), (-1, 0), self.PDF_LINEWIDTH, (0.7, 0.7, 0.7)),
|
|
],
|
|
"min": ue_min,
|
|
"max": ue_max,
|
|
"moy": ue_moy,
|
|
}
|
|
rows.append(t)
|
|
|
|
# Liste chaque ressource puis chaque SAE
|
|
for mod_type in ("ressources", "saes"):
|
|
for mod_code, mod in ue[mod_type].items():
|
|
t = {
|
|
"titre": f"{mod_code} {self.infos[mod_type][mod_code]['titre']}",
|
|
"moyenne": Paragraph(f'<para align=right>{mod["moyenne"]}</para>'),
|
|
"coef": mod["coef"],
|
|
"_coef_pdf": Paragraph(
|
|
f"<para align=right><i>{mod['coef']}</i></para>"
|
|
),
|
|
"_pdf_style": [
|
|
(
|
|
"LINEBELOW",
|
|
(0, 0),
|
|
(-1, 0),
|
|
self.PDF_LINEWIDTH,
|
|
(0.7, 0.7, 0.7), # gris clair
|
|
)
|
|
],
|
|
}
|
|
rows.append(t)
|
|
|
|
def ue_sport_rows(self, rows: list, ue: dict, title_bg: tuple):
|
|
"Lignes décrivant l'UE bonus dans la table de synthèse"
|
|
# UE BONUS
|
|
for mod_code, mod in ue["modules"].items():
|
|
rows.append(
|
|
{
|
|
"titre": f"{mod_code or ''} {mod['titre'] or ''}",
|
|
}
|
|
)
|
|
self.evaluations_rows(rows, mod["evaluations"])
|
|
|
|
def but_table_ressources(self):
|
|
"""La table de synthèse; pour chaque ressources, note et liste d'évaluations
|
|
Renvoie: colkeys, P, pdf_style, colWidths
|
|
"""
|
|
return self.bul_table_modules(
|
|
mod_type="ressources", title="Ressources", title_bg=(248, 200, 68)
|
|
)
|
|
|
|
def but_table_saes(self):
|
|
"table des SAEs"
|
|
return self.bul_table_modules(
|
|
mod_type="saes",
|
|
title="Situations d'apprentissage et d'évaluation",
|
|
title_bg=(198, 255, 171),
|
|
)
|
|
|
|
def bul_table_modules(self, mod_type=None, title="", title_bg=(248, 200, 68)):
|
|
"""Table ressources ou SAEs
|
|
- colkeys: nom des colonnes de la table (clés)
|
|
- P : table (liste de dicts de chaines de caracteres)
|
|
- pdf_style : commandes table Platypus
|
|
- largeurs de colonnes pour PDF
|
|
"""
|
|
# UE à utiliser pour les poids (# colonne/UE)
|
|
ue_infos = self.infos["ues"]
|
|
ue_acros = list(
|
|
[k for k in ue_infos if ue_infos[k]["type"] != UE_SPORT]
|
|
) # ['RT1.1', 'RT2.1', 'RT3.1']
|
|
# Colonnes à afficher:
|
|
col_keys = ["titre"] + ue_acros + ["coef", "moyenne"]
|
|
# Largeurs des colonnes:
|
|
col_widths = {
|
|
"titre": None,
|
|
# "poids": None,
|
|
"moyenne": 2 * cm,
|
|
"coef": 2 * cm,
|
|
}
|
|
for ue_acro in ue_acros:
|
|
col_widths[ue_acro] = 12 * mm # largeur col. poids
|
|
|
|
title_bg = tuple(x / 255.0 for x in title_bg)
|
|
# elems pour générer table avec gen_table (liste de dicts)
|
|
# Ligne de titres
|
|
t = {
|
|
"titre": title,
|
|
# "_titre_colspan": 1 + len(ue_acros),
|
|
"moyenne": "Note/20",
|
|
"coef": "Coef.",
|
|
"_coef_pdf": Paragraph("<para align=right><i>Coef.</i></para>"),
|
|
"_css_row_class": "note_bold",
|
|
"_pdf_row_markup": ["b"],
|
|
"_pdf_style": [
|
|
("BACKGROUND", (0, 0), (-1, 0), title_bg),
|
|
("BOTTOMPADDING", (0, 0), (-1, 0), 7),
|
|
(
|
|
"LINEBELOW",
|
|
(0, 0),
|
|
(-1, 0),
|
|
self.PDF_LINEWIDTH,
|
|
blue,
|
|
),
|
|
],
|
|
}
|
|
for ue_acro in ue_acros:
|
|
t[ue_acro] = Paragraph(
|
|
f"<para align=right fontSize={self.small_fontsize}><i>{ue_acro}</i></para>"
|
|
)
|
|
rows = [t]
|
|
for mod_code, mod in self.infos[mod_type].items():
|
|
# 1er ligne titre module
|
|
t = {
|
|
"titre": f"{mod_code} - {mod['titre']}",
|
|
"_titre_colspan": 2 + len(ue_acros),
|
|
"_css_row_class": "note_bold",
|
|
"_pdf_row_markup": ["b"],
|
|
"_pdf_style": [
|
|
(
|
|
"LINEABOVE",
|
|
(0, 0),
|
|
(-1, 0),
|
|
self.PDF_LINEWIDTH,
|
|
self.PDF_LINECOLOR,
|
|
),
|
|
("BACKGROUND", (0, 0), (-1, 0), title_bg),
|
|
("BOTTOMPADDING", (0, 0), (-1, 0), 7),
|
|
],
|
|
}
|
|
rows.append(t)
|
|
# Evaluations:
|
|
self.evaluations_rows(rows, mod["evaluations"], ue_acros)
|
|
|
|
# Global pdf style commands:
|
|
pdf_style = [
|
|
("VALIGN", (0, 0), (-1, -1), "TOP"),
|
|
("BOX", (0, 0), (-1, -1), 0.4, blue), # ajoute cadre extérieur bleu:
|
|
]
|
|
return col_keys, rows, pdf_style, col_widths
|
|
|
|
def evaluations_rows(self, rows, evaluations: list[dict], ue_acros=()):
|
|
"lignes des évaluations"
|
|
for e in evaluations:
|
|
coef = e["coef"] if e["evaluation_type"] == scu.EVALUATION_NORMALE else "*"
|
|
t = {
|
|
"titre": f"{e['description'] or ''}",
|
|
"moyenne": e["note"]["value"],
|
|
"_moyenne_pdf": Paragraph(
|
|
f"""<para align=right>{e["note"]["value"]}</para>"""
|
|
),
|
|
"coef": coef,
|
|
"_coef_pdf": Paragraph(
|
|
f"<para align=right fontSize={self.small_fontsize}><i>{coef}</i></para>"
|
|
),
|
|
"_pdf_style": [
|
|
(
|
|
"LINEBELOW",
|
|
(0, 0),
|
|
(-1, 0),
|
|
self.PDF_LINEWIDTH,
|
|
(0.7, 0.7, 0.7), # gris clair
|
|
)
|
|
],
|
|
}
|
|
col_idx = 1 # 1ere col. poids
|
|
for ue_acro in ue_acros:
|
|
t[ue_acro] = Paragraph(
|
|
f"""<para align=right fontSize={self.small_fontsize}><i>{
|
|
e["poids"].get(ue_acro, "") or ""}</i></para>"""
|
|
)
|
|
t["_pdf_style"].append(
|
|
(
|
|
"BOX",
|
|
(col_idx, 0),
|
|
(col_idx, 0),
|
|
self.PDF_LINEWIDTH,
|
|
(0.7, 0.7, 0.7), # gris clair
|
|
),
|
|
)
|
|
col_idx += 1
|
|
rows.append(t)
|