ScoDoc/app/but/bulletin_but_court_pdf.py

424 lines
15 KiB
Python

##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
"""Génération bulletin BUT PDF synthétique en une page
On génère du PDF avec reportLab en utilisant les classes
ScoDoc BulletinGenerator et GenTable.
"""
import datetime
from flask_login import current_user
from reportlab.lib import styles
from reportlab.lib.colors import black, white, Color
from reportlab.lib.enums import TA_CENTER
from reportlab.lib.units import cm, mm
from reportlab.platypus import Paragraph, Spacer, Table
from app.but import cursus_but
from app.models import FormSemestre, Identite, ScolarFormSemestreValidation
from app.scodoc.sco_bulletins_standard import BulletinGeneratorStandard
from app.scodoc import sco_bulletins
from app.scodoc.sco_logos import Logo
from app.scodoc import sco_pdf, sco_preferences
from app.scodoc.sco_pdf import PDFLOCK, SU
def make_bulletin_but_court_pdf(
bul: dict = None,
cursus: cursus_but.EtudCursusBUT = None,
decision_ues: dict = None,
ects_total: float = 0.0,
etud: Identite = None,
formsemestre: FormSemestre = None,
logo: Logo = None,
title: str = "",
ue_validation_by_niveau: dict[tuple[int, str], ScolarFormSemestreValidation] = None,
ues_acronyms: list[str] = None,
) -> bytes:
# A priori ce verrou n'est plus nécessaire avec Flask (multi-process)
# mais...
try:
PDFLOCK.acquire()
bul_generator = BulletinGeneratorBUTCourt(**locals())
bul_pdf = bul_generator.generate(format="pdf")
finally:
PDFLOCK.release()
return bul_pdf
class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
"""Ce générateur de bulletin BUT court est assez différent des autres bulletins.
Ne génére que du PDF.
Il reprend la mise en page et certains éléments (pied de page, signature).
"""
# spécialisation du BulletinGeneratorStandard, ne pas présenter à l'utilisateur:
list_in_menu = False
scale_table_in_page = True # pas de mise à l'échelle pleine page auto
multi_pages = False # une page par bulletin
small_fontsize = "8"
color_blue_bg = Color(0, 153 / 255, 204 / 255)
def __init__(
self,
bul: dict = None,
cursus: cursus_but.EtudCursusBUT = None,
decision_ues: dict = None,
ects_total: float = 0.0,
etud: Identite = None,
formsemestre: FormSemestre = None,
logo: Logo = None,
title: str = "",
ue_validation_by_niveau: dict[
tuple[int, str], ScolarFormSemestreValidation
] = None,
ues_acronyms: list[str] = None,
):
super().__init__(bul, authuser=current_user)
self.bul = bul
self.cursus = cursus
self.decision_ues = decision_ues
self.ects_total = ects_total
self.etud = etud
self.formsemestre = formsemestre
self.logo = logo
self.title = title
self.ue_validation_by_niveau = ue_validation_by_niveau
self.ues_acronyms = ues_acronyms # sans UEs sport
self.nb_ues = len(self.ues_acronyms)
# Styles PDF
self.style_cell = styles.ParagraphStyle("style_cell")
self.style_cell.fontName = "Helvetica"
self.style_cell.fontSize = 7
self.style_cell.leading = 7
self.style_bold = styles.ParagraphStyle("style_bold", self.style_cell)
self.style_bold.fontName = "Helvetica-Bold"
self.style_head = styles.ParagraphStyle("style_head", self.style_bold)
self.style_head.fontSize = 9
self.style_niveaux = styles.ParagraphStyle("style_niveaux", self.style_cell)
self.style_niveaux.alignment = TA_CENTER
self.style_niveaux.leading = 9
self.style_niveaux.firstLineIndent = 0
self.style_niveaux.leftIndent = 1
self.style_niveaux.rightIndent = 1
self.style_niveaux.borderWidth = 1
self.style_niveaux.borderPadding = 2
self.style_niveaux.borderRadius = 2
self.style_niveaux_top = styles.ParagraphStyle(
"style_niveaux_top", self.style_niveaux
)
self.style_niveaux_top.fontName = "Helvetica-Bold"
self.style_niveaux_top.fontSize = 8
self.style_niveaux_titre = styles.ParagraphStyle(
"style_niveaux_titre", self.style_niveaux
)
self.style_niveaux_titre.textColor = white
self.style_niveaux_titre.backColor = self.color_blue_bg
self.style_niveaux_titre.borderColor = self.color_blue_bg
self.style_niveaux_code = styles.ParagraphStyle(
"style_niveaux_code", self.style_niveaux
)
self.style_niveaux_code.borderColor = black
# Géométrie page
self.width_page_avail = 185 * mm # largeur utilisable
# Géométrie tableaux
self.width_col_ue = 18 * mm
self.width_col_ue_titres = 15 * mm
# Modules
self.width_col_code = self.width_col_ue
# Niveaux
self.width_col_niveaux_titre = 24 * mm
self.width_col_niveaux_code = 12 * mm
def bul_table(self, fmt=None) -> list:
"""Génère la table centrale du bulletin de notes
Renvoie: une liste d'objets PLATYPUS (eg instance de Table).
L'argument fmt est ici ignoré (toujours en PDF)
"""
style_table_2cols = [
("ALIGN", (0, -1), (0, -1), "LEFT"),
("ALIGN", (-1, -1), (-1, -1), "RIGHT"),
("VALIGN", (0, 0), (-1, 1), "TOP"),
("LEFTPADDING", (0, 0), (-1, -1), 0),
("TOPPADDING", (0, 0), (-1, -1), 0),
("RIGHTPADDING", (0, 0), (-1, -1), 0),
("BOTTOMPADDING", (0, 0), (-1, -1), 0),
]
# Ligne avec boite assiduité et table UEs
table_abs_ues = Table(
[[self.box_assiduite(), self.table_ues()]],
colWidths=(3 * cm, self.width_page_avail - 3 * cm),
style=style_table_2cols,
)
table_abs_ues.hAlign = "RIGHT"
# Ligne (en bas) avec table cursus et boite jury
table_cursus_jury = Table(
[[self.table_cursus_but(), self.boite_decisions_jury()]],
colWidths=(self.width_page_avail - 45 * mm, 45 * mm),
style=style_table_2cols,
)
return [
table_abs_ues,
Spacer(0, 3 * mm),
self.table_ressources(),
Spacer(0, 3 * mm),
self.table_saes(),
Spacer(0, 5 * mm),
table_cursus_jury,
]
def table_ues(self) -> Table:
"""Table avec les résultats d'UE du semestre courant"""
bul = self.bul
rows = [
[
f"Unités d'enseignement du semestre {self.formsemestre.semestre_id}",
],
[""] + self.ues_acronyms,
["Moyenne"]
+ [bul["ues"][ue]["moyenne"]["value"] for ue in self.ues_acronyms],
["Bonus"]
+ [
bul["ues"][ue]["bonus"] if bul["ues"][ue]["bonus"] != "00.00" else ""
for ue in self.ues_acronyms
],
["Malus"]
+ [
bul["ues"][ue]["malus"] if bul["ues"][ue]["malus"] != "00.00" else ""
for ue in self.ues_acronyms
],
["Rang"] + [bul["ues"][ue]["moyenne"]["rang"] for ue in self.ues_acronyms],
["Effectif"]
+ [bul["ues"][ue]["moyenne"]["total"] for ue in self.ues_acronyms],
["ECTS"]
+ [
f'{self.decision_ues[ue]["ects"]:g}' if ue in self.decision_ues else ""
for ue in self.ues_acronyms
],
["Jury"]
+ [
self.decision_ues[ue]["code"] if ue in self.decision_ues else ""
for ue in self.ues_acronyms
],
]
blue_bg = Color(183 / 255.0, 235 / 255.0, 255 / 255.0)
table_style = [
("VALIGN", (0, 0), (-1, -1), "TOP"),
("BOX", (0, 0), (-1, -1), 1.0, black), # ajoute cadre extérieur
("INNERGRID", (0, 0), (-1, -1), 0.25, black),
("LEADING", (0, 1), (-1, -1), 5),
("SPAN", (0, 0), (self.nb_ues, 0)),
("BACKGROUND", (0, 0), (self.nb_ues, 0), blue_bg),
]
col_widths = [self.width_col_ue_titres] + [self.width_col_ue] * self.nb_ues
rows_styled = [[Paragraph(SU(str(cell)), self.style_head) for cell in rows[0]]]
rows_styled += [[Paragraph(SU(str(cell)), self.style_bold) for cell in rows[1]]]
rows_styled += [
[Paragraph(SU(str(cell)), self.style_cell) for cell in row]
for row in rows[2:-1]
]
rows_styled += [
[Paragraph(SU(str(cell)), self.style_bold) for cell in rows[-1]]
]
table = Table(
rows_styled,
colWidths=col_widths,
style=table_style,
)
table.hAlign = "RIGHT"
return table
def _table_modules(self, mod_type: str = "ressources", title: str = "") -> Table:
"génère table des modules: resources ou SAEs"
bul = self.bul
rows = [
["", "", "Unités d'enseignement"] + [""] * (self.nb_ues - 1),
[title, ""] + self.ues_acronyms,
]
for mod in self.bul[mod_type]:
row = [mod, bul[mod_type][mod]["titre"]]
row += [
bul["ues"][ue][mod_type][mod]["moyenne"]
if mod in bul["ues"][ue][mod_type]
else ""
for ue in self.ues_acronyms
]
rows.append(row)
title_bg = (
Color(255 / 255, 192 / 255, 0)
if mod_type == "ressources"
else Color(176 / 255, 255 / 255, 99 / 255)
)
table_style = [
("VALIGN", (0, 0), (-1, -1), "TOP"),
("BOX", (0, 0), (-1, -1), 1.0, black), # ajoute cadre extérieur
("INNERGRID", (0, 0), (-1, -1), 0.25, black),
("LEADING", (0, 1), (-1, -1), 5),
# 1ère ligne titre
("SPAN", (0, 0), (1, 0)),
("SPAN", (2, 0), (self.nb_ues, 0)),
# 2ème ligne titre
("SPAN", (0, 1), (1, 1)),
("BACKGROUND", (0, 1), (1, 1), title_bg),
]
# Estime l'espace horizontal restant pour les titres de modules
width_col_titre_module = (
self.width_page_avail
- self.width_col_code
- self.width_col_ue * self.nb_ues
)
col_widths = [self.width_col_code, width_col_titre_module] + [
self.width_col_ue
] * self.nb_ues
rows_styled = [
[Paragraph(SU(str(cell)), self.style_bold) for cell in row]
for row in rows[:2]
]
rows_styled += [
[Paragraph(SU(str(cell)), self.style_cell) for cell in row]
for row in rows[2:]
]
table = Table(
rows_styled,
colWidths=col_widths,
style=table_style,
)
table.hAlign = "RIGHT"
return table
def table_ressources(self) -> Table:
"La table des ressources"
return self._table_modules("ressources", "Ressources")
def table_saes(self) -> Table:
"La table des SAEs"
return self._table_modules(
"saes", "Situations d'Apprentissage et d'Évaluation (SAÉ)"
)
def box_assiduite(self) -> Table:
"Les informations sur l'assiduité"
if not self.bul["options"]["show_abs"]:
return Paragraph("") # empty
color_bg = Color(245 / 255, 237 / 255, 200 / 255)
rows = [
["Absences", ""],
[f'({self.bul["semestre"]["absences"]["metrique"]})', ""],
["Non justifiées", self.bul["semestre"]["absences"]["injustifie"]],
["Total", self.bul["semestre"]["absences"]["total"]],
]
rows_styled = [
[Paragraph(SU(str(cell)), self.style_head) for cell in row]
for row in rows[:1]
]
rows_styled += [
[Paragraph(SU(str(cell)), self.style_cell) for cell in row]
for row in rows[1:]
]
table = Table(
rows_styled,
# [topLeft, topRight, bottomLeft bottomRight]
cornerRadii=[2 * mm] * 4,
style=[
("BACKGROUND", (0, 0), (-1, -1), color_bg),
("SPAN", (0, 0), (1, 0)),
("SPAN", (0, 1), (1, 1)),
("VALIGN", (0, 0), (-1, -1), "TOP"),
],
)
table.hAlign = "LEFT"
return table
def table_cursus_but(self) -> Table:
"La table avec niveaux et validations BUT1, BUT2, BUT3"
rows = [
["", "BUT 1", "BUT 2", "BUT 3"],
]
for competence_id in self.cursus.to_dict():
row = [self.cursus.competences[competence_id].titre]
for annee in ("BUT1", "BUT2", "BUT3"):
validation = self.cursus.validation_par_competence_et_annee.get(
competence_id, {}
).get(annee)
has_niveau = self.cursus.competence_annee_has_niveau(
competence_id, annee
)
txt = ""
if validation:
txt = validation.code
elif has_niveau:
txt = "-"
row.append(txt)
rows.append(row)
rows_styled = [
[Paragraph(SU(str(cell)), self.style_niveaux_top) for cell in rows[0]]
] + [
[Paragraph(SU(str(row[0])), self.style_niveaux_titre)]
+ [
Paragraph(SU(str(cell)), self.style_niveaux_code) if cell else ""
for cell in row[1:]
]
for row in rows[1:]
]
table = Table(
rows_styled,
colWidths=[
self.width_col_niveaux_titre,
self.width_col_niveaux_code,
self.width_col_niveaux_code,
self.width_col_niveaux_code,
],
style=[
("ALIGN", (0, 0), (-1, -1), "CENTER"),
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
("LEFTPADDING", (0, 0), (-1, -1), 2),
("TOPPADDING", (0, 0), (-1, -1), 4),
("RIGHTPADDING", (0, 0), (-1, -1), 2),
("BOTTOMPADDING", (0, 0), (-1, -1), 4),
# sert de séparateur entre les lignes:
("LINEABOVE", (0, 1), (-1, -1), 3, white),
],
)
table.hAlign = "LEFT"
return table
def boite_decisions_jury(self):
"""La boite en bas à droite avec jury"""
txt = f"""ECTS acquis : {self.ects_total}<br/>"""
if self.bul["semestre"]["decision_annee"]:
txt += f"""
Jury tenu le {
datetime.datetime.fromisoformat(self.bul["semestre"]["decision_annee"]["date"]).strftime("%d/%m/%Y à %H:%M")
}, année BUT {self.bul["semestre"]["decision_annee"]["code"]}.
<br/>
"""
if self.bul["semestre"]["autorisation_inscription"]:
txt += (
"Autorisé à s'inscrire en "
+ ", ".join(
[
f"S{aut['semestre_id']}"
for aut in self.bul["semestre"]["autorisation_inscription"]
]
)
+ "."
)
return Paragraph(txt)