From 55c4f2c7999a709ae04a1ff2a9ab48d01b727a7f Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sat, 12 Mar 2022 09:40:48 +0100 Subject: [PATCH] Bul. BUT Bul: bookmarks, reportlab templates --- app/but/bulletin_but_pdf.py | 34 ++++++--- app/scodoc/gen_tables.py | 2 +- app/scodoc/sco_bulletins_generator.py | 9 ++- app/scodoc/sco_bulletins_pdf.py | 31 ++++---- app/scodoc/sco_bulletins_standard.py | 25 ++++--- app/scodoc/sco_pdf.py | 101 ++++++++++++++++++++------ app/scodoc/sco_trombino.py | 4 +- app/scodoc/sco_trombino_tours.py | 4 +- 8 files changed, 143 insertions(+), 67 deletions(-) diff --git a/app/but/bulletin_but_pdf.py b/app/but/bulletin_but_pdf.py index ea3291c8..2bbb5bcc 100644 --- a/app/but/bulletin_but_pdf.py +++ b/app/but/bulletin_but_pdf.py @@ -24,6 +24,7 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard): list_in_menu = False # spécialisation du BulletinGeneratorStandard, ne pas présenter à l'utilisateur scale_table_in_page = False + small_fontsize = "8" def bul_table(self, format="html"): """Génère la table centrale du bulletin de notes @@ -77,16 +78,28 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard): "coef": 2 * cm, } title_bg = tuple(x / 255.0 for x in title_bg) + nota_bene = "La moyenne des ressources et SAÉs dans une UE dépend des poids donnés aux évaluations." # elems pour générer table avec gen_table (liste de dicts) rows = [ # Ligne de titres { "titre": "Unités d'enseignement", - "moyenne": "Note/20", + "moyenne": Paragraph("Note/20"), "coef": "Coef.", "_coef_pdf": Paragraph("Coef."), "_css_row_class": "note_bold", "_pdf_row_markup": ["b"], + "_pdf_style": [ + ("BACKGROUND", (0, 0), (-1, 0), title_bg), + # ("BOTTOMPADDING", (0, 0), (-1, 0), 7), + ], + }, + { + "titre": nota_bene, + "_titre_pdf": Paragraph( + f"{nota_bene}" + ), + "_titre_colspan": 3, "_pdf_style": [ ("BACKGROUND", (0, 0), (-1, 0), title_bg), ("BOTTOMPADDING", (0, 0), (-1, 0), 7), @@ -98,7 +111,7 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard): blue, ), ], - } + }, ] col_keys = ["titre", "coef", "moyenne"] # noms des colonnes à afficher for ue_acronym, ue in self.infos["ues"].items(): @@ -106,7 +119,9 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard): moy_ue = ue.get("moyenne") t = { "titre": f"{ue_acronym} - {ue['titre']}", - "moyenne": moy_ue.get("value", "-") if moy_ue is not None else "-", + "moyenne": Paragraph( + f"""{moy_ue.get("value", "-") if moy_ue is not None else "-"}""" + ), "_css_row_class": "note_bold", "_pdf_row_markup": ["b"], "_pdf_style": [ @@ -128,7 +143,7 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard): "titre": f"""Bonus: {ue['bonus']} - Malus: { ue["malus"]}""", "coef": ects_txt, - "_coef_pdf": Paragraph(f"""{ects_txt}"""), + "_coef_pdf": Paragraph(f"""{ects_txt}"""), "_coef_colspan": 2, # "_css_row_class": "", # "_pdf_row_markup": [""], @@ -157,7 +172,9 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard): for mod_code, mod in ue[mod_type].items(): t = { "titre": f"{mod_code} {self.infos[mod_type][mod_code]['titre']}", - "moyenne": mod["moyenne"], + "moyenne": Paragraph( + f'{mod["moyenne"]}' + ), "coef": mod["coef"], "_coef_pdf": Paragraph( f"{mod['coef']}" @@ -203,7 +220,6 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard): - pdf_style : commandes table Platypus - largeurs de colonnes pour PDF """ - poids_fontsize = "8" # UE à utiliser pour les poids (# colonne/UE) ue_acros = list(self.infos["ues"].keys()) # ['RT1.1', 'RT2.1', 'RT3.1'] # Colonnes à afficher: @@ -243,7 +259,7 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard): } for ue_acro in ue_acros: t[ue_acro] = Paragraph( - f"{ue_acro}" + f"{ue_acro}" ) rows = [t] for mod_code, mod in self.infos[mod_type].items(): @@ -273,7 +289,7 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard): "moyenne": e["note"]["value"], "coef": e["coef"], "_coef_pdf": Paragraph( - f"{e['coef']}" + f"{e['coef']}" ), "_pdf_style": [ ( @@ -288,7 +304,7 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard): col_idx = 1 # 1ere col. poids for ue_acro in ue_acros: t[ue_acro] = Paragraph( - f"""{e["poids"].get(ue_acro, "")}""" + f"""{e["poids"].get(ue_acro, "") or ""}""" ) t["_pdf_style"].append( ( diff --git a/app/scodoc/gen_tables.py b/app/scodoc/gen_tables.py index 0a4b65ca..7f5531c6 100644 --- a/app/scodoc/gen_tables.py +++ b/app/scodoc/gen_tables.py @@ -761,7 +761,7 @@ if __name__ == "__main__": doc = io.BytesIO() document = sco_pdf.BaseDocTemplate(doc) document.addPageTemplates( - sco_pdf.ScolarsPageTemplate( + sco_pdf.ScoDocPageTemplate( document, ) ) diff --git a/app/scodoc/sco_bulletins_generator.py b/app/scodoc/sco_bulletins_generator.py index 5fa07aa8..23e6751f 100644 --- a/app/scodoc/sco_bulletins_generator.py +++ b/app/scodoc/sco_bulletins_generator.py @@ -154,9 +154,11 @@ class BulletinGenerator: from app.scodoc import sco_preferences formsemestre_id = self.infos["formsemestre_id"] - + objects = [] # partie haute du bulletin - objects = self.bul_title_pdf() # pylint: disable=no-member + objects += self.bul_title_pdf() # pylint: disable=no-member + objects += [sco_pdf.DebutBulletin(self.infos["etud"]["nomprenom"])] + # table des notes objects += self.bul_table(format="pdf") # pylint: disable=no-member # infos sous la table @@ -168,6 +170,7 @@ class BulletinGenerator: # Réduit sur une page objects = [KeepInFrame(0, 0, objects, mode="shrink")] # + # objects.append(sco_pdf.FinBulletin()) if not stand_alone: objects.append(PageBreak()) # insert page break at end return objects @@ -177,7 +180,7 @@ class BulletinGenerator: report = io.BytesIO() # in-memory document, no disk file document = sco_pdf.BaseDocTemplate(report) document.addPageTemplates( - sco_pdf.ScolarsPageTemplate( + sco_pdf.ScoDocPageTemplate( document, author="%s %s (E. Viennet) [%s]" % (sco_version.SCONAME, sco_version.SCOVERSION, self.description), diff --git a/app/scodoc/sco_bulletins_pdf.py b/app/scodoc/sco_bulletins_pdf.py index 1df2ca66..d51a34eb 100644 --- a/app/scodoc/sco_bulletins_pdf.py +++ b/app/scodoc/sco_bulletins_pdf.py @@ -54,9 +54,7 @@ import io import re import time import traceback -from pydoc import html - -from reportlab.platypus.doctemplate import BaseDocTemplate +import pydoc from flask import g, request @@ -74,17 +72,17 @@ import app.scodoc.sco_utils as scu import sco_version -def pdfassemblebulletins( - formsemestre_id, - objects, - bul_title, +def assemble_bulletins_pdf( + formsemestre_id: int, + story: list, + bul_title: str, infos, pagesbookmarks, filigranne=None, server_name="", ): - "generate PDF document from a list of PLATYPUS objects" - if not objects: + "Generate PDF document from a story (list of PLATYPUS objects)." + if not story: return "" # Paramètres de mise en page margins = ( @@ -93,11 +91,10 @@ def pdfassemblebulletins( sco_preferences.get_preference("right_margin", formsemestre_id), sco_preferences.get_preference("bottom_margin", formsemestre_id), ) - report = io.BytesIO() # in-memory document, no disk file - document = BaseDocTemplate(report) + document = sco_pdf.BulletinDocTemplate(report) document.addPageTemplates( - sco_pdf.ScolarsPageTemplate( + sco_pdf.ScoDocPageTemplate( document, author="%s %s (E. Viennet)" % (sco_version.SCONAME, sco_version.SCOVERSION), title="Bulletin %s" % bul_title, @@ -109,7 +106,8 @@ def pdfassemblebulletins( preferences=sco_preferences.SemPreferences(formsemestre_id), ) ) - document.build(objects) + document.build(story) + # document.multiBuild(story) data = report.getvalue() return data @@ -121,7 +119,8 @@ def replacement_function(match): if logo is not None: return r'' % (match.group(2), logo.filepath, match.group(4)) raise ScoValueError( - 'balise "%s": logo "%s" introuvable' % (html.escape(balise), html.escape(name)) + 'balise "%s": logo "%s" introuvable' + % (pydoc.html.escape(balise), pydoc.html.escape(name)) ) @@ -205,7 +204,7 @@ def get_formsemestre_bulletins_pdf(formsemestre_id, version="selectedevals"): server_name = "" try: sco_pdf.PDFLOCK.acquire() - pdfdoc = pdfassemblebulletins( + pdfdoc = assemble_bulletins_pdf( formsemestre_id, fragments, formsemestre.titre_mois(), @@ -255,7 +254,7 @@ def get_etud_bulletins_pdf(etudid, version="selectedevals"): server_name = "" try: sco_pdf.PDFLOCK.acquire() - pdfdoc = pdfassemblebulletins( + pdfdoc = assemble_bulletins_pdf( None, fragments, etud["nomprenom"], diff --git a/app/scodoc/sco_bulletins_standard.py b/app/scodoc/sco_bulletins_standard.py index 95f6f172..842844d6 100644 --- a/app/scodoc/sco_bulletins_standard.py +++ b/app/scodoc/sco_bulletins_standard.py @@ -73,7 +73,7 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator): description = "standard ScoDoc (version 2011)" # la description doit être courte: elle apparait dans le menu de paramètrage ScoDoc supported_formats = ["html", "pdf"] - def bul_title_pdf(self): + def bul_title_pdf(self) -> list: """Génère la partie "titre" du bulletin de notes. Renvoie une liste d'objets platypus """ @@ -115,11 +115,11 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator): - en PDF: une liste d'objets platypus """ H = [] # html - Op = [] # objets platypus + story = [] # objets platypus # ----- ABSENCES if self.preferences["bul_show_abs"]: nbabs = self.infos["nbabs"] - Op.append(Spacer(1, 2 * mm)) + story.append(Spacer(1, 2 * mm)) if nbabs: H.append( """

@@ -130,7 +130,7 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator): """ % self.infos ) - Op.append( + story.append( Paragraph( SU( "%(nbabs)s absences (1/2 journées), dont %(nbabsjust)s justifiées." @@ -141,7 +141,7 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator): ) else: H.append("""

Pas d'absences signalées.

""") - Op.append(Paragraph(SU("Pas d'absences signalées."), self.CellStyle)) + story.append(Paragraph(SU("Pas d'absences signalées."), self.CellStyle)) # ---- APPRECIATIONS # le dir. des etud peut ajouter des appreciations, @@ -168,10 +168,10 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator): % self.infos ) H.append("") - # Appreciations sur PDF: + # Appréciations sur PDF: if self.infos.get("appreciations_list", False): - Op.append(Spacer(1, 3 * mm)) - Op.append( + story.append(Spacer(1, 3 * mm)) + story.append( Paragraph( SU("Appréciation : " + "\n".join(self.infos["appreciations_txt"])), self.CellStyle, @@ -180,7 +180,7 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator): # ----- DECISION JURY if self.preferences["bul_show_decision"]: - Op += sco_bulletins_pdf.process_field( + story += sco_bulletins_pdf.process_field( self.preferences["bul_pdf_caption"], self.infos, self.FieldStyle, @@ -196,7 +196,12 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator): # ----- if format == "pdf": - return [KeepTogether(Op)] + if self.scale_table_in_page: + # le scaling (pour tenir sur une page) semble incompatible avec + # le KeepTogether() + return story + else: + return [KeepTogether(story)] elif format == "html": return "\n".join(H) diff --git a/app/scodoc/sco_pdf.py b/app/scodoc/sco_pdf.py index 3949f50a..4f674e6c 100755 --- a/app/scodoc/sco_pdf.py +++ b/app/scodoc/sco_pdf.py @@ -175,7 +175,27 @@ def bold_paras(L, tag="b", close=None): return [b + (x or "") + close for x in L] -class ScolarsPageTemplate(PageTemplate): +class BulMarker(Flowable): + """Custom Flowables pour nos bulletins PDF: invisibles, juste pour se repérer""" + + def wrap(self, *args): + return (0, 0) + + def draw(self): + return + + +class DebutBulletin(BulMarker): + """Début d'un bulletin. + Element vide utilisé pour générer les bookmarks + """ + + def __init__(self, bookmark=None): + self.bookmark = bookmark + super().__init__() + + +class ScoDocPageTemplate(PageTemplate): """Our own page template.""" def __init__( @@ -203,6 +223,8 @@ class ScolarsPageTemplate(PageTemplate): self.pdfmeta_subject = subject self.server_name = server_name self.filigranne = filigranne + self.page_number = 1 + self.current_page_bookmark = None self.footer_template = footer_template if self.preferences: self.with_page_background = self.preferences["bul_pdf_with_background"] @@ -217,7 +239,7 @@ class ScolarsPageTemplate(PageTemplate): document.pagesize[0] - 20.0 * mm - left * mm - right * mm, document.pagesize[1] - 18.0 * mm - top * mm - bottom * mm, ) - PageTemplate.__init__(self, "ScolarsPageTemplate", [content]) + super().__init__("ScoDocPageTemplate", [content]) self.logo = None logo = find_logo( logoname="bul_pdf_background", dept_id=g.scodoc_dept_id @@ -265,21 +287,6 @@ class ScolarsPageTemplate(PageTemplate): ) = self.logo canvas.drawImage(image, inch, doc.pagesize[1] - inch, width, height) - # ---- Filigranne (texte en diagonal en haut a gauche de chaque page) - if self.filigranne: - if isinstance(self.filigranne, str): - filigranne = self.filigranne # same for all pages - else: - filigranne = self.filigranne.get(doc.page, None) - if filigranne: - canvas.saveState() - canvas.translate(9 * cm, 27.6 * cm) - canvas.rotate(30) - canvas.scale(4.5, 4.5) - canvas.setFillColorRGB(1.0, 0.65, 0.65) - canvas.drawRightString(0, 0, SU(filigranne)) - canvas.restoreState() - # ---- Add some meta data and bookmarks if self.pdfmeta_author: canvas.setAuthor(SU(self.pdfmeta_author)) @@ -287,12 +294,16 @@ class ScolarsPageTemplate(PageTemplate): canvas.setTitle(SU(self.pdfmeta_title)) if self.pdfmeta_subject: canvas.setSubject(SU(self.pdfmeta_subject)) - bm = self.pagesbookmarks.get(doc.page, None) - if bm != None: - key = bm - txt = SU(bm) - canvas.bookmarkPage(key) - canvas.addOutlineEntry(txt, bm) + # if self.current_page_bookmark: + # canvas.bookmarkPage(self.current_page_bookmark) + # canvas.addOutlineEntry( + # SU(self.current_page_bookmark), self.current_page_bookmark + # ) + # else: XXX A REMETTRE + # bookmark = self.pagesbookmarks.get(doc.page, None) + # if bookmark != None: + # canvas.bookmarkPage(bookmark) + # canvas.addOutlineEntry(SU(bookmark), bookmark) # ---- Footer canvas.setFont( self.preferences["SCOLAR_FONT"], self.preferences["SCOLAR_FONT_SIZE_FOOT"] @@ -308,6 +319,48 @@ class ScolarsPageTemplate(PageTemplate): ) canvas.restoreState() + def afterDrawPage(self, canvas, doc): + if not self.preferences: + return + # ---- Filigranne (texte en diagonal en haut a gauche de chaque page) + if self.filigranne: + if isinstance(self.filigranne, str): + filigranne = self.filigranne # same for all pages + else: + filigranne = self.filigranne.get(doc.page, None) + if filigranne: + canvas.saveState() + canvas.translate(9 * cm, 27.6 * cm) + canvas.rotate(30) + canvas.scale(4.5, 4.5) + canvas.setFillColorRGB(1.0, 0.65, 0.65, alpha=0.6) + canvas.drawRightString(0, 0, SU(filigranne)) + canvas.restoreState() + + def afterPage(self): + """Called after all flowables have been drawn on a page. + Increment pageNum since the page has been completed. + """ + self.page_number += 1 + self.current_page_bookmark = None + + +class BulletinDocTemplate(BaseDocTemplate): + """Doc template pour les bulletins PDF + ajoute la gestion des bookmarks + """ + + # inspired by https://www.reportlab.com/snippets/13/ + def afterFlowable(self, flowable): + """Called by Reportlab after each flowable""" + if isinstance(flowable, DebutBulletin): + if flowable.bookmark: + self.canv.bookmarkPage(flowable.bookmark) + self.canv.addOutlineEntry( + SU(flowable.bookmark), flowable.bookmark, level=0, closed=None + ) + # log(f"afterFlowable addOutlineEntry {flowable.bookmark} page {self.page}") + def _makeTimeDict(): # ... suboptimal but we don't care @@ -333,7 +386,7 @@ def pdf_basic_page( report = io.BytesIO() # in-memory document, no disk file document = BaseDocTemplate(report) document.addPageTemplates( - ScolarsPageTemplate( + ScoDocPageTemplate( document, title=title, author="%s %s (E. Viennet)" % (sco_version.SCONAME, sco_version.SCOVERSION), diff --git a/app/scodoc/sco_trombino.py b/app/scodoc/sco_trombino.py index 9132336e..e9102fc6 100644 --- a/app/scodoc/sco_trombino.py +++ b/app/scodoc/sco_trombino.py @@ -378,7 +378,7 @@ def _trombino_pdf(groups_infos): # Build document document = BaseDocTemplate(report) document.addPageTemplates( - sco_pdf.ScolarsPageTemplate( + sco_pdf.ScoDocPageTemplate( document, preferences=sco_preferences.SemPreferences(sem["formsemestre_id"]), ) @@ -458,7 +458,7 @@ def _listeappel_photos_pdf(groups_infos): # Build document document = BaseDocTemplate(report) document.addPageTemplates( - sco_pdf.ScolarsPageTemplate( + sco_pdf.ScoDocPageTemplate( document, preferences=sco_preferences.SemPreferences(sem["formsemestre_id"]), ) diff --git a/app/scodoc/sco_trombino_tours.py b/app/scodoc/sco_trombino_tours.py index 3861bb74..c1ff7bcc 100644 --- a/app/scodoc/sco_trombino_tours.py +++ b/app/scodoc/sco_trombino_tours.py @@ -264,7 +264,7 @@ def pdf_trombino_tours( filename = "trombino-%s-%s.pdf" % (DeptName, groups_infos.groups_filename) document = BaseDocTemplate(report) document.addPageTemplates( - ScolarsPageTemplate( + ScoDocPageTemplate( document, preferences=sco_preferences.SemPreferences(), ) @@ -460,7 +460,7 @@ def pdf_feuille_releve_absences( else: document = BaseDocTemplate(report, pagesize=taille) document.addPageTemplates( - ScolarsPageTemplate( + ScoDocPageTemplate( document, preferences=sco_preferences.SemPreferences(), )