Ajout image de fond sur PV jury et lettres individuelles de décision.

This commit is contained in:
viennet 2020-10-13 15:41:04 +02:00
parent 439571862b
commit 6160d48e9e
4 changed files with 187 additions and 62 deletions

View File

@ -69,7 +69,7 @@ def SU(s):
# Remplace caractères composés # Remplace caractères composés
# eg 'e\xcc\x81' COMBINING ACUTE ACCENT par '\xc3\xa9' LATIN SMALL LETTER E WITH ACUTE # 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 # 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")) u = unicodedata.normalize("NFC", unicode(s, SCO_ENCODING, "replace"))
return u.encode("utf8") return u.encode("utf8")
@ -200,9 +200,10 @@ class ScolarsPageTemplate(PageTemplate):
minute: Minute as a decimal number [00,59]. minute: Minute as a decimal number [00,59].
server_url: URL du serveur ScoDoc server_url: URL du serveur ScoDoc
""" """
canvas.saveState() canvas.saveState()
# ---- Logo: a small image, positionned at top left of the page
if self.logo is not None: if self.logo is not None:
# draws the logo if it exists # draws the logo if it exists
((width, height), image) = self.logo ((width, height), image) = self.logo

View File

@ -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", "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", "PV_LETTER_DIPLOMA_SIGNATURE",
{ {
@ -872,11 +943,21 @@ s'est réuni le %(date_jury)s.
"category": "pvpdf", "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", "PV_LETTER_WITH_HEADER",
{ {
"initvalue": 0, "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", "input_type": "boolcheckbox",
"labels": ["non", "oui"], "labels": ["non", "oui"],
"category": "pvpdf", "category": "pvpdf",
@ -886,7 +967,7 @@ s'est réuni le %(date_jury)s.
"PV_LETTER_WITH_FOOTER", "PV_LETTER_WITH_FOOTER",
{ {
"initvalue": 0, "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", "input_type": "boolcheckbox",
"labels": ["non", "oui"], "labels": ["non", "oui"],
"category": "pvpdf", "category": "pvpdf",
@ -1633,8 +1714,7 @@ class sco_base_preferences:
self.load() self.load()
def load(self): def load(self):
"""Load all preferences from db """Load all preferences from db"""
"""
log("loading preferences") log("loading preferences")
try: try:
GSL.acquire() GSL.acquire()
@ -2063,12 +2143,14 @@ def doc_preferences(context):
L = [] L = []
for cat, cat_descr in PREF_CATEGORIES: for cat, cat_descr in PREF_CATEGORIES:
L.append([""]) L.append([""])
L.append(["## " + cat_descr.get("title", "") ]) L.append(["## " + cat_descr.get("title", "")])
L.append([""]) L.append([""])
L.append( ["Nom", " ", " " ] ) L.append(["Nom", " ", " "])
L.append( ["----", "----", "----"] ) L.append(["----", "----", "----"])
for pref_name, pref in PREFS: for pref_name, pref in PREFS:
if pref["category"] == cat: 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]) return "\n".join([" | ".join(x) for x in L])

View File

@ -94,7 +94,9 @@ def pageFooter(canvas, doc, logo, preferences, with_page_numbers=True):
("RIGHTPADDING", (-1, 0), (-1, 0), 1 * cm), ("RIGHTPADDING", (-1, 0), (-1, 0), 1 * cm),
] ]
) )
elems = [p, logo] elems = [p]
if logo:
elems.append(logo)
colWidths = [None, LOGO_FOOTER_WIDTH + 2 * mm] colWidths = [None, LOGO_FOOTER_WIDTH + 2 * mm]
if with_page_numbers: if with_page_numbers:
elems.append(np) elems.append(np)
@ -124,14 +126,14 @@ def pageHeader(canvas, doc, logo, preferences, only_on_first_page=False):
id="monheader", id="monheader",
showBoundary=0, showBoundary=0,
) )
canvas.saveState() # is it necessary ? if logo:
head.addFromList([logo], canvas) canvas.saveState() # is it necessary ?
canvas.restoreState() head.addFromList([logo], canvas)
canvas.restoreState()
class CourrierIndividuelTemplate(PageTemplate): 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__( def __init__(
self, self,
@ -156,6 +158,13 @@ class CourrierIndividuelTemplate(PageTemplate):
self.preferences = preferences self.preferences = preferences
self.force_header = force_header self.force_header = force_header
self.force_footer = force_footer 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.with_page_numbers = False
self.header_only_on_first_page = False self.header_only_on_first_page = False
# Our doc is made of a single frame # Our doc is made of a single frame
@ -175,16 +184,32 @@ class CourrierIndividuelTemplate(PageTemplate):
PageTemplate.__init__(self, template_name, [content]) PageTemplate.__init__(self, template_name, [content])
self.logo_footer = Image( self.background_image_filename = None
image_dir + "/logo_footer.jpg", self.logo_footer = None
height=LOGO_FOOTER_HEIGHT, self.logo_header = None
width=LOGO_FOOTER_WIDTH, for suffix in LOGOS_IMAGES_ALLOWED_TYPES:
) if template_name == "PVJuryTemplate":
self.logo_header = Image( fn = image_dir + "/pvjury_background" + "." + suffix
image_dir + "/logo_header.jpg", else:
height=LOGO_HEADER_HEIGHT, fn = image_dir + "/letter_background" + "." + suffix
width=LOGO_HEADER_WIDTH, 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): def beforeDrawPage(self, canvas, doc):
"""Draws a logo and an contribution message on each page.""" """Draws a logo and an contribution message on each page."""
@ -201,8 +226,15 @@ class CourrierIndividuelTemplate(PageTemplate):
txt = SU(bm) txt = SU(bm)
canvas.bookmarkPage(key) canvas.bookmarkPage(key)
canvas.addOutlineEntry(txt, bm) 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( pageHeader(
canvas, canvas,
doc, doc,
@ -210,8 +242,7 @@ class CourrierIndividuelTemplate(PageTemplate):
self.preferences, self.preferences,
self.header_only_on_first_page, self.header_only_on_first_page,
) )
if self.force_footer or self.preferences["PV_LETTER_WITH_FOOTER"]: if self.with_footer:
# --- Add footer
pageFooter( pageFooter(
canvas, canvas,
doc, doc,
@ -222,8 +253,7 @@ class CourrierIndividuelTemplate(PageTemplate):
class PVTemplate(CourrierIndividuelTemplate): class PVTemplate(CourrierIndividuelTemplate):
"""Template pour les pages des PV de jury """Template pour les pages des PV de jury"""
"""
def __init__( def __init__(
self, self,
@ -231,10 +261,17 @@ class PVTemplate(CourrierIndividuelTemplate):
author=None, author=None,
title=None, title=None,
subject=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="", image_dir="",
preferences=None, # dictionnary with preferences, required 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__( CourrierIndividuelTemplate.__init__(
self, self,
document, document,
@ -250,6 +287,9 @@ class PVTemplate(CourrierIndividuelTemplate):
) )
self.with_page_numbers = True self.with_page_numbers = True
self.header_only_on_first_page = 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): def afterDrawPage(self, canvas, doc):
"""Called after all flowables have been drawn on a page""" """Called after all flowables have been drawn on a page"""
@ -331,7 +371,7 @@ def pdf_lettres_individuelles(
document = BaseDocTemplate(report) document = BaseDocTemplate(report)
image_dir = SCODOC_LOGOS_DIR + "/logos_" + context.DeptId() + "/" image_dir = SCODOC_LOGOS_DIR + "/logos_" + context.DeptId() + "/"
if not os.path.exists(image_dir): 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( document.addPageTemplates(
CourrierIndividuelTemplate( CourrierIndividuelTemplate(
document, document,
@ -437,9 +477,13 @@ def pdf_lettre_individuelle(sem, decision, etud, params, signature=None, context
s = "s" s = "s"
else: else:
s = "" s = ""
params["autorisations_txt"] = ( params[
"""Vous êtes autorisé%s à continuer dans le%s semestre%s : <b>%s</b>""" "autorisations_txt"
% (etud["ne"], s, s, decision["autorisations_descr"]) ] = """Vous êtes autorisé%s à continuer dans le%s semestre%s : <b>%s</b>""" % (
etud["ne"],
s,
s,
decision["autorisations_descr"],
) )
else: else:
params["autorisations_txt"] = "" 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( def pvjury_pdf(
context, context,
dpv, dpv,
@ -554,6 +595,7 @@ def pvjury_pdf(
anonymous=False, anonymous=False,
): ):
"""Doc PDF récapitulant les décisions de jury """Doc PDF récapitulant les décisions de jury
(tableau en format paysage)
dpv: result of dict_pvjury dpv: result of dict_pvjury
""" """
if not dpv: if not dpv:
@ -602,7 +644,7 @@ def pvjury_pdf(
document.pagesize = landscape(A4) document.pagesize = landscape(A4)
image_dir = SCODOC_LOGOS_DIR + "/logos_" + context.DeptId() + "/" image_dir = SCODOC_LOGOS_DIR + "/logos_" + context.DeptId() + "/"
if not os.path.exists(image_dir): 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( document.addPageTemplates(
PVTemplate( PVTemplate(
document, document,

View File

@ -156,15 +156,14 @@ def fmt_note(val, note_max=None, keep_numeric=False):
def fmt_coef(val): def fmt_coef(val):
"""Conversion valeur coefficient (float) en chaine """Conversion valeur coefficient (float) en chaine"""
"""
if val < 0.01: if val < 0.01:
return "%g" % val # unusually small value return "%g" % val # unusually small value
return "%g" % round(val, 2) return "%g" % round(val, 2)
def fmt_abs(val): 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 => NbAbs / Nb_justifiees
""" """
return "%s / %s" % (val[0], val[1]) return "%s / %s" % (val[0], val[1])
@ -194,7 +193,7 @@ GSL = thread.allocate_lock() # Global ScoDoc Lock
if "INSTANCE_HOME" in os.environ: if "INSTANCE_HOME" in os.environ:
# ----- Repertoire "var" (local) # ----- 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 # ----- Version information
SCODOC_VERSION_DIR = os.path.join(SCODOC_VAR_DIR, "config", "version") SCODOC_VERSION_DIR = os.path.join(SCODOC_VAR_DIR, "config", "version")
# ----- Repertoire tmp # ----- Repertoire tmp
@ -203,13 +202,12 @@ if "INSTANCE_HOME" in os.environ:
os.mkdir(SCO_TMPDIR, 0o755) os.mkdir(SCO_TMPDIR, 0o755)
# ----- Les logos: /opt/scodoc/var/scodoc/config/logos # ----- Les logos: /opt/scodoc/var/scodoc/config/logos
SCODOC_LOGOS_DIR = os.path.join(SCODOC_VAR_DIR, "config", "logos") SCODOC_LOGOS_DIR = os.path.join(SCODOC_VAR_DIR, "config", "logos")
# ----- Repertoire "config" (devrait s'appeler "tools"...) # ----- Repertoire "config" (devrait s'appeler "tools"...)
SCO_CONFIG_DIR = os.path.join( SCO_CONFIG_DIR = os.path.join(
os.environ["INSTANCE_HOME"], "Products", "ScoDoc", "config" os.environ["INSTANCE_HOME"], "Products", "ScoDoc", "config"
) )
# ----- Lecture du fichier de configuration # ----- Lecture du fichier de configuration
SCO_SRCDIR = os.path.split(VERSION.__file__)[0] SCO_SRCDIR = os.path.split(VERSION.__file__)[0]
@ -231,7 +229,9 @@ except:
raise raise
if hasattr(CONFIG, "CODES_EXPL"): 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: 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: # Valeurs utilisées pour affichage seulement, pas de requetes ni de mails envoyés:
SCO_WEBSITE = "https://scodoc.org" 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_ANNONCES_WEBSITE = "https://listes.univ-paris13.fr/mailman/listinfo/scodoc-annonces"
SCO_DEVEL_LIST = "scodoc-devel@listes.univ-paris13.fr" SCO_DEVEL_LIST = "scodoc-devel@listes.univ-paris13.fr"
SCO_USERS_LIST = "notes@listes.univ-paris13.fr" SCO_USERS_LIST = "notes@listes.univ-paris13.fr"
@ -296,6 +296,8 @@ PDF_MIMETYPE = "application/pdf"
XML_MIMETYPE = "text/xml" XML_MIMETYPE = "text/xml"
JSON_MIMETYPE = "application/json" JSON_MIMETYPE = "application/json"
LOGOS_IMAGES_ALLOWED_TYPES = ("jpg", "png") # remind that PIL does not read pdf
class DictDefault(dict): # obsolete, use collections.defaultdict class DictDefault(dict): # obsolete, use collections.defaultdict
"""A dictionnary with default value for all keys """A dictionnary with default value for all keys
@ -421,7 +423,7 @@ def simple_dictlist2xml(dictlist, doc=None, tagname=None, quote=False):
<ues note="10" /> <ues note="10" />
<ues /> <ues />
</infos> </infos>
""" """
if not tagname: if not tagname:
raise ValueError("invalid empty tagname !") raise ValueError("invalid empty tagname !")
@ -462,8 +464,7 @@ ALPHANUM_EXP = re.compile(r"^[\w-]+$", re.UNICODE)
def is_valid_code_nip(s): 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: if not s:
return False return False
return re.match(r"^[0-9]{6,32}$", s) return re.match(r"^[0-9]{6,32}$", s)
@ -492,7 +493,7 @@ def suppress_accents(s):
def sanitize_string(s): def sanitize_string(s):
"""s is an ordinary string, encoding given by SCO_ENCODING" """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) Irreversible (not a quote)
For ids and some filenames For ids and some filenames
@ -747,8 +748,7 @@ from sgmllib import SGMLParser
class html2txt_parser(SGMLParser): class html2txt_parser(SGMLParser):
"""html2txt() """html2txt()"""
"""
def reset(self): def reset(self):
"""reset() --> initialize the parser""" """reset() --> initialize the parser"""
@ -757,15 +757,15 @@ class html2txt_parser(SGMLParser):
def handle_data(self, text): def handle_data(self, text):
"""handle_data(text) --> appends the pieces to self.pieces """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) self.pieces.append(text)
def handle_entityref(self, ref): def handle_entityref(self, ref):
"""called for each entity reference, e.g. for "&copy;", ref will be """called for each entity reference, e.g. for "&copy;", ref will be
"copy" "copy"
Reconstruct the original entity reference. Reconstruct the original entity reference.
""" """
if ref == "amp": if ref == "amp":
self.pieces.append("&") self.pieces.append("&")
@ -794,7 +794,7 @@ def icontag(name, file_format="png", **attrs):
"""tag HTML pour un icone. """tag HTML pour un icone.
(dans les versions anterieures on utilisait Zope) (dans les versions anterieures on utilisait Zope)
Les icones sont des fichiers PNG dans .../static/icons 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). pour la mesurer (et cache le résultat).
""" """
if ("width" not in attrs) or ("height" not in attrs): if ("width" not in attrs) or ("height" not in attrs):