From 6160d48e9e1fcc66987c58e4dc483fa553a44106 Mon Sep 17 00:00:00 2001 From: viennet Date: Tue, 13 Oct 2020 15:41:04 +0200 Subject: [PATCH] =?UTF-8?q?Ajout=20image=20de=20fond=20sur=20PV=20jury=20e?= =?UTF-8?q?t=20lettres=20individuelles=20de=20d=C3=A9cision.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sco_pdf.py | 5 ++- sco_preferences.py | 100 +++++++++++++++++++++++++++++++++++++++---- sco_pvpdf.py | 104 +++++++++++++++++++++++++++++++-------------- sco_utils.py | 40 ++++++++--------- 4 files changed, 187 insertions(+), 62 deletions(-) diff --git a/sco_pdf.py b/sco_pdf.py index 18d8af2..90dc076 100644 --- a/sco_pdf.py +++ b/sco_pdf.py @@ -69,7 +69,7 @@ def SU(s): # Remplace caractères composés # eg 'e\xcc\x81' COMBINING ACUTE ACCENT par '\xc3\xa9' LATIN SMALL LETTER E WITH ACUTE # car les "combining accents" ne sont pas traités par ReportLab mais peuvent - # nous être envoyés par certains navigaters ou imports + # nous être envoyés par certains navigateurs ou imports u = unicodedata.normalize("NFC", unicode(s, SCO_ENCODING, "replace")) return u.encode("utf8") @@ -200,9 +200,10 @@ class ScolarsPageTemplate(PageTemplate): minute: Minute as a decimal number [00,59]. server_url: URL du serveur ScoDoc - + """ canvas.saveState() + # ---- Logo: a small image, positionned at top left of the page if self.logo is not None: # draws the logo if it exists ((width, height), image) = self.logo diff --git a/sco_preferences.py b/sco_preferences.py index cff9a66..f28b010 100644 --- a/sco_preferences.py +++ b/sco_preferences.py @@ -786,6 +786,77 @@ vu la délibération de la commission %(Type)s en date du %(Date)s présidée pa "category": "pvpdf", }, ), + ( + "PV_WITH_BACKGROUND", + { + "initvalue": 0, + "title": "Mettre l'image de fond sur les PV de jury (paysage)", + "input_type": "boolcheckbox", + "labels": ["non", "oui"], + "category": "pvpdf", + }, + ), + ( + "PV_WITH_HEADER", + { + "initvalue": 1, # legacy + "title": "Ajouter l'en-tête sur les PV (paysage)", + "input_type": "boolcheckbox", + "labels": ["non", "oui"], + "category": "pvpdf", + }, + ), + ( + "PV_WITH_FOOTER", + { + "initvalue": 1, # legacy + "title": "Ajouter le pied de page sur les PV (paysage)", + "input_type": "boolcheckbox", + "labels": ["non", "oui"], + "category": "pvpdf", + }, + ), + # marges PV paysages (en millimètres) + ( + "pv_left_margin", + { + "initvalue": 0, + "size": 10, + "title": "Marge gauche PV en mm", + "type": "float", + "category": "pvpdf", + }, + ), + ( + "pv_top_margin", + { + "initvalue": 23, + "size": 10, + "title": "Marge haute PV", + "type": "float", + "category": "pvpdf", + }, + ), + ( + "pv_right_margin", + { + "initvalue": 0, + "size": 10, + "title": "Marge droite PV", + "type": "float", + "category": "pvpdf", + }, + ), + ( + "pv_bottom_margin", + { + "initvalue": 5, + "size": 10, + "title": "Marge basse PV", + "type": "float", + "category": "pvpdf", + }, + ), ( "PV_LETTER_DIPLOMA_SIGNATURE", { @@ -872,11 +943,21 @@ s'est réuni le %(date_jury)s. "category": "pvpdf", }, ), + ( + "PV_LETTER_WITH_BACKGROUND", + { + "initvalue": 0, + "title": "Mettre l'image de fond sur les lettres individuelles de décision", + "input_type": "boolcheckbox", + "labels": ["non", "oui"], + "category": "pvpdf", + }, + ), ( "PV_LETTER_WITH_HEADER", { "initvalue": 0, - "title": "Imprimer le logo en tête des lettres individuelles de décision", + "title": "Ajouter l'en-tête sur les lettres individuelles de décision", "input_type": "boolcheckbox", "labels": ["non", "oui"], "category": "pvpdf", @@ -886,7 +967,7 @@ s'est réuni le %(date_jury)s. "PV_LETTER_WITH_FOOTER", { "initvalue": 0, - "title": "Imprimer le pied de page sur les lettres individuelles de décision", + "title": "Ajouter le pied de page sur les lettres individuelles de décision", "input_type": "boolcheckbox", "labels": ["non", "oui"], "category": "pvpdf", @@ -1633,8 +1714,7 @@ class sco_base_preferences: self.load() def load(self): - """Load all preferences from db - """ + """Load all preferences from db""" log("loading preferences") try: GSL.acquire() @@ -2063,12 +2143,14 @@ def doc_preferences(context): L = [] for cat, cat_descr in PREF_CATEGORIES: L.append([""]) - L.append(["## " + cat_descr.get("title", "") ]) + L.append(["## " + cat_descr.get("title", "")]) L.append([""]) - L.append( ["Nom", " ", " " ] ) - L.append( ["----", "----", "----"] ) + L.append(["Nom", " ", " "]) + L.append(["----", "----", "----"]) for pref_name, pref in PREFS: if pref["category"] == cat: - L.append(['`'+pref_name+'`', pref["title"], pref.get("explanation", "")]) - + L.append( + ["`" + pref_name + "`", pref["title"], pref.get("explanation", "")] + ) + return "\n".join([" | ".join(x) for x in L]) diff --git a/sco_pvpdf.py b/sco_pvpdf.py index 92531c4..b38d504 100644 --- a/sco_pvpdf.py +++ b/sco_pvpdf.py @@ -94,7 +94,9 @@ def pageFooter(canvas, doc, logo, preferences, with_page_numbers=True): ("RIGHTPADDING", (-1, 0), (-1, 0), 1 * cm), ] ) - elems = [p, logo] + elems = [p] + if logo: + elems.append(logo) colWidths = [None, LOGO_FOOTER_WIDTH + 2 * mm] if with_page_numbers: elems.append(np) @@ -124,14 +126,14 @@ def pageHeader(canvas, doc, logo, preferences, only_on_first_page=False): id="monheader", showBoundary=0, ) - canvas.saveState() # is it necessary ? - head.addFromList([logo], canvas) - canvas.restoreState() + if logo: + canvas.saveState() # is it necessary ? + head.addFromList([logo], canvas) + canvas.restoreState() class CourrierIndividuelTemplate(PageTemplate): - """Template pour courrier avisant des decisions de jury (1 page /etudiant) - """ + """Template pour courrier avisant des decisions de jury (1 page /etudiant)""" def __init__( self, @@ -156,6 +158,13 @@ class CourrierIndividuelTemplate(PageTemplate): self.preferences = preferences self.force_header = force_header self.force_footer = force_footer + self.with_footer = ( + self.force_footer or self.preferences["PV_LETTER_WITH_HEADER"] + ) + self.with_header = ( + self.force_header or self.preferences["PV_LETTER_WITH_FOOTER"] + ) + self.with_page_background = self.preferences["PV_LETTER_WITH_BACKGROUND"] self.with_page_numbers = False self.header_only_on_first_page = False # Our doc is made of a single frame @@ -175,16 +184,32 @@ class CourrierIndividuelTemplate(PageTemplate): PageTemplate.__init__(self, template_name, [content]) - self.logo_footer = Image( - image_dir + "/logo_footer.jpg", - height=LOGO_FOOTER_HEIGHT, - width=LOGO_FOOTER_WIDTH, - ) - self.logo_header = Image( - image_dir + "/logo_header.jpg", - height=LOGO_HEADER_HEIGHT, - width=LOGO_HEADER_WIDTH, - ) + self.background_image_filename = None + self.logo_footer = None + self.logo_header = None + for suffix in LOGOS_IMAGES_ALLOWED_TYPES: + if template_name == "PVJuryTemplate": + fn = image_dir + "/pvjury_background" + "." + suffix + else: + fn = image_dir + "/letter_background" + "." + suffix + if os.path.exists(fn): + self.background_image_filename = fn + + fn = image_dir + "/logo_footer" + "." + suffix + if os.path.exists(fn): + self.logo_footer = Image( + fn, + height=LOGO_FOOTER_HEIGHT, + width=LOGO_FOOTER_WIDTH, + ) + + fn = image_dir + "/logo_header" + "." + suffix + if os.path.exists(fn): + self.logo_header = Image( + fn, + height=LOGO_HEADER_HEIGHT, + width=LOGO_HEADER_WIDTH, + ) def beforeDrawPage(self, canvas, doc): """Draws a logo and an contribution message on each page.""" @@ -201,8 +226,15 @@ class CourrierIndividuelTemplate(PageTemplate): txt = SU(bm) canvas.bookmarkPage(key) canvas.addOutlineEntry(txt, bm) - if self.force_footer or self.preferences["PV_LETTER_WITH_HEADER"]: - # --- Add header + + # ---- Background image + if self.background_image_filename and self.with_page_background: + canvas.drawImage( + self.background_image_filename, 0, 0, doc.pagesize[0], doc.pagesize[1] + ) + + # ---- Header/Footer + if self.with_header: pageHeader( canvas, doc, @@ -210,8 +242,7 @@ class CourrierIndividuelTemplate(PageTemplate): self.preferences, self.header_only_on_first_page, ) - if self.force_footer or self.preferences["PV_LETTER_WITH_FOOTER"]: - # --- Add footer + if self.with_footer: pageFooter( canvas, doc, @@ -222,8 +253,7 @@ class CourrierIndividuelTemplate(PageTemplate): class PVTemplate(CourrierIndividuelTemplate): - """Template pour les pages des PV de jury - """ + """Template pour les pages des PV de jury""" def __init__( self, @@ -231,10 +261,17 @@ class PVTemplate(CourrierIndividuelTemplate): author=None, title=None, subject=None, - margins=(0, 23, 0, 5), # additional margins in mm (left,top,right, bottom) + margins=None, # additional margins in mm (left,top,right, bottom) image_dir="", preferences=None, # dictionnary with preferences, required ): + if margins is None: + margins = ( + preferences["pv_left_margin"], + preferences["pv_top_margin"], + preferences["pv_right_margin"], + preferences["pv_bottom_margin"], + ) CourrierIndividuelTemplate.__init__( self, document, @@ -250,6 +287,9 @@ class PVTemplate(CourrierIndividuelTemplate): ) self.with_page_numbers = True self.header_only_on_first_page = True + self.with_header = self.preferences["PV_WITH_HEADER"] + self.with_footer = self.preferences["PV_WITH_FOOTER"] + self.with_page_background = self.preferences["PV_WITH_BACKGROUND"] def afterDrawPage(self, canvas, doc): """Called after all flowables have been drawn on a page""" @@ -331,7 +371,7 @@ def pdf_lettres_individuelles( document = BaseDocTemplate(report) image_dir = SCODOC_LOGOS_DIR + "/logos_" + context.DeptId() + "/" if not os.path.exists(image_dir): - image_dir = SCODOC_LOGOS_DIR + "/" # use global logos + image_dir = SCODOC_LOGOS_DIR + "/" # use global logos document.addPageTemplates( CourrierIndividuelTemplate( document, @@ -437,9 +477,13 @@ def pdf_lettre_individuelle(sem, decision, etud, params, signature=None, context s = "s" else: s = "" - params["autorisations_txt"] = ( - """Vous êtes autorisé%s à continuer dans le%s semestre%s : %s""" - % (etud["ne"], s, s, decision["autorisations_descr"]) + params[ + "autorisations_txt" + ] = """Vous êtes autorisé%s à continuer dans le%s semestre%s : %s""" % ( + etud["ne"], + s, + s, + decision["autorisations_descr"], ) else: params["autorisations_txt"] = "" @@ -538,9 +582,6 @@ def _make_signature_image(signature, leftindent, formsemestre_id, context=None): # ---------------------------------------------- -# PV complet, tableau en format paysage - - def pvjury_pdf( context, dpv, @@ -554,6 +595,7 @@ def pvjury_pdf( anonymous=False, ): """Doc PDF récapitulant les décisions de jury + (tableau en format paysage) dpv: result of dict_pvjury """ if not dpv: @@ -602,7 +644,7 @@ def pvjury_pdf( document.pagesize = landscape(A4) image_dir = SCODOC_LOGOS_DIR + "/logos_" + context.DeptId() + "/" if not os.path.exists(image_dir): - image_dir = SCODOC_LOGOS_DIR + "/" # use global logos + image_dir = SCODOC_LOGOS_DIR + "/" # use global logos document.addPageTemplates( PVTemplate( document, diff --git a/sco_utils.py b/sco_utils.py index 9e9da47..596e006 100644 --- a/sco_utils.py +++ b/sco_utils.py @@ -156,15 +156,14 @@ def fmt_note(val, note_max=None, keep_numeric=False): def fmt_coef(val): - """Conversion valeur coefficient (float) en chaine - """ + """Conversion valeur coefficient (float) en chaine""" if val < 0.01: return "%g" % val # unusually small value return "%g" % round(val, 2) def fmt_abs(val): - """ Conversion absences en chaine. val est une list [nb_abs_total, nb_abs_justifiees + """Conversion absences en chaine. val est une list [nb_abs_total, nb_abs_justifiees => NbAbs / Nb_justifiees """ return "%s / %s" % (val[0], val[1]) @@ -194,7 +193,7 @@ GSL = thread.allocate_lock() # Global ScoDoc Lock if "INSTANCE_HOME" in os.environ: # ----- Repertoire "var" (local) - SCODOC_VAR_DIR = os.path.join( os.environ["INSTANCE_HOME"], "var", "scodoc" ) + SCODOC_VAR_DIR = os.path.join(os.environ["INSTANCE_HOME"], "var", "scodoc") # ----- Version information SCODOC_VERSION_DIR = os.path.join(SCODOC_VAR_DIR, "config", "version") # ----- Repertoire tmp @@ -203,13 +202,12 @@ if "INSTANCE_HOME" in os.environ: os.mkdir(SCO_TMPDIR, 0o755) # ----- Les logos: /opt/scodoc/var/scodoc/config/logos SCODOC_LOGOS_DIR = os.path.join(SCODOC_VAR_DIR, "config", "logos") - + # ----- Repertoire "config" (devrait s'appeler "tools"...) SCO_CONFIG_DIR = os.path.join( os.environ["INSTANCE_HOME"], "Products", "ScoDoc", "config" ) - # ----- Lecture du fichier de configuration SCO_SRCDIR = os.path.split(VERSION.__file__)[0] @@ -231,7 +229,9 @@ except: raise if hasattr(CONFIG, "CODES_EXPL"): - CODES_EXPL.update(CONFIG.CODES_EXPL) # permet de customiser les explications de codes + CODES_EXPL.update( + CONFIG.CODES_EXPL + ) # permet de customiser les explications de codes if CONFIG.CUSTOM_HTML_HEADER: @@ -270,7 +270,7 @@ SCO_DEFAULT_SQL_USERS_CNX = "dbname=SCOUSERS port=%s" % SCO_DEFAULT_SQL_PORT # Valeurs utilisées pour affichage seulement, pas de requetes ni de mails envoyés: SCO_WEBSITE = "https://scodoc.org" -SCO_USER_MANUAL = "https://scodoc.org/GuideUtilisateur" +SCO_USER_MANUAL = "https://scodoc.org/GuideUtilisateur" SCO_ANNONCES_WEBSITE = "https://listes.univ-paris13.fr/mailman/listinfo/scodoc-annonces" SCO_DEVEL_LIST = "scodoc-devel@listes.univ-paris13.fr" SCO_USERS_LIST = "notes@listes.univ-paris13.fr" @@ -296,6 +296,8 @@ PDF_MIMETYPE = "application/pdf" XML_MIMETYPE = "text/xml" JSON_MIMETYPE = "application/json" +LOGOS_IMAGES_ALLOWED_TYPES = ("jpg", "png") # remind that PIL does not read pdf + class DictDefault(dict): # obsolete, use collections.defaultdict """A dictionnary with default value for all keys @@ -421,7 +423,7 @@ def simple_dictlist2xml(dictlist, doc=None, tagname=None, quote=False): - + """ if not tagname: raise ValueError("invalid empty tagname !") @@ -462,8 +464,7 @@ ALPHANUM_EXP = re.compile(r"^[\w-]+$", re.UNICODE) def is_valid_code_nip(s): - """True si s peut être un code NIP: au moins 6 chiffres décimaux - """ + """True si s peut être un code NIP: au moins 6 chiffres décimaux""" if not s: return False return re.match(r"^[0-9]{6,32}$", s) @@ -492,7 +493,7 @@ def suppress_accents(s): def sanitize_string(s): """s is an ordinary string, encoding given by SCO_ENCODING" - suppress accents and chars interpreted in XML + suppress accents and chars interpreted in XML Irreversible (not a quote) For ids and some filenames @@ -747,8 +748,7 @@ from sgmllib import SGMLParser class html2txt_parser(SGMLParser): - """html2txt() - """ + """html2txt()""" def reset(self): """reset() --> initialize the parser""" @@ -757,15 +757,15 @@ class html2txt_parser(SGMLParser): def handle_data(self, text): """handle_data(text) --> appends the pieces to self.pieces - handles all normal data not between brackets "<>" - """ + handles all normal data not between brackets "<>" + """ self.pieces.append(text) def handle_entityref(self, ref): """called for each entity reference, e.g. for "©", ref will be - "copy" - Reconstruct the original entity reference. - """ + "copy" + Reconstruct the original entity reference. + """ if ref == "amp": self.pieces.append("&") @@ -794,7 +794,7 @@ def icontag(name, file_format="png", **attrs): """tag HTML pour un icone. (dans les versions anterieures on utilisait Zope) Les icones sont des fichiers PNG dans .../static/icons - Si la taille (width et height) n'est pas spécifiée, lit l'image + Si la taille (width et height) n'est pas spécifiée, lit l'image pour la mesurer (et cache le résultat). """ if ("width" not in attrs) or ("height" not in attrs):