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
# 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

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",
},
),
(
"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])

View File

@ -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 : <b>%s</b>"""
% (etud["ne"], s, s, decision["autorisations_descr"])
params[
"autorisations_txt"
] = """Vous êtes autorisé%s à continuer dans le%s semestre%s : <b>%s</b>""" % (
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,

View File

@ -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):
<ues note="10" />
<ues />
</infos>
"""
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 "&copy;", 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):