# -*- 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, ScoPDFFormatError 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 = ['
'] # 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("
") 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("

Export des PDF interdit par l'administrateur

")] # # 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), ) ) try: document.build(story) except ( ValueError, KeyError, reportlab.platypus.doctemplate.LayoutError, ) as exc: raise ScoPDFFormatError(str(exc)) from exc 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
" self.diagnostic += "
" + traceback.format_exc() + "
" 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