388 lines
14 KiB
Python
388 lines
14 KiB
Python
# -*- mode: python -*-
|
|
# -*- coding: utf-8 -*-
|
|
|
|
##############################################################################
|
|
#
|
|
# Gestion scolarite IUT
|
|
#
|
|
# Copyright (c) 1999 - 2024 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@gmail.com
|
|
#
|
|
##############################################################################
|
|
|
|
"""Génération des bulletins de note: super-classe pour les générateurs (HTML et PDF)
|
|
|
|
class BulletinGenerator:
|
|
description
|
|
supported_formats = [ 'pdf', 'html' ]
|
|
.bul_title_pdf()
|
|
.bul_table(fmt)
|
|
.bul_part_below(fmt)
|
|
.bul_signatures_pdf()
|
|
|
|
.__init__ et .generate(fmt) methodes appelees par le client (sco_bulletin)
|
|
|
|
La préférence 'bul_class_name' donne le nom de la classe generateur.
|
|
La préférence 'bul_pdf_class_name' est obsolete (inutilisée).
|
|
|
|
|
|
"""
|
|
import collections
|
|
import io
|
|
import time
|
|
import traceback
|
|
|
|
|
|
import reportlab
|
|
from reportlab.platypus import (
|
|
DocIf,
|
|
Paragraph,
|
|
PageBreak,
|
|
)
|
|
from reportlab.platypus import Table, KeepInFrame
|
|
|
|
from flask import request
|
|
from flask_login import current_user
|
|
|
|
from app.models import FormSemestre, Identite, ScoDocSiteConfig
|
|
from app.scodoc import sco_utils as scu
|
|
from app.scodoc.sco_exceptions import NoteProcessError
|
|
from app import log
|
|
from app.scodoc import sco_formsemestre
|
|
from app.scodoc import sco_pdf
|
|
from app.scodoc.sco_pdf import PDFLOCK
|
|
import sco_version
|
|
|
|
|
|
class BulletinGenerator:
|
|
"Virtual superclass for PDF bulletin generators" ""
|
|
# Here some helper methods
|
|
# see sco_bulletins_standard.BulletinGeneratorStandard subclass for real methods
|
|
supported_formats = [] # should list supported formats, eg [ 'html', 'pdf' ]
|
|
description = "superclass for bulletins" # description for user interface
|
|
list_in_menu = True # la classe doit-elle est montrée dans le menu de config ?
|
|
scale_table_in_page = True # rescale la table sur 1 page
|
|
multi_pages = False
|
|
|
|
def __init__(
|
|
self,
|
|
bul_dict,
|
|
authuser=None,
|
|
etud: Identite = None,
|
|
filigranne=None,
|
|
formsemestre: FormSemestre = None,
|
|
server_name=None,
|
|
version="long",
|
|
with_img_signatures_pdf: bool = True,
|
|
):
|
|
from app.scodoc import sco_preferences
|
|
|
|
if version not in scu.BULLETINS_VERSIONS:
|
|
raise ValueError("invalid version code !")
|
|
self.bul_dict = bul_dict
|
|
self.infos = bul_dict # legacy code compat
|
|
# authuser nécessaire pour version HTML qui contient liens dépendants de l'utilisateur
|
|
self.authuser = authuser
|
|
self.etud: Identite = etud
|
|
self.filigranne = filigranne
|
|
self.formsemestre: FormSemestre = formsemestre
|
|
self.server_name = server_name
|
|
self.version = version
|
|
self.with_img_signatures_pdf = with_img_signatures_pdf
|
|
# Store preferences for convenience:
|
|
formsemestre_id = self.bul_dict["formsemestre_id"]
|
|
self.preferences = sco_preferences.SemPreferences(formsemestre_id)
|
|
self.diagnostic = None # error message if any problem
|
|
# Common PDF styles:
|
|
# - Pour tous les champs du bulletin sauf les cellules de table:
|
|
self.style_field = reportlab.lib.styles.ParagraphStyle({})
|
|
self.style_field.fontName = self.preferences["SCOLAR_FONT_BUL_FIELDS"]
|
|
self.style_field.fontSize = self.preferences["SCOLAR_FONT_SIZE"]
|
|
self.style_field.firstLineIndent = 0
|
|
# Champ signatures
|
|
self.style_signature = self.style_field
|
|
# - Pour les cellules de table:
|
|
self.CellStyle = reportlab.lib.styles.ParagraphStyle({})
|
|
self.CellStyle.fontSize = self.preferences["SCOLAR_FONT_SIZE"]
|
|
self.CellStyle.fontName = self.preferences["SCOLAR_FONT"]
|
|
self.CellStyle.leading = (
|
|
1.0 * self.preferences["SCOLAR_FONT_SIZE"]
|
|
) # vertical space
|
|
# Marges du document PDF
|
|
self.margins = (
|
|
self.preferences["left_margin"],
|
|
self.preferences["top_margin"],
|
|
self.preferences["right_margin"],
|
|
self.preferences["bottom_margin"],
|
|
)
|
|
|
|
def get_filename(self):
|
|
"""Build a filename to be proposed to the web client"""
|
|
sem = sco_formsemestre.get_formsemestre(self.bul_dict["formsemestre_id"])
|
|
return scu.bul_filename_old(sem, self.bul_dict["etud"], "pdf")
|
|
|
|
def generate(self, fmt="", stand_alone=True):
|
|
"""Return bulletin in specified format"""
|
|
if not fmt in self.supported_formats:
|
|
raise ValueError(f"unsupported bulletin format ({fmt})")
|
|
try:
|
|
PDFLOCK.acquire() # this lock is necessary since reportlab is not re-entrant
|
|
if fmt == "html":
|
|
return self.generate_html()
|
|
elif fmt == "pdf":
|
|
return self.generate_pdf(stand_alone=stand_alone)
|
|
else:
|
|
raise ValueError(f"invalid bulletin format ({fmt})")
|
|
finally:
|
|
PDFLOCK.release()
|
|
|
|
def generate_html(self):
|
|
"""Return bulletin as an HTML string"""
|
|
H = ['<div class="notes_bulletin">']
|
|
# table des notes:
|
|
H.append(self.bul_table(fmt="html")) # pylint: disable=no-member
|
|
# infos sous la table:
|
|
H.append(self.bul_part_below(fmt="html")) # pylint: disable=no-member
|
|
H.append("</div>")
|
|
return "\n".join(H)
|
|
|
|
def generate_pdf(self, stand_alone=True):
|
|
"""Build PDF bulletin from distinct parts
|
|
Si stand_alone, génère un doc PDF complet et renvoie une string
|
|
Sinon, renvoie juste une liste d'objets PLATYPUS pour intégration
|
|
dans un autre document.
|
|
"""
|
|
from app.scodoc import sco_preferences
|
|
|
|
formsemestre_id = self.bul_dict["formsemestre_id"]
|
|
nomprenom = self.bul_dict["etud"]["nomprenom"]
|
|
etat_civil = self.bul_dict["etud"]["etat_civil"]
|
|
marque_debut_bulletin = sco_pdf.DebutBulletin(
|
|
etat_civil,
|
|
filigranne=self.bul_dict["filigranne"],
|
|
footer_content=f"""ScoDoc - Bulletin de {nomprenom} - {time.strftime("%d/%m/%Y %H:%M")}""",
|
|
)
|
|
story = []
|
|
# partie haute du bulletin
|
|
story += self.bul_title_pdf() # pylint: disable=no-member
|
|
index_obj_debut = len(story)
|
|
|
|
# table des notes
|
|
story += self.bul_table(fmt="pdf") # pylint: disable=no-member
|
|
# infos sous la table
|
|
story += self.bul_part_below(fmt="pdf") # pylint: disable=no-member
|
|
# signatures
|
|
story += self.bul_signatures_pdf() # pylint: disable=no-member
|
|
if self.scale_table_in_page:
|
|
# Réduit sur une page
|
|
story = [marque_debut_bulletin, KeepInFrame(0, 0, story, mode="shrink")]
|
|
else:
|
|
# Insere notre marqueur qui permet de générer les bookmarks et filigrannes:
|
|
story.insert(index_obj_debut, marque_debut_bulletin)
|
|
|
|
if ScoDocSiteConfig.is_bul_pdf_disabled():
|
|
story = [Paragraph("<p>Export des PDF interdit par l'administrateur</p>")]
|
|
|
|
#
|
|
# objects.append(sco_pdf.FinBulletin())
|
|
if not stand_alone:
|
|
if self.multi_pages:
|
|
# Bulletins sur plusieurs page, force début suivant sur page impaire
|
|
story.append(
|
|
DocIf("doc.page%2 == 1", [PageBreak(), PageBreak()], [PageBreak()])
|
|
)
|
|
else:
|
|
story.append(PageBreak()) # insert page break at end
|
|
|
|
return story
|
|
|
|
# Generation du document PDF
|
|
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
|
report = io.BytesIO() # in-memory document, no disk file
|
|
document = sco_pdf.BaseDocTemplate(report)
|
|
document.addPageTemplates(
|
|
sco_pdf.ScoDocPageTemplate(
|
|
document,
|
|
author=f"""{sco_version.SCONAME} {
|
|
sco_version.SCOVERSION} (E. Viennet) [{self.description}]""",
|
|
title=f"""Bulletin {sem["titremois"]} de {etat_civil}""",
|
|
subject="Bulletin de note",
|
|
margins=self.margins,
|
|
server_name=self.server_name,
|
|
filigranne=self.filigranne,
|
|
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
|
)
|
|
)
|
|
document.build(story)
|
|
data = report.getvalue()
|
|
return data
|
|
|
|
def buildTableObject(self, P, pdfTableStyle, colWidths):
|
|
"""Utility used by some old-style generators.
|
|
Build a platypus Table instance from a nested list of cells, style and widths.
|
|
P: table, as a list of lists
|
|
PdfTableStyle: commandes de style pour la table (reportlab)
|
|
"""
|
|
try:
|
|
# put each table cell in a Paragraph
|
|
Pt = [
|
|
[Paragraph(sco_pdf.SU(x), self.CellStyle) for x in line] for line in P
|
|
]
|
|
except:
|
|
# enquête sur exception intermittente...
|
|
log("*** bug in PDF buildTableObject:")
|
|
log("P=%s" % P)
|
|
# compris: reportlab is not thread safe !
|
|
# see http://two.pairlist.net/pipermail/reportlab-users/2006-June/005037.html
|
|
# (donc maintenant protégé dans ScoDoc par un Lock global)
|
|
self.diagnostic = "erreur lors de la génération du PDF<br>"
|
|
self.diagnostic += "<pre>" + traceback.format_exc() + "</pre>"
|
|
return []
|
|
return Table(Pt, colWidths=colWidths, style=pdfTableStyle)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
def make_formsemestre_bulletin_etud(
|
|
bul_dict,
|
|
etud: Identite = None,
|
|
formsemestre: FormSemestre = None,
|
|
version=None, # short, long, selectedevals
|
|
fmt="pdf", # html, pdf
|
|
stand_alone=True,
|
|
with_img_signatures_pdf: bool = True,
|
|
):
|
|
"""Bulletin de notes
|
|
|
|
Appelle une fonction générant le bulletin au format spécifié à partir des informations infos,
|
|
selon les préférences du semestre.
|
|
|
|
"""
|
|
from app.scodoc import sco_preferences
|
|
|
|
version = version or "long"
|
|
if version not in scu.BULLETINS_VERSIONS:
|
|
raise ValueError("invalid version code !")
|
|
|
|
formsemestre_id = bul_dict["formsemestre_id"]
|
|
bul_class_name = sco_preferences.get_preference("bul_class_name", formsemestre_id)
|
|
gen_class = None
|
|
for bul_class_name in (
|
|
sco_preferences.get_preference("bul_class_name", formsemestre_id),
|
|
# si pas trouvé (modifs locales bizarres ,), ré-essaye avec la valeur par défaut
|
|
bulletin_default_class_name(),
|
|
):
|
|
if bul_dict.get("type") == "BUT" and fmt.startswith("pdf"):
|
|
gen_class = bulletin_get_class(bul_class_name + "BUT")
|
|
if gen_class is None and bul_dict.get("type") != "BUT":
|
|
gen_class = bulletin_get_class(bul_class_name)
|
|
if gen_class is not None:
|
|
break
|
|
|
|
if gen_class is None:
|
|
raise ValueError(f"Type de bulletin PDF invalide (paramètre: {bul_class_name})")
|
|
|
|
try:
|
|
PDFLOCK.acquire()
|
|
bul_generator = gen_class(
|
|
bul_dict,
|
|
authuser=current_user,
|
|
etud=etud,
|
|
filigranne=bul_dict["filigranne"],
|
|
formsemestre=formsemestre,
|
|
server_name=request.url_root,
|
|
version=version,
|
|
with_img_signatures_pdf=with_img_signatures_pdf,
|
|
)
|
|
if fmt not in bul_generator.supported_formats:
|
|
# use standard generator
|
|
log(
|
|
"Bulletin format %s not supported by %s, using %s"
|
|
% (fmt, bul_class_name, bulletin_default_class_name())
|
|
)
|
|
bul_class_name = bulletin_default_class_name()
|
|
gen_class = bulletin_get_class(bul_class_name)
|
|
bul_generator = gen_class(
|
|
bul_dict,
|
|
authuser=current_user,
|
|
etud=etud,
|
|
filigranne=bul_dict["filigranne"],
|
|
formsemestre=formsemestre,
|
|
server_name=request.url_root,
|
|
version=version,
|
|
with_img_signatures_pdf=with_img_signatures_pdf,
|
|
)
|
|
data = bul_generator.generate(fmt=fmt, stand_alone=stand_alone)
|
|
finally:
|
|
PDFLOCK.release()
|
|
|
|
if bul_generator.diagnostic:
|
|
log(f"bul_error: {bul_generator.diagnostic}")
|
|
raise NoteProcessError(bul_generator.diagnostic)
|
|
|
|
filename = bul_generator.get_filename()
|
|
return data, filename
|
|
|
|
|
|
####
|
|
|
|
# Liste des types des classes de générateurs de bulletins PDF:
|
|
BULLETIN_CLASSES = collections.OrderedDict()
|
|
|
|
|
|
def register_bulletin_class(klass):
|
|
BULLETIN_CLASSES[klass.__name__] = klass
|
|
|
|
|
|
def bulletin_class_descriptions():
|
|
return [
|
|
BULLETIN_CLASSES[class_name].description
|
|
for class_name in BULLETIN_CLASSES
|
|
if BULLETIN_CLASSES[class_name].list_in_menu
|
|
]
|
|
|
|
|
|
def bulletin_class_names() -> list[str]:
|
|
"Liste les noms des classes de bulletins à présenter à l'utilisateur"
|
|
return [
|
|
class_name
|
|
for class_name in BULLETIN_CLASSES
|
|
if BULLETIN_CLASSES[class_name].list_in_menu
|
|
]
|
|
|
|
|
|
def bulletin_default_class_name():
|
|
return bulletin_class_names()[0]
|
|
|
|
|
|
def bulletin_get_class(class_name: str) -> BulletinGenerator:
|
|
"""La class de génération de bulletin de ce nom,
|
|
ou None si pas trouvée
|
|
"""
|
|
return BULLETIN_CLASSES.get(class_name)
|
|
|
|
|
|
def bulletin_get_class_name_displayed(formsemestre_id):
|
|
"""Le nom du générateur utilisé, en clair"""
|
|
from app.scodoc import sco_preferences
|
|
|
|
bul_class_name = sco_preferences.get_preference("bul_class_name", formsemestre_id)
|
|
gen_class = bulletin_get_class(bul_class_name)
|
|
if gen_class is None:
|
|
return "invalide ! (voir paramètres)"
|
|
return gen_class.description
|