forked from ScoDoc/ScoDoc
550 lines
20 KiB
Python
550 lines
20 KiB
Python
##############################################################################
|
|
# ScoDoc
|
|
# Copyright (c) 1999 - 2024 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 mm
|
|
from reportlab.platypus import Paragraph, Spacer, Table
|
|
|
|
from app.but import cursus_but
|
|
from app.models import (
|
|
BulAppreciations,
|
|
FormSemestre,
|
|
Identite,
|
|
ScolarFormSemestreValidation,
|
|
)
|
|
|
|
from app.scodoc.sco_bulletins_standard import BulletinGeneratorStandard
|
|
from app.scodoc.sco_logos import Logo
|
|
from app.scodoc.sco_pdf import PDFLOCK, SU
|
|
from app.scodoc.sco_preferences import SemPreferences
|
|
from app.scodoc import sco_utils as scu
|
|
|
|
|
|
def make_bulletin_but_court_pdf(
|
|
args: dict,
|
|
stand_alone: bool = True,
|
|
) -> bytes:
|
|
"""génère le bulletin court BUT en pdf.
|
|
Si stand_alone, génère un doc pdf complet (une page ici),
|
|
sinon un morceau (fragment) à intégrer dans un autre document.
|
|
|
|
args donne toutes les infos du contenu du bulletin:
|
|
bul: dict = None,
|
|
cursus: cursus_but.EtudCursusBUT = None,
|
|
decision_ues: dict = None,
|
|
ects_total: float = 0.0,
|
|
etud: Identite = None,
|
|
formsemestre: FormSemestre = None,
|
|
filigranne=""
|
|
logo: Logo = None,
|
|
prefs: SemPreferences = None,
|
|
title: str = "",
|
|
ue_validation_by_niveau: dict[tuple[int, str], ScolarFormSemestreValidation] = None,
|
|
ues_acronyms: list[str] = None,
|
|
"""
|
|
# A priori ce verrou n'est plus nécessaire avec Flask (multi-process)
|
|
# mais...
|
|
try:
|
|
PDFLOCK.acquire()
|
|
bul_generator = BulletinGeneratorBUTCourt(**args)
|
|
bul_pdf = bul_generator.generate(fmt="pdf", stand_alone=stand_alone)
|
|
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)
|
|
color_gray_bg = Color(0.86, 0.86, 0.86)
|
|
|
|
def __init__(
|
|
self,
|
|
bul: dict = None,
|
|
cursus: cursus_but.EtudCursusBUT = None,
|
|
decision_ues: dict = None,
|
|
ects_total: float = 0.0,
|
|
etud: Identite = None,
|
|
filigranne="",
|
|
formsemestre: FormSemestre = None,
|
|
logo: Logo = None,
|
|
prefs: SemPreferences = None,
|
|
title: str = "",
|
|
ue_validation_by_niveau: dict[
|
|
tuple[int, str], ScolarFormSemestreValidation
|
|
] = None,
|
|
ues_acronyms: list[str] = None,
|
|
):
|
|
super().__init__(bul, authuser=current_user, filigranne=filigranne)
|
|
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.prefs = prefs
|
|
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_base = styles.ParagraphStyle("style_base")
|
|
self.style_base.fontName = "Helvetica"
|
|
self.style_base.fontSize = 9
|
|
self.style_base.firstLineIndent = 0
|
|
# écrase style defaut des bulletins
|
|
self.style_field = self.style_base
|
|
|
|
# Le nom/prénom de l'étudiant:
|
|
self.style_nom = styles.ParagraphStyle("style_nom", self.style_base)
|
|
self.style_nom.fontSize = 11
|
|
self.style_nom.fontName = "Helvetica-Bold"
|
|
|
|
self.style_cell = styles.ParagraphStyle("style_cell", self.style_base)
|
|
self.style_cell.fontSize = 7
|
|
self.style_cell.leading = 7
|
|
self.style_cell_bold = styles.ParagraphStyle("style_cell_bold", self.style_cell)
|
|
self.style_cell_bold.fontName = "Helvetica-Bold"
|
|
|
|
self.style_head = styles.ParagraphStyle("style_head", self.style_cell_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 = 0.5
|
|
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
|
|
#
|
|
self.style_jury = styles.ParagraphStyle("style_jury", self.style_base)
|
|
self.style_jury.fontSize = 9
|
|
self.style_jury.leading = self.style_jury.fontSize * 1.4 # espace les lignes
|
|
self.style_jury.backColor = self.color_gray_bg
|
|
self.style_jury.borderColor = black
|
|
self.style_jury.borderWidth = 1
|
|
self.style_jury.borderPadding = 2
|
|
self.style_jury.borderRadius = 2
|
|
|
|
self.style_appreciations = styles.ParagraphStyle(
|
|
"style_appreciations", self.style_base
|
|
)
|
|
self.style_appreciations.fontSize = 9
|
|
self.style_appreciations.leading = (
|
|
self.style_jury.fontSize * 1.4
|
|
) # espace les lignes
|
|
|
|
self.style_assiduite = self.style_cell
|
|
self.style_signature = self.style_appreciations
|
|
|
|
# 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 = 16.5 * mm
|
|
# Modules
|
|
self.width_col_code = self.width_col_ue
|
|
# Niveaux
|
|
self.width_col_niveaux_titre = 24 * mm
|
|
self.width_col_niveaux_code = 14 * mm
|
|
|
|
def bul_title_pdf(self, preference_field="bul_but_pdf_title") -> list:
|
|
"""Génère la partie "titre" du bulletin de notes.
|
|
Renvoie une liste d'objets platypus
|
|
"""
|
|
# comme les bulletins standards, mais avec notre préférence
|
|
return super().bul_title_pdf(preference_field=preference_field)
|
|
|
|
def bul_part_below(self, fmt="pdf") -> list:
|
|
"""Génère les informations placées sous la table
|
|
Dans le cas du bul. court BUT pdf, seulement les appréciations.
|
|
fmt est ignoré ici.
|
|
"""
|
|
appreciations = BulAppreciations.get_appreciations_list(
|
|
self.formsemestre.id, self.etud.id
|
|
)
|
|
return (
|
|
[
|
|
Spacer(1, 3 * mm),
|
|
self.bul_appreciations_pdf(
|
|
appreciations, style=self.style_appreciations
|
|
),
|
|
]
|
|
if appreciations
|
|
else []
|
|
)
|
|
|
|
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.boite_identite() + [Spacer(1, 3 * mm), self.boite_assiduite()],
|
|
self.table_ues(),
|
|
],
|
|
],
|
|
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(),
|
|
[Spacer(1, 8 * mm), self.boite_decisions_jury()],
|
|
]
|
|
],
|
|
colWidths=(self.width_page_avail - 84 * mm, 84 * 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],
|
|
["dont bonus"]
|
|
+ [
|
|
bul["ues"][ue]["bonus"] if bul["ues"][ue]["bonus"] != "00.00" else ""
|
|
for ue in self.ues_acronyms
|
|
],
|
|
["et malus"]
|
|
+ [
|
|
bul["ues"][ue]["malus"] if bul["ues"][ue]["malus"] != "00.00" else ""
|
|
for ue in self.ues_acronyms
|
|
],
|
|
["Rang"]
|
|
+ [
|
|
f'{bul["ues"][ue]["moyenne"]["rang"]} / {bul["ues"][ue]["moyenne"]["total"]}'
|
|
for ue in self.ues_acronyms
|
|
],
|
|
]
|
|
if self.prefs["bul_show_ects"]:
|
|
rows += [
|
|
["ECTS"]
|
|
+ [
|
|
f'{bul["ues"][ue]["ECTS"]["acquis"]:g} /{bul["ues"][ue]["ECTS"]["total"]:g}'
|
|
for ue in self.ues_acronyms
|
|
]
|
|
]
|
|
rows += [
|
|
["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_cell_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_cell_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_cell_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 boite_identite(self) -> list:
|
|
"Les informations sur l'identité et l'inscription de l'étudiant"
|
|
parcour = self.formsemestre.etuds_inscriptions[self.etud.id].parcour
|
|
|
|
return [
|
|
Paragraph(
|
|
SU(f"""{self.etud.nomprenom}"""),
|
|
style=self.style_nom,
|
|
),
|
|
Paragraph(
|
|
SU(
|
|
f"""
|
|
<b>{self.bul["demission"]}</b><br/>
|
|
Formation: {self.formsemestre.titre_num()}<br/>
|
|
{'Parcours ' + parcour.code + '<br/>' if parcour else ''}
|
|
Année universitaire: {self.formsemestre.annee_scolaire_str()}<br/>
|
|
"""
|
|
),
|
|
style=self.style_base,
|
|
),
|
|
]
|
|
|
|
def boite_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_assiduite) 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"),
|
|
],
|
|
colWidths=(25 * mm, 10 * mm),
|
|
)
|
|
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), 5),
|
|
("TOPPADDING", (0, 0), (-1, -1), 4),
|
|
("RIGHTPADDING", (0, 0), (-1, -1), 5),
|
|
("BOTTOMPADDING", (0, 0), (-1, -1), 4),
|
|
# sert de séparateur entre les lignes:
|
|
("LINEABOVE", (0, 1), (-1, -1), 3, white),
|
|
# séparateur colonne
|
|
("LINEBEFORE", (1, 1), (-1, -1), 5, white),
|
|
],
|
|
)
|
|
table.hAlign = "LEFT"
|
|
return table
|
|
|
|
def boite_decisions_jury(self):
|
|
"""La boite en bas à droite avec jury"""
|
|
txt = f"""ECTS acquis en BUT : <b>{self.ects_total:g}</b><br/>"""
|
|
|
|
if self.bul["semestre"].get("decision_annee", None):
|
|
txt += f"""
|
|
Décision saisie le {
|
|
datetime.datetime.fromisoformat(self.bul["semestre"]["decision_annee"]["date"]).strftime(scu.DATE_FMT)
|
|
}, année BUT{self.bul["semestre"]["decision_annee"]["ordre"]}
|
|
<b>{self.bul["semestre"]["decision_annee"]["code"]}</b>.
|
|
<br/>
|
|
{self.bul["diplomation"]}
|
|
"""
|
|
|
|
if self.bul["semestre"].get("autorisation_inscription", None):
|
|
txt += (
|
|
"<br/>Autorisé à s'inscrire en <b>"
|
|
+ ", ".join(
|
|
[
|
|
f"S{aut['semestre_id']}"
|
|
for aut in self.bul["semestre"]["autorisation_inscription"]
|
|
]
|
|
)
|
|
+ "</b>."
|
|
)
|
|
|
|
return Paragraph(txt, style=self.style_jury)
|