From 5e461f7dd61bacd2896c16662ded2ab0c21353f8 Mon Sep 17 00:00:00 2001 From: Place Jean-Marie Date: Sun, 7 Nov 2021 08:53:44 +0100 Subject: [PATCH 01/19] =?UTF-8?q?Ecriture=20des=20fonctions=20d'acc=C3=A9s?= =?UTF-8?q?=20aux=20logos=20(et=20aux=20images)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/scodoc/sco_logos.py | 217 ++++++++++++++++++++++++++++++++-------- app/scodoc/sco_utils.py | 5 + 2 files changed, 180 insertions(+), 42 deletions(-) diff --git a/app/scodoc/sco_logos.py b/app/scodoc/sco_logos.py index e29b5183..0737e7f0 100644 --- a/app/scodoc/sco_logos.py +++ b/app/scodoc/sco_logos.py @@ -34,30 +34,188 @@ SCODOC_LOGOS_DIR /opt/scodoc-data/config/logos """ import imghdr import os +import re +from pathlib import Path -from flask import abort, current_app +from flask import abort, current_app, url_for +from werkzeug.utils import secure_filename +from app import Departement, ScoValueError from app.scodoc import sco_utils as scu +from PIL import Image as PILImage + +GLOBAL = "_GLOBAL" # category for server level logos -def get_logo_filename(logo_type: str, scodoc_dept: str) -> str: - """return full filename for this logo, or "" if not found - an existing file with extension. - logo_type: "header" or "footer" - scodoc-dept: acronym +def find_logo(logoname, dept_id=None, strict=False, prefix=scu.LOGO_FILE_PREFIX): """ - # Search logos in dept specific dir (/opt/scodoc-data/config/logos/logos_), - # then in config dir /opt/scodoc-data/config/logos/ - for image_dir in ( - scu.SCODOC_LOGOS_DIR + "/logos_" + scodoc_dept, - scu.SCODOC_LOGOS_DIR, # global logos - ): - for suffix in scu.LOGOS_IMAGES_ALLOWED_TYPES: - filename = os.path.join(image_dir, f"logo_{logo_type}.{suffix}") - if os.path.isfile(filename) and os.access(filename, os.R_OK): - return filename + "Recherche un logo 'name' existant. + Deux strategies: + si strict: + reherche uniquement dans le département puis si non trouvé au niveau global + sinon + On recherche en local au dept d'abord puis si pas trouvé recherche globale + quelque soit la stratégie, retourne None si pas trouvé + :param logoname: le nom recherche + :param dept_id: l'id du département dans lequel se fait la recherche (None si global) + :param strict: stratégie de recherche (strict = False => dept ou global) + :param prefix: le prefix utilisé (parmi scu.LOGO_FILE_PREFIX / scu.BACKGROUND_FILE_PREFIX) + :return: un objet Logo désignant le fichier image trouvé (ou None) + """ + logo = Logo(logoname, dept_id, prefix).select() + if logo is None and not strict: + logo = Logo(logoname=logoname, dept_id=None, prefix=prefix).select() + return logo - return "" + +def write_logo(stream, name, dept_id=None): + """Crée le fichier logo sur le serveur. + Le suffixe du fichier (parmi LOGO_IMAGES_ALLOWED_TYPES) est déduit du contenu du stream""" + Logo(logoname=name, dept_id=dept_id).create(stream) + + +def list_logos(): + """Crée l'inventaire de tous les logos existants. + L'inventaire se présente comme un dictionnaire de dictionnaire de Logo: + [GLOBAL][name] pour les logos globaux + [dept_id][name] pour les logos propres à un département (attention id numérique du dept) + """ + inventory = {GLOBAL: _list_dept_logos()} # logos globaux (header / footer) + for dept in Departement.query.filter_by(visible=True).all(): + logos_dept = _list_dept_logos(dept_id=dept.id) + if logos_dept: + inventory[dept.acronym] = _list_dept_logos(dept.id) + return inventory + + +def _list_dept_logos(dept_id=None, prefix=scu.LOGO_FILE_PREFIX): + """nventorie toutes les images existantes pour un niveau (GLOBAL ou un département). + retourne un dictionnaire de Logo [logoname] -> Logo + les noms des fichiers concernés doivent être de la forme: /. + : répertoire de recherche (déduit du dept_id) + : le prefix (LOGO_FILE_PREFIX pour les logos) + : un des suffixes autorisés + :param dept_id: l'id du departement concerné (si None -> global) + :param prefix: le préfixe utilisé + :return: le résultat de la recherche ou None si aucune image trouvée + """ + allowed_ext = "|".join(scu.LOGOS_IMAGES_ALLOWED_TYPES) + filename_parser = re.compile(f"{prefix}([^.]*).({allowed_ext})") + logos = {} + path_dir = Path(scu.SCODOC_LOGOS_DIR) + if dept_id: + path_dir = Path( + os.path.sep.join( + [scu.SCODOC_LOGOS_DIR, scu.LOGOS_DIR_PREFIX + str(dept_id)] + ) + ) + if path_dir.exists(): + for entry in path_dir.iterdir(): + if os.access(path_dir.joinpath(entry).absolute(), os.R_OK): + result = filename_parser.match(entry.name) + if result: + logoname = result.group(1) + logos[logoname] = Logo(logoname=logoname, dept_id=dept_id).select() + return logos if len(logos.keys()) > 0 else None + + +class Logo: + """Responsable des opérations (select, create), du calcul des chemins et url + ainsi que de la récupération des informations sur un logp. + Usage: + logo existant: Logo(, , ...).select() (retourne None si fichier non trouvé) + logo en création: Logo(, , ...).create(stream) + Les attributs filename, filepath, get_url() ne devraient pas être utilisés avant les opérations + select ou save (le format n'est pas encore connu à ce moement là) + """ + + def __init__(self, logoname, dept_id=None, prefix=scu.LOGO_FILE_PREFIX): + """Initialisation des noms et département des logos. + if prefix = None on recherche simplement une image 'logoname.*' + Le format est renseigné au moment de la lecture (select) ou de la création (create) de l'objet + """ + self.logoname = secure_filename(logoname) + self.scodoc_dept_id = dept_id + self.prefix = prefix or "" + self.suffix = None + self.dimensions = None + if self.scodoc_dept_id: + self.dirpath = os.path.sep.join( + [ + scu.SCODOC_LOGOS_DIR, + scu.LOGOS_DIR_PREFIX + secure_filename(str(dept_id)), + ] + ) + else: + self.dirpath = scu.SCODOC_LOGOS_DIR + self.basepath = os.path.sep.join( + [self.dirpath, self.prefix + secure_filename(self.logoname)] + ) + self.filepath = None + self.filename = None + + def _set_format(self, fmt): + self.suffix = fmt + self.filepath = self.basepath + "." + fmt + self.filename = self.logoname + "." + fmt + + def _ensure_directory_exists(self): + "create enclosing directory if necessary" + if not Path(self.dirpath).exists(): + current_app.logger.info(f"sco_logos creating directory %s", self.dirpath) + os.mkdir(self.dirpath) + + def create(self, stream): + img_type = guess_image_type(stream) + if img_type not in scu.LOGOS_IMAGES_ALLOWED_TYPES: + abort(400, "type d'image invalide") + self._set_format(img_type) + self._ensure_directory_exists() + filename = self.basepath + "." + self.suffix + with open(filename, "wb") as f: + f.write(stream.read()) + current_app.logger.info(f"sco_logos.store_image %s", self.filename) + # erase other formats if they exists + for suffix in set(scu.LOGOS_IMAGES_ALLOWED_TYPES) - set([img_type]): + try: + os.unlink(self.basepath + "." + suffix) + except IOError: + pass + + def select(self): + """ + Récupération des données pour un logo existant + il doit exister un et un seul fichier image parmi de suffixe/types autorisés + (sinon on prend le premier trouvé) + cette opération permet d'affiner le format d'un logo de format inconnu + """ + for suffix in scu.LOGOS_IMAGES_ALLOWED_TYPES: + path = Path(self.basepath + "." + suffix) + if path.exists(): + self._set_format(suffix) + with open(self.filepath, "rb") as f: + img = PILImage.open(f) + self.dimensions = img.size + return self + return None + + def get_url(self): + """Retourne l'URL permettant d'obtenir l'image du logo""" + return url_for( + "scodoc.get_logo", + scodoc_dept=self.scodoc_dept_id, + name=self.logoname, + global_if_not_found=False, + ) + + def get_url_small(self): + """Retourne l'URL permettant d'obtenir l'image du logo sous forme de miniature""" + return url_for( + "scodoc.get_logo_small", + scodoc_dept=self.scodoc_dept_id, + name=self.logoname, + global_if_not_found=False, + ) def guess_image_type(stream) -> str: @@ -68,28 +226,3 @@ def guess_image_type(stream) -> str: if not fmt: return None return fmt if fmt != "jpeg" else "jpg" - - -def _ensure_directory_exists(filename): - "create enclosing directory if necessary" - directory = os.path.split(filename)[0] - if not os.path.exists(directory): - current_app.logger.info(f"sco_logos creating directory %s", directory) - os.mkdir(directory) - - -def store_image(stream, basename): - img_type = guess_image_type(stream) - if img_type not in scu.LOGOS_IMAGES_ALLOWED_TYPES: - abort(400, "type d'image invalide") - filename = basename + "." + img_type - _ensure_directory_exists(filename) - with open(filename, "wb") as f: - f.write(stream.read()) - current_app.logger.info(f"sco_logos.store_image %s", filename) - # erase other formats if they exists - for extension in set(scu.LOGOS_IMAGES_ALLOWED_TYPES) - set([img_type]): - try: - os.unlink(basename + "." + extension) - except IOError: - pass diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index 861e9487..c023c738 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -283,7 +283,12 @@ if not os.path.exists(SCO_TMP_DIR) and os.path.exists(Config.SCODOC_VAR_DIR): # ----- Les logos: /opt/scodoc-data/config/logos SCODOC_LOGOS_DIR = os.path.join(SCODOC_CFG_DIR, "logos") LOGOS_IMAGES_ALLOWED_TYPES = ("jpg", "jpeg", "png") # remind that PIL does not read pdf +LOGOS_DIR_PREFIX = "logos_" +LOGO_FILE_PREFIX = "logo_" +# forme générale des noms des fichiers logos/background: +# SCODOC_LOGO_DIR/LOGO_FILE_PREFIX. (fichier global) ou +# SCODOC_LOGO_DIR/LOGOS_DIR_PREFIX/LOGO_FILE_PREFIX. (fichier départemental) # ----- Les outils distribués SCO_TOOLS_DIR = os.path.join(Config.SCODOC_DIR, "tools") From d7e34b8ce271c6dfb1657d96dbb22a4ef79a8719 Mon Sep 17 00:00:00 2001 From: Place Jean-Marie Date: Sun, 7 Nov 2021 09:02:53 +0100 Subject: [PATCH 02/19] =?UTF-8?q?adaptation=20de=20la=20sauvegarde=20des?= =?UTF-8?q?=20fichiers=20logos=20poursuites=20d'=C3=A9tudes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/pe/pe_tools.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/app/pe/pe_tools.py b/app/pe/pe_tools.py index 46e706ee..99adbedd 100644 --- a/app/pe/pe_tools.py +++ b/app/pe/pe_tools.py @@ -44,6 +44,7 @@ import unicodedata import app.scodoc.sco_utils as scu from app import log +from app.scodoc.sco_logos import find_logo PE_DEBUG = 0 @@ -201,11 +202,11 @@ def add_pe_stuff_to_zip(zipfile, ziproot): add_local_file_to_zip(zipfile, ziproot, pathname, "avis/" + filename) # Logos: (add to logos/ directory in zip) - logos_names = ["logo_header.jpg", "logo_footer.jpg"] - for f in logos_names: - logo = os.path.join(scu.SCODOC_LOGOS_DIR, f) - if os.path.isfile(logo): - add_local_file_to_zip(zipfile, ziproot, logo, "avis/logos/" + f) + logos_names = ["header", "footer"] + for name in logos_names: + logo = find_logo(logoname=name, dept_id=g.scodoc_dept_id) + if logo is not None: + add_local_file_to_zip(zipfile, ziproot, logo, "avis/logos/" + logo.filename) # ---------------------------------------------------------------------------------------- From ecd637fb39273c3dffc14278c7526f71c768fdcd Mon Sep 17 00:00:00 2001 From: Place Jean-Marie Date: Sun, 7 Nov 2021 09:05:53 +0100 Subject: [PATCH 03/19] =?UTF-8?q?adaptation=20du=20code=20d'=C3=A9dition?= =?UTF-8?q?=20des=20pv?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/scodoc/sco_pvpdf.py | 54 ++++++++++++++++++++++------------------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/app/scodoc/sco_pvpdf.py b/app/scodoc/sco_pvpdf.py index 41dce9ba..b8664c51 100644 --- a/app/scodoc/sco_pvpdf.py +++ b/app/scodoc/sco_pvpdf.py @@ -52,6 +52,7 @@ from app.scodoc import sco_pdf from app.scodoc import sco_preferences from app.scodoc import sco_etud import sco_version +from app.scodoc.sco_logos import find_logo from app.scodoc.sco_pdf import PDFLOCK from app.scodoc.sco_pdf import SU @@ -201,33 +202,36 @@ class CourrierIndividuelTemplate(PageTemplate): self.logo_footer = None self.logo_header = None # Search logos in dept specific dir, then in global scu.CONFIG dir - for image_dir in ( - scu.SCODOC_LOGOS_DIR + "/logos_" + g.scodoc_dept, - scu.SCODOC_LOGOS_DIR, # global logos - ): - for suffix in scu.LOGOS_IMAGES_ALLOWED_TYPES: - if template_name == "PVJuryTemplate": - fn = image_dir + "/pvjury_background" + "." + suffix - else: - fn = image_dir + "/letter_background" + "." + suffix - if not self.background_image_filename and os.path.exists(fn): - self.background_image_filename = fn + if template_name == "PVJuryTemplate": + background = find_logo( + logoname="pvjury_background", + dept_id=g.scodoc_dept_id, + prefix="", + ) + else: + background = find_logo( + logoname="letter_background", + dept_id=g.scodoc_dept_id, + prefix="", + ) + if not self.background_image_filename and background is not None: + self.background_image_filename = background.filepath - fn = image_dir + "/logo_footer" + "." + suffix - if not self.logo_footer and os.path.exists(fn): - self.logo_footer = Image( - fn, - height=LOGO_FOOTER_HEIGHT, - width=LOGO_FOOTER_WIDTH, - ) + footer = find_logo(logoname="footer", dept_id=g.scodoc_dept_id) + if footer is not None: + self.logo_footer = Image( + footer.filepath, + height=LOGO_FOOTER_HEIGHT, + width=LOGO_FOOTER_WIDTH, + ) - fn = image_dir + "/logo_header" + "." + suffix - if not self.logo_header and os.path.exists(fn): - self.logo_header = Image( - fn, - height=LOGO_HEADER_HEIGHT, - width=LOGO_HEADER_WIDTH, - ) + header = find_logo(logoname="header", dept_id=g.scodoc_dept_id) + if header is not None: + self.logo_header = Image( + header.filepath, + height=LOGO_HEADER_HEIGHT, + width=LOGO_HEADER_WIDTH, + ) def beforeDrawPage(self, canvas, doc): """Draws a logo and an contribution message on each page.""" From f219b8d0034257354b2ff9aced9c786621d5d5ed Mon Sep 17 00:00:00 2001 From: Place Jean-Marie Date: Sun, 7 Nov 2021 09:07:51 +0100 Subject: [PATCH 04/19] adaptation de code de traitement des templates pdf --- app/scodoc/sco_pdf.py | 31 ++++++++++++------------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/app/scodoc/sco_pdf.py b/app/scodoc/sco_pdf.py index 77e6f4e1..54ed29b2 100755 --- a/app/scodoc/sco_pdf.py +++ b/app/scodoc/sco_pdf.py @@ -60,11 +60,8 @@ from reportlab.lib.pagesizes import letter, A4, landscape from flask import g import app.scodoc.sco_utils as scu -from app.scodoc.sco_utils import ( - CONFIG, - SCODOC_LOGOS_DIR, - LOGOS_IMAGES_ALLOWED_TYPES, -) +from app.scodoc.sco_logos import find_logo +from app.scodoc.sco_utils import CONFIG from app import log from app.scodoc.sco_exceptions import ScoGenError, ScoValueError import sco_version @@ -219,20 +216,16 @@ class ScolarsPageTemplate(PageTemplate): ) PageTemplate.__init__(self, "ScolarsPageTemplate", [content]) self.logo = None - # XXX COPIED from sco_pvpdf, to be refactored (no time now) - # Search background in dept specific dir, then in global config dir - for image_dir in ( - SCODOC_LOGOS_DIR + "/logos_" + g.scodoc_dept + "/", - SCODOC_LOGOS_DIR + "/", # global logos - ): - for suffix in LOGOS_IMAGES_ALLOWED_TYPES: - fn = image_dir + "/bul_pdf_background" + "." + suffix - if not self.background_image_filename and os.path.exists(fn): - self.background_image_filename = fn - # Also try to use PV background - fn = image_dir + "/letter_background" + "." + suffix - if not self.background_image_filename and os.path.exists(fn): - self.background_image_filename = fn + logo = find_logo( + logoname="bul_pdf_background", dept_id=g.scodoc_dept_id, prefix=None + ) + if logo is None: + # Also try to use PV background + logo = find_logo( + logoname="letter_background", dept_id=g.scodoc_dept_id, prefix=None + ) + if logo is not None: + self.background_image_filename = logo.filepath def beforeDrawPage(self, canvas, doc): """Draws (optional) background, logo and contribution message on each page. From b3e1659049c03c21edbe1087a7bacaab41830b85 Mon Sep 17 00:00:00 2001 From: Place Jean-Marie Date: Sun, 7 Nov 2021 09:11:40 +0100 Subject: [PATCH 05/19] =?UTF-8?q?adaptation=20du=20code=20de=20traitement?= =?UTF-8?q?=20des=20balises=20=20des=20=C3=A9ditions=20param?= =?UTF-8?q?=C3=A9tr=C3=A9es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/scodoc/sco_bulletins_pdf.py | 42 +++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/app/scodoc/sco_bulletins_pdf.py b/app/scodoc/sco_bulletins_pdf.py index 81df5b1e..b28e9db5 100644 --- a/app/scodoc/sco_bulletins_pdf.py +++ b/app/scodoc/sco_bulletins_pdf.py @@ -51,23 +51,24 @@ Chaque semestre peut si nécessaire utiliser un type de bulletin différent. """ import io -import os import re import time import traceback +from pydoc import html from reportlab.platypus.doctemplate import PageTemplate, BaseDocTemplate -from flask import g, url_for, request +from flask import g, request import app.scodoc.sco_utils as scu -from app import log +from app import log, ScoValueError from app.scodoc import sco_cache from app.scodoc import sco_formsemestre from app.scodoc import sco_pdf from app.scodoc import sco_preferences from app.scodoc import sco_etud import sco_version +from app.scodoc.sco_logos import find_logo def pdfassemblebulletins( @@ -110,6 +111,17 @@ def pdfassemblebulletins( return data +def replacement_function(match): + balise = match.group(1) + name = match.group(3) + logo = find_logo(logoname=name, dept_id=g.scodoc_dept_id) + 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)) + ) + + def process_field(field, cdict, style, suppress_empty_pars=False, format="pdf"): """Process a field given in preferences, returns - if format = 'pdf': a list of Platypus objects @@ -141,24 +153,18 @@ def process_field(field, cdict, style, suppress_empty_pars=False, format="pdf"): return text # --- PDF format: # handle logos: - image_dir = scu.SCODOC_LOGOS_DIR + "/logos_" + g.scodoc_dept + "/" - if not os.path.exists(image_dir): - image_dir = scu.SCODOC_LOGOS_DIR + "/" # use global logos - if not os.path.exists(image_dir): - log(f"Warning: missing global logo directory ({image_dir})") - image_dir = None - text = re.sub( r"<(\s*)logo(.*?)src\s*=\s*(.*?)>", r"<\1logo\2\3>", text ) # remove forbidden src attribute - if image_dir is not None: - text = re.sub( - r'<\s*logo(.*?)name\s*=\s*"(\w*?)"(.*?)/?>', - r'' % image_dir, - text, - ) - # nota: le match sur \w*? donne le nom du logo et interdit les .. et autres - # tentatives d'acceder à d'autres fichiers ! + text = re.sub( + r'(<\s*logo(.*?)name\s*=\s*"(\w*?)"(.*?)/?>)', + replacement_function, + text, + ) + # nota: le match sur \w*? donne le nom du logo et interdit les .. et autres + # tentatives d'acceder à d'autres fichiers ! + # la protection contre des noms malveillants est aussi assurée par l'utilisation de + # secure_filename dans la classe Logo # log('field: %s' % (text)) return sco_pdf.makeParas(text, style, suppress_empty=suppress_empty_pars) From 29c9982afc13cf456dc1f6c1f315829cd0245890 Mon Sep 17 00:00:00 2001 From: Place Jean-Marie Date: Sun, 7 Nov 2021 09:13:24 +0100 Subject: [PATCH 06/19] adaptation du template d'affichage des images (maintenant miniatures) --- app/templates/configuration.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/templates/configuration.html b/app/templates/configuration.html index 6dcf1c51..88f1fd81 100644 --- a/app/templates/configuration.html +++ b/app/templates/configuration.html @@ -36,12 +36,12 @@ From b336a1c1a26abc7857b67df992a05052e831b5fe Mon Sep 17 00:00:00 2001 From: Place Jean-Marie Date: Sun, 7 Nov 2021 09:19:27 +0100 Subject: [PATCH 07/19] =?UTF-8?q?ajout=20URL=20de=20r=C3=A9cup=C3=A9raions?= =?UTF-8?q?=20des=20logos=20(deprecates=20/ScoDoc/logo=5Fheader=20et=20/Sc?= =?UTF-8?q?oDoc/logo=5Ffooter)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/views/scodoc.py | 93 +++++++++++++++++++++++++++++++++------------ 1 file changed, 69 insertions(+), 24 deletions(-) diff --git a/app/views/scodoc.py b/app/views/scodoc.py index c8ea5aad..0def371c 100644 --- a/app/views/scodoc.py +++ b/app/views/scodoc.py @@ -30,6 +30,8 @@ Module main: page d'accueil, avec liste des départements Emmanuel Viennet, 2021 """ +import io + from app.auth.models import User import os @@ -38,7 +40,7 @@ from flask import abort, flash, url_for, redirect, render_template, send_file from flask import request from flask.app import Flask import flask_login -from flask_login.utils import login_required +from flask_login.utils import login_required, current_user from flask_wtf import FlaskForm from flask_wtf.file import FileField, FileAllowed from werkzeug.exceptions import BadRequest, NotFound @@ -65,6 +67,8 @@ from app.scodoc.sco_exceptions import AccessDenied from app.scodoc.sco_permissions import Permission from app.views import scodoc_bp as bp +from PIL import Image as PILImage + @bp.route("/") @bp.route("/ScoDoc") @@ -238,13 +242,9 @@ def configuration(): if form.validate_on_submit(): ScoDocSiteConfig.set_bonus_sport_func(form.bonus_sport_func_name.data) if form.logo_header.data: - sco_logos.store_image( - form.logo_header.data, os.path.join(scu.SCODOC_LOGOS_DIR, "logo_header") - ) + sco_logos.write_logo(stream=form.logo_header.data, name="header") if form.logo_footer.data: - sco_logos.store_image( - form.logo_footer.data, os.path.join(scu.SCODOC_LOGOS_DIR, "logo_footer") - ) + sco_logos.write_logo(stream=form.logo_footer.data, name="footer") app.clear_scodoc_cache() flash(f"Configuration enregistrée") return redirect(url_for("scodoc.index")) @@ -257,29 +257,74 @@ def configuration(): ) -def _return_logo(logo_type="header", scodoc_dept=""): +SMALL_SIZE = (300, 300) + + +def _return_logo(name="header", dept_id="", small=False, strict: bool = True): # stockée dans /opt/scodoc-data/config/logos donc servie manuellement ici - filename = sco_logos.get_logo_filename(logo_type, scodoc_dept) - if filename: - extension = os.path.splitext(filename)[1] - return send_file(filename, mimetype=f"image/{extension}") + logo = sco_logos.find_logo(name, dept_id, strict) + if logo is not None: + suffix = logo.suffix + if small: + with PILImage.open(logo.filepath) as im: + im.thumbnail(SMALL_SIZE) + stream = io.BytesIO() + # on garde le même format (on pourrait plus simplement générer systématiquement du JPEG) + fmt = { # adapt suffix to be compliant with PIL save format + "PNG": "PNG", + "JPG": "JPEG", + "JPEG": "JPEG", + }[suffix.upper()] + im.save(stream, fmt) + stream.seek(0) + return send_file(stream, mimetype=f"image/{fmt}") + else: + return send_file(logo.filepath, mimetype=f"image/{suffix}") else: - return "" + abort(404) -@bp.route("/ScoDoc/logo_header") -@bp.route("/ScoDoc//logo_header") -def logo_header(scodoc_dept=""): - "Image logo header" - # "/opt/scodoc-data/config/logos/logo_header") - return _return_logo(logo_type="header", scodoc_dept=scodoc_dept) +# small version (copy/paste from get_logo +@bp.route("/ScoDoc/logos//small", defaults={"dept_id": None}) +@bp.route("/ScoDoc//logos//small") +@admin_required +def get_logo_small(name: str, dept_id: int): + strict = request.args.get("strict", "False") + return _return_logo( + name, + dept_id=dept_id, + small=True, + strict=strict.upper() not in ["0", "FALSE"], + ) -@bp.route("/ScoDoc/logo_footer") -@bp.route("/ScoDoc//logo_footer") -def logo_footer(scodoc_dept=""): - "Image logo footer" - return _return_logo(logo_type="footer", scodoc_dept=scodoc_dept) +@bp.route( + "/ScoDoc/logos/", defaults={"dept_id": None} +) # if dept not specified, take global logo +@bp.route("/ScoDoc//logos/") +@admin_required +def get_logo(name: str, dept_id: int): + strict = request.args.get("strict", "False") + return _return_logo( + name, + dept_id=dept_id, + small=False, + strict=strict.upper() not in ["0", "FALSE"], + ) + + +# @bp.route("/ScoDoc/logo_header") +# @bp.route("/ScoDoc//logo_header") +# def logo_header(scodoc_dept=""): +# "Image logo header" +# return _return_logo(name="header", scodoc_dept=scodoc_dept) + + +# @bp.route("/ScoDoc/logo_footer") +# @bp.route("/ScoDoc//logo_footer") +# def logo_footer(scodoc_dept=""): +# "Image logo footer" +# return _return_logo(name="footer", scodoc_dept=scodoc_dept) # essais From df3439351d60f1ba6abcb4df3c66784c7a9a02c8 Mon Sep 17 00:00:00 2001 From: Place Jean-Marie Date: Tue, 9 Nov 2021 08:21:52 +0100 Subject: [PATCH 08/19] ajout tests logos --- app/scodoc/sco_logos.py | 41 +++- tests/ressources/test_logos/logo_A.jpg | Bin 0 -> 39157 bytes tests/ressources/test_logos/logo_C.jpg | Bin 0 -> 2417 bytes tests/ressources/test_logos/logo_D.png | Bin 0 -> 15701 bytes tests/ressources/test_logos/logo_E.jpg | Bin 0 -> 2935 bytes tests/ressources/test_logos/logo_F.jpeg | Bin 0 -> 2882 bytes .../ressources/test_logos/logos_1/logo_A.jpg | Bin 0 -> 2730 bytes .../ressources/test_logos/logos_1/logo_B.jpg | Bin 0 -> 2832 bytes .../ressources/test_logos/logos_2/logo_A.jpg | Bin 0 -> 2703 bytes tests/unit/test_logos.py | 183 ++++++++++++++++++ 10 files changed, 219 insertions(+), 5 deletions(-) create mode 100644 tests/ressources/test_logos/logo_A.jpg create mode 100644 tests/ressources/test_logos/logo_C.jpg create mode 100644 tests/ressources/test_logos/logo_D.png create mode 100644 tests/ressources/test_logos/logo_E.jpg create mode 100644 tests/ressources/test_logos/logo_F.jpeg create mode 100644 tests/ressources/test_logos/logos_1/logo_A.jpg create mode 100644 tests/ressources/test_logos/logos_1/logo_B.jpg create mode 100644 tests/ressources/test_logos/logos_2/logo_A.jpg create mode 100644 tests/unit/test_logos.py diff --git a/app/scodoc/sco_logos.py b/app/scodoc/sco_logos.py index 0737e7f0..dba5dc61 100644 --- a/app/scodoc/sco_logos.py +++ b/app/scodoc/sco_logos.py @@ -137,8 +137,6 @@ class Logo: self.logoname = secure_filename(logoname) self.scodoc_dept_id = dept_id self.prefix = prefix or "" - self.suffix = None - self.dimensions = None if self.scodoc_dept_id: self.dirpath = os.path.sep.join( [ @@ -151,8 +149,16 @@ class Logo: self.basepath = os.path.sep.join( [self.dirpath, self.prefix + secure_filename(self.logoname)] ) - self.filepath = None - self.filename = None + # next attributes are computer by the select function + self.suffix = "Not inited: call the select or create function before access" + self.filepath = "Not inited: call the select or create function before access" + self.filename = "Not inited: call the select or create function before access" + self.size = "Not inited: call the select or create function before access" + self.aspect_ratio = ( + "Not inited: call the select or create function before access" + ) + self.density = "Not inited: call the select or create function before access" + self.cm = "Not inited: call the select or create function before access" def _set_format(self, fmt): self.suffix = fmt @@ -182,6 +188,31 @@ class Logo: except IOError: pass + def _read_info(self, img): + """computes some properties from the real image + aspect_ratio assumes that x_density and y_density are equals + """ + x_size, y_size = img.size + self.density = img.info.get("dpi", None) + unit = 1 + if self.density is None: # no dpi found try jfif infos + self.density = img.info.get("jfif_density", None) + unit = img.info.get("jfif_unit", 0) # 0 = no unit ; 1 = inch ; 2 = cm + if self.density is not None: + x_density, y_density = self.density + if unit != 0: + unit2cm = [0, 1 / 2.54, 1][unit] + x_cm = round(x_size * unit2cm / x_density, 2) + y_cm = round(y_size * unit2cm / y_density, 2) + self.cm = (x_cm, y_cm) + else: + self.cm = None + else: + self.cm = None + + self.size = (x_size, y_size) + self.aspect_ratio = float(x_size) / y_size + def select(self): """ Récupération des données pour un logo existant @@ -195,7 +226,7 @@ class Logo: self._set_format(suffix) with open(self.filepath, "rb") as f: img = PILImage.open(f) - self.dimensions = img.size + self._read_info(img) return self return None diff --git a/tests/ressources/test_logos/logo_A.jpg b/tests/ressources/test_logos/logo_A.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7f107a05b6075d40551a7f7e695b06ec9f0763bf GIT binary patch literal 39157 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

N>>8#`X%9MS21@~^ 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 Hu0mjfEZ>RB literal 0 HcmV?d00001 diff --git a/tests/ressources/test_logos/logo_E.jpg b/tests/ressources/test_logos/logo_E.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e97b78d07f6c1aa25929af8eaf478fcef68a2e5a GIT binary patch literal 2935 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 F{{>~3qf!6> literal 0 HcmV?d00001 diff --git a/tests/ressources/test_logos/logo_F.jpeg b/tests/ressources/test_logos/logo_F.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..c3750e29f2e44516d028f96c0a1a3ce181a42f63 GIT binary patch literal 2882 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 literal 0 HcmV?d00001 diff --git a/tests/ressources/test_logos/logos_1/logo_B.jpg b/tests/ressources/test_logos/logos_1/logo_B.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2ebb82823e3db7f757b5d29322b1e9efb0db2c94 GIT binary patch literal 2832 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 Xrl?*&eLha9q*r$u@mGi+S9|{lD=b$E literal 0 HcmV?d00001 diff --git a/tests/unit/test_logos.py b/tests/unit/test_logos.py new file mode 100644 index 00000000..8cf2e275 --- /dev/null +++ b/tests/unit/test_logos.py @@ -0,0 +1,183 @@ +# -*- coding: utf-8 -*- + +"""Test Logos + + +Utiliser comme: + pytest tests/unit/test_logos.py + +""" +from io import BytesIO +from pathlib import Path +from shutil import copytree, copy, rmtree + +import pytest as pytest +from _pytest.python_api import approx + +import app +from app import db +from app.models import Departement +import app.scodoc.sco_utils as scu +from app.scodoc.sco_logos import find_logo, Logo, list_logos + +RESOURCES_DIR = "/opt/scodoc/tests/ressources/test_logos" + + +@pytest.fixture +def create_dept(test_client): + """Crée 2 départements: + return departements object + """ + dept1 = Departement(acronym="RT") + dept2 = Departement(acronym="INFO") + db.session.add(dept1) + db.session.add(dept2) + db.session.commit() + yield dept1, dept2 + db.session.delete(dept1) + db.session.delete(dept2) + db.session.commit() + + +@pytest.fixture +def create_logos(create_dept): + """Crée les logos: + ...logos --+-- logo_A.jpg + +-- logo_C.jpg + +-- logo_D.png + +-- logo_E.jpg + +-- logo_F.jpeg + +-- logos_{d1} --+-- logo_A.jpg + | +-- logo_B.jpg + +-- logos_{d2} --+-- logo_A.jpg + + """ + dept1, dept2 = create_dept + d1 = dept1.id + d2 = dept2.id + FILE_LIST = ["logo_A.jpg", "logo_C.jpg", "logo_D.png", "logo_E.jpg", "logo_F.jpeg"] + for fn in FILE_LIST: + from_path = Path(RESOURCES_DIR).joinpath(fn) + to_path = Path(scu.SCODOC_LOGOS_DIR).joinpath(fn) + copy(from_path.absolute(), to_path.absolute()) + copytree( + f"{RESOURCES_DIR}/logos_1", + f"{scu.SCODOC_LOGOS_DIR}/logos_{d1}", + ) + copytree( + f"{RESOURCES_DIR}/logos_2", + f"{scu.SCODOC_LOGOS_DIR}/logos_{d2}", + ) + yield None + rmtree(f"{scu.SCODOC_LOGOS_DIR}/logos_{d1}") + rmtree(f"{scu.SCODOC_LOGOS_DIR}/logos_{d2}") + # rm files + for fn in FILE_LIST: + to_path = Path(scu.SCODOC_LOGOS_DIR).joinpath(fn) + to_path.unlink() + + +def test_select_global_only(create_logos): + C_logo = app.scodoc.sco_logos.find_logo(logoname="C") + assert C_logo.filepath == f"{scu.SCODOC_LOGOS_DIR}/logo_C.jpg" + + +def test_select_local_only(create_dept, create_logos): + dept1, dept2 = create_dept + B_logo = app.scodoc.sco_logos.find_logo(logoname="B", dept_id=dept1.id) + assert B_logo.filepath == f"{scu.SCODOC_LOGOS_DIR}/logos_{dept1.id}/logo_B.jpg" + + +def test_select_local_override_global(create_dept, create_logos): + dept1, dept2 = create_dept + A1_logo = app.scodoc.sco_logos.find_logo(logoname="A", dept_id=dept1.id) + assert A1_logo.filepath == f"{scu.SCODOC_LOGOS_DIR}/logos_{dept1.id}/logo_A.jpg" + + +def test_select_global_with_strict(create_dept, create_logos): + dept1, dept2 = create_dept + A_logo = app.scodoc.sco_logos.find_logo(logoname="A", dept_id=dept1.id, strict=True) + assert A_logo.filepath == f"{scu.SCODOC_LOGOS_DIR}/logos_{dept1.id}/logo_A.jpg" + + +def test_looks_for_non_existant_should_give_none(create_dept, create_logos): + # search for a local non-existant logo returns None + dept1, dept2 = create_dept + no_logo = app.scodoc.sco_logos.find_logo(logoname="Z", dept_id=dept1.id) + assert no_logo is None + + +def test_looks_localy_for_a_global_should_give_none(create_dept, create_logos): + # search for a local non-existant logo returns None + dept1, dept2 = create_dept + no_logo = app.scodoc.sco_logos.find_logo( + logoname="C", dept_id=dept1.id, strict=True + ) + assert no_logo is None + + +def test_get_jpg_data(create_dept, create_logos): + logo = find_logo("A", dept_id=None) + assert logo is not None + logo.select() + assert logo.logoname == "A" + assert logo.suffix == "jpg" + assert logo.filename == "A.jpg" + assert logo.size == (1200, 600) + assert logo.cm == approx((4.0, 3.0), 0.01) + + +def test_get_png_without_data(create_dept, create_logos): + logo = find_logo("D", dept_id=None) + assert logo is not None + logo.select() + assert logo.logoname == "D" + assert logo.suffix == "png" + assert logo.filename == "D.png" + assert logo.size == (121, 121) + assert logo.density is None + assert logo.cm is None + + +def test_create_globale_jpg_logo(create_dept, create_logos): + path = Path(f"{RESOURCES_DIR}/logo_C.jpg") + logo = Logo("X") # create global logo + stream = path.open("rb") + + +def test_create_jpg_instead_of_png_logo(create_dept, create_logos): + # action + logo = Logo("D") # create global logo (replace logo_D.png) + path = Path(f"{RESOURCES_DIR}/logo_C.jpg") + stream = path.open("rb") + logo.create(stream) + # test + created = Path(f"{scu.SCODOC_LOGOS_DIR}/logo_D.jpg") + removed = Path(f"{scu.SCODOC_LOGOS_DIR}/logo_D.png") + # file system check + assert created.exists() + assert not removed.exists() + # logo check + logo = find_logo("D") + assert logo is not None + assert logo.filepath == f"{scu.SCODOC_LOGOS_DIR}/logo_D.jpg" # created.absolute() + # restore initial state + original = Path(f"{RESOURCES_DIR}/logo_D.png") + copy(original, removed) + created.unlink(missing_ok=True) + + +def test_list_logo(create_dept, create_logos): + # test only existence of copied logos. We assumes that they are OK + dept1, dept2 = create_dept + logos = list_logos() + assert logos.keys() == {"_GLOBAL", "RT", "INFO"} + assert {"A", "C", "D", "E", "F", "header", "footer"}.issubset( + set(logos["_GLOBAL"].keys()) + ) + rt = logos.get("RT", None) + assert rt is not None + assert {"A", "B"}.issubset(set(rt.keys())) + info = logos.get("INFO", None) + assert info is not None + assert {"A"}.issubset(set(rt.keys())) From 51506c6d6f61a2c5f017f181584138660a26314f Mon Sep 17 00:00:00 2001 From: Place Jean-Marie Date: Sun, 14 Nov 2021 10:33:37 +0100 Subject: [PATCH 09/19] migration des logos_dept scodoc7 -> scodoc9 --- scodoc.py | 9 ++++++ tools/__init__.py | 1 + tools/migrate_from_scodoc7.sh | 3 ++ tools/migrate_scodoc7_logos.py | 53 ++++++++++++++++++++++++++++++++++ 4 files changed, 66 insertions(+) create mode 100644 tools/migrate_scodoc7_logos.py diff --git a/scodoc.py b/scodoc.py index cfea19d7..6b34d941 100755 --- a/scodoc.py +++ b/scodoc.py @@ -22,6 +22,7 @@ from app import models from app.auth.models import User, Role, UserRole from app.models import ScoPreference +from app.scodoc.sco_logos import make_logo_local from app.models import Formation, UniteEns, Module from app.models import FormSemestre, FormsemestreInscription from app.models import ModuleImpl, ModuleImplInscription @@ -340,6 +341,14 @@ def migrate_scodoc7_dept_archives(dept: str): # migrate-scodoc7-dept-archives tools.migrate_scodoc7_dept_archives(dept) +@app.cli.command() +@click.argument("dept", default="") +@with_appcontext +def migrate_scodoc7_dept_logos(dept: str = ""): # migrate-scodoc7-dept-logos + """Post-migration: renomme les logos en fonction des id / dept de ScoDoc 9""" + tools.migrate_scodoc7_dept_logos(dept) + + @app.cli.command() @click.argument("formsemestre_id", type=click.INT) @click.argument("xlsfile", type=click.File("rb")) diff --git a/tools/__init__.py b/tools/__init__.py index 7ed85cae..ac9e681c 100644 --- a/tools/__init__.py +++ b/tools/__init__.py @@ -7,3 +7,4 @@ from tools.import_scodoc7_user_db import import_scodoc7_user_db from tools.import_scodoc7_dept import import_scodoc7_dept from tools.migrate_scodoc7_archives import migrate_scodoc7_dept_archives +from tools.migrate_scodoc7_logos import migrate_scodoc7_dept_logos diff --git a/tools/migrate_from_scodoc7.sh b/tools/migrate_from_scodoc7.sh index 59c0cee8..7d4132ca 100755 --- a/tools/migrate_from_scodoc7.sh +++ b/tools/migrate_from_scodoc7.sh @@ -274,6 +274,9 @@ done # ----- Post-Migration: renomme archives en fonction des nouveaux ids su -c "(cd $SCODOC_DIR && source venv/bin/activate && flask migrate-scodoc7-dept-archives)" "$SCODOC_USER" || die "Erreur de la post-migration des archives" +# ----- Post-Migration: renomme logos en fonction des nouveaux ids +su -c "(cd $SCODOC_DIR && source venv/bin/activate && flask migrate-scodoc7-dept-logos)" || die "Erreur de la post-migration des logos" + # --- Si migration "en place", désactive ScoDoc 7 if [ "$INPLACE" == 1 ] diff --git a/tools/migrate_scodoc7_logos.py b/tools/migrate_scodoc7_logos.py new file mode 100644 index 00000000..57c7775f --- /dev/null +++ b/tools/migrate_scodoc7_logos.py @@ -0,0 +1,53 @@ +# -*- mode: python -*- +# -*- coding: utf-8 -*- + +import glob +import os +import shutil + +from app.models import Departement + + +def migrate_scodoc7_dept_logos(dept_name=""): + if dept_name: + depts = Departement.query.filter_by(acronym=dept_name) + else: + depts = Departement.query + n_dir = 0 + n_moves = 0 + n_depts = 0 + purged_candidates = [] # directory that maybe purged at the end + for dept in depts: + logos_dir7 = f"/opt/scodoc-data/config/logos/logos_{dept.acronym}" + logos_dir9 = f"/opt/scodoc-data/config/logos/logos_{dept.id}" + if os.path.exists(logos_dir7): + print(f"Migrating {dept.acronym} logos...") + purged_candidates.append(logos_dir7) + n_depts += 1 + if not os.path.exists(logos_dir9): + # print(f"renaming {logos_dir7} to {logos_dir9}") + shutil.move(logos_dir7, logos_dir9) + n_dir += 1 + else: + # print(f"merging {logos_dir7} with {logos_dir9}") + for logo in glob.glob(f"{logos_dir7}/*"): + # print(f"\tmoving {logo}") + fn = os.path.split(logo)[1] + if not os.path.exists(os.path.sep.join([logos_dir9, fn])): + shutil.move(logo, logos_dir9) + n_moves += 1 + n_purged = 0 + for candidate in purged_candidates: + if len(os.listdir(candidate)) == 0: + os.rmdir(candidate) + n_purged += 1 + print(f"{n_depts} department(s) scanned") + if n_dir: + print(f"{n_dir} directory(ies) moved") + if n_moves: + print(f"{n_moves} file(s) moved") + if n_purged: + print(f"{n_purged} scodoc7 logo dir(s) removed") + if n_dir + n_moves + n_purged == 0: + print("nothing done") + # print(f"moved {n_moves}/{n} etuds") From a6c95b013bdf87d88e7e9ba1c858c50204adfb82 Mon Sep 17 00:00:00 2001 From: Place Jean-Marie Date: Sun, 14 Nov 2021 10:43:55 +0100 Subject: [PATCH 10/19] ajout localize-logo flask command --- app/scodoc/sco_logos.py | 35 +++++++++++++++++++++++++++++++++++ scodoc.py | 14 ++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/app/scodoc/sco_logos.py b/app/scodoc/sco_logos.py index dba5dc61..d2814fe6 100644 --- a/app/scodoc/sco_logos.py +++ b/app/scodoc/sco_logos.py @@ -32,9 +32,11 @@ avec `ext` membre de LOGOS_IMAGES_ALLOWED_TYPES (= jpg, png) SCODOC_LOGOS_DIR /opt/scodoc-data/config/logos """ +import glob import imghdr import os import re +import shutil from pathlib import Path from flask import abort, current_app, url_for @@ -257,3 +259,36 @@ def guess_image_type(stream) -> str: if not fmt: return None return fmt if fmt != "jpeg" else "jpg" + + +def make_logo_local(logoname, dept_name): + breakpoint() + depts = Departement.query.filter_by(acronym=dept_name).all() + if len(depts) == 0: + print(f"no dept {dept_name} found. aborting") + return + if len(depts) > 1: + print(f"several depts {dept_name} found. aborting") + return + dept = depts[0] + print(f"Move logo {logoname}' from global to {dept.acronym}") + old_path_wild = f"/opt/scodoc-data/config/logos/logo_{logoname}.*" + new_dir = f"/opt/scodoc-data/config/logos/logos_{dept.id}" + logos = glob.glob(old_path_wild) + # checks that there is non local already present + for logo in logos: + filename = os.path.split(logo)[1] + new_name = os.path.sep.join([new_dir, filename]) + if os.path.exists(new_name): + print("local version of global logo already exists. aborting") + return + # create new__dir if necessary + if not os.path.exists(new_dir): + print(f"- create {new_dir} directory") + os.mkdir(new_dir) + # move global logo (all suffixes) to local dir note: pre existent file (logo_XXX.*) in local dir does not + # prevent operation if there is no conflict with moved files + # At this point everything is ok so we can do files manipulation + for logo in logos: + shutil.move(logo, new_dir) + # print(f"moved {n_moves}/{n} etuds") diff --git a/scodoc.py b/scodoc.py index 6b34d941..3204c766 100755 --- a/scodoc.py +++ b/scodoc.py @@ -349,6 +349,20 @@ def migrate_scodoc7_dept_logos(dept: str = ""): # migrate-scodoc7-dept-logos tools.migrate_scodoc7_dept_logos(dept) +@app.cli.command() +@click.argument("logo", default=None) +@click.argument("dept", default=None) +@with_appcontext +def localize_logo(logo: str = None, dept: str = None): # migrate-scodoc7-dept-logos + """Make local to a dept a global logo (both logo and dept names are mandatory)""" + if logo in ["header", "footer"]: + print( + f"Can't make logo '{logo}' local: add a local version throught configuration form instead" + ) + return + make_logo_local(logoname=logo, dept_name=dept) + + @app.cli.command() @click.argument("formsemestre_id", type=click.INT) @click.argument("xlsfile", type=click.File("rb")) From d8091b4efbfb1db993dc507b89a9b2c1303d0410 Mon Sep 17 00:00:00 2001 From: Place Jean-Marie Date: Tue, 16 Nov 2021 07:43:39 +0100 Subject: [PATCH 11/19] strip config_logo from scolar --- app/views/scolar.py | 63 ++------------------------------------------- 1 file changed, 2 insertions(+), 61 deletions(-) diff --git a/app/views/scolar.py b/app/views/scolar.py index c708495a..d284b04e 100644 --- a/app/views/scolar.py +++ b/app/views/scolar.py @@ -39,7 +39,7 @@ from flask import current_app, g, request from flask_login import current_user from flask_wtf import FlaskForm from flask_wtf.file import FileField, FileAllowed -from wtforms import SubmitField +from wtforms import SubmitField, FormField from app.decorators import ( scodoc, @@ -49,6 +49,7 @@ from app.decorators import ( admin_required, login_required, ) +from app.scodoc.sco_logos import find_logo from app.views import scolar_bp as bp @@ -165,66 +166,6 @@ def doc_preferences(): return response -class DeptLogosConfigurationForm(FlaskForm): - "Panneau de configuration logos dept" - - logo_header = FileField( - label="Modifier l'image:", - description="logo placé en haut des documents PDF", - validators=[ - FileAllowed( - scu.LOGOS_IMAGES_ALLOWED_TYPES, - f"n'accepte que les fichiers image {','.join([e for e in scu.LOGOS_IMAGES_ALLOWED_TYPES])}", - ) - ], - ) - - logo_footer = FileField( - label="Modifier l'image:", - description="logo placé en pied des documents PDF", - validators=[ - FileAllowed( - scu.LOGOS_IMAGES_ALLOWED_TYPES, - f"n'accepte que les fichiers image {','.join([e for e in scu.LOGOS_IMAGES_ALLOWED_TYPES])}", - ) - ], - ) - - submit = SubmitField("Enregistrer") - - -@bp.route("/config_logos", methods=["GET", "POST"]) -@permission_required(Permission.ScoChangePreferences) -def config_logos(scodoc_dept): - "Panneau de configuration général" - form = DeptLogosConfigurationForm() - if form.validate_on_submit(): - if form.logo_header.data: - sco_logos.store_image( - form.logo_header.data, - os.path.join( - scu.SCODOC_LOGOS_DIR, "logos_" + scodoc_dept, "logo_header" - ), - ) - if form.logo_footer.data: - sco_logos.store_image( - form.logo_footer.data, - os.path.join( - scu.SCODOC_LOGOS_DIR, "logos_" + scodoc_dept, "logo_footer" - ), - ) - app.clear_scodoc_cache() - flash(f"Logos enregistrés") - return flask.redirect(url_for("scolar.index_html", scodoc_dept=scodoc_dept)) - - return render_template( - "configuration.html", - title="Configuration Logos du département", - form=form, - scodoc_dept=scodoc_dept, - ) - - # -------------------------------------------------------------------- # # ETUDIANTS From 483c22678aa76257e20e0f4e6871e1c647d7b152 Mon Sep 17 00:00:00 2001 From: Place Jean-Marie Date: Tue, 16 Nov 2021 18:48:56 +0100 Subject: [PATCH 12/19] build logo form (header & footer) --- app/scodoc/sco_logos.py | 24 ++++--- app/templates/configuration.html | 60 +++++++++++----- app/views/scodoc.py | 96 ++++++++++++++----------- app/views/scolar.py | 63 +++++++++++++++- tests/ressources/test_logos/logo_D.png | Bin 15701 -> 15640 bytes tests/unit/test_logos.py | 4 +- 6 files changed, 175 insertions(+), 72 deletions(-) diff --git a/app/scodoc/sco_logos.py b/app/scodoc/sco_logos.py index d2814fe6..5808945f 100644 --- a/app/scodoc/sco_logos.py +++ b/app/scodoc/sco_logos.py @@ -160,7 +160,7 @@ class Logo: "Not inited: call the select or create function before access" ) self.density = "Not inited: call the select or create function before access" - self.cm = "Not inited: call the select or create function before access" + self.mm = "Not inited: call the select or create function before access" def _set_format(self, fmt): self.suffix = fmt @@ -199,21 +199,21 @@ class Logo: unit = 1 if self.density is None: # no dpi found try jfif infos self.density = img.info.get("jfif_density", None) - unit = img.info.get("jfif_unit", 0) # 0 = no unit ; 1 = inch ; 2 = cm + unit = img.info.get("jfif_unit", 0) # 0 = no unit ; 1 = inch ; 2 = mm if self.density is not None: x_density, y_density = self.density if unit != 0: - unit2cm = [0, 1 / 2.54, 1][unit] - x_cm = round(x_size * unit2cm / x_density, 2) - y_cm = round(y_size * unit2cm / y_density, 2) - self.cm = (x_cm, y_cm) + unit2mm = [0, 1 / 0.254, 0.1][unit] + x_mm = round(x_size * unit2mm / x_density, 2) + y_mm = round(y_size * unit2mm / y_density, 2) + self.mm = (x_mm, y_mm) else: - self.cm = None + self.mm = None else: - self.cm = None + self.mm = None self.size = (x_size, y_size) - self.aspect_ratio = float(x_size) / y_size + self.aspect_ratio = round(float(x_size) / y_size, 2) def select(self): """ @@ -250,6 +250,12 @@ class Logo: global_if_not_found=False, ) + def get_usage(self): + if self.mm is None: + return f'' + else: + return f'' + def guess_image_type(stream) -> str: "guess image type from header in stream" diff --git a/app/templates/configuration.html b/app/templates/configuration.html index 88f1fd81..077c094d 100644 --- a/app/templates/configuration.html +++ b/app/templates/configuration.html @@ -16,38 +16,66 @@ {% endmacro %} +{% macro render_logo(logo_form, titre=None) %} + {% if titre %} + + +

{{ titre }}

+ + + {% endif %} + + +

{{ logo_form.form.description }} Image actuelle:

+
pas de logo chargé
+ + + Nom: {{ logo_form.form.logo.logoname }}
+ {{ logo_form.form.description }}
+ Format: {{ logo_form.logo.suffix }}
+ Taille en px: {{ logo_form.logo.size }}
+ {% if logo_form.logo.mm %} + Taile en mm: {{ logo_form.logo.mm }}
+ {% endif %} + Aspect ratio: {{ logo_form.logo.aspect_ratio }}
+ Usage: {{ logo_form.logo.get_usage() }} + {{ logo_form.action()|safe }} + {{ render_field(logo_form.upload) }} + {% if logo_form.can_delete %} + {{ render_field(logo_form.do_delete) }} + {% endif %} + + +{% endmacro %} + + {% block app_content %} {% if scodoc_dept %}

Logos du département {{ scodoc_dept }}

{% else %} -

Configuration générale {{ scodoc_dept }}

+

Configuration générale

{% endif %}
{{ form.hidden_tag() }} - {% if not scodoc_dept %}
Les paramètres donnés ici s'appliquent à tout ScoDoc (tous les départements):
- - {{ render_field(form.bonus_sport_func_name)}} + {{ render_field(form.bonus_sport_func_name)}} + {% endif %} - +
{{ form.submit() }}
{% endblock %} \ No newline at end of file diff --git a/app/views/scodoc.py b/app/views/scodoc.py index 0def371c..70433025 100644 --- a/app/views/scodoc.py +++ b/app/views/scodoc.py @@ -32,6 +32,8 @@ Emmanuel Viennet, 2021 """ import io +import wtforms.validators + from app.auth.models import User import os @@ -44,9 +46,9 @@ from flask_login.utils import login_required, current_user from flask_wtf import FlaskForm from flask_wtf.file import FileField, FileAllowed from werkzeug.exceptions import BadRequest, NotFound -from wtforms import SelectField, SubmitField +from wtforms import SelectField, SubmitField, FormField, validators, Form from wtforms.fields import IntegerField -from wtforms.fields.simple import BooleanField, StringField, TextAreaField +from wtforms.fields.simple import BooleanField, StringField, TextAreaField, HiddenField from wtforms.validators import ValidationError, DataRequired, Email, EqualTo import app @@ -64,6 +66,7 @@ from app.decorators import ( permission_required_compat_scodoc7, ) from app.scodoc.sco_exceptions import AccessDenied +from app.scodoc.sco_logos import find_logo from app.scodoc.sco_permissions import Permission from app.views import scodoc_bp as bp @@ -178,6 +181,34 @@ def about(scodoc_dept=None): # ---- CONFIGURATION +class LogoForm(FlaskForm): + action = HiddenField("action") + upload = FileField( + label="Modifier l'image:", + validators=[ + FileAllowed( + scu.LOGOS_IMAGES_ALLOWED_TYPES, + f"n'accepte que les fichiers image {','.join([e for e in scu.LOGOS_IMAGES_ALLOWED_TYPES])}", + ) + ], + ) + do_delete = SubmitField("Supprimer") + + def set_infos(self, logo, description=None, can_delete=None): + self.logo = logo + self.description = description + self.can_delete = can_delete + + def breakpoint(self, form): + breakpoint() + + def __init__(self, *args, **kwargs): + super(LogoForm, self).__init__(*args, **kwargs) + self.logo = None + self.description = None + self.can_delete = None + + class ScoDocConfigurationForm(FlaskForm): "Panneau de configuration général" @@ -188,31 +219,24 @@ class ScoDocConfigurationForm(FlaskForm): for x in ScoDocSiteConfig.get_bonus_sport_func_names() ], ) - - logo_header = FileField( - label="Modifier l'image:", - description="logo placé en haut des documents PDF", - validators=[ - FileAllowed( - scu.LOGOS_IMAGES_ALLOWED_TYPES, - f"n'accepte que les fichiers image {','.join([e for e in scu.LOGOS_IMAGES_ALLOWED_TYPES])}", - ) - ], - ) - - logo_footer = FileField( - label="Modifier l'image:", - description="logo placé en pied des documents PDF", - validators=[ - FileAllowed( - scu.LOGOS_IMAGES_ALLOWED_TYPES, - f"n'accepte que les fichiers image {','.join([e for e in scu.LOGOS_IMAGES_ALLOWED_TYPES])}", - ) - ], - ) - + header = FormField(LogoForm) + footer = FormField(LogoForm) submit = SubmitField("Enregistrer") + def __init__(self, *args, **kwargs): + super(ScoDocConfigurationForm, self).__init__(*args, **kwargs) + breakpoint() + self.header.form.set_infos( + logo=find_logo("header", dept_id=None).select(), + description="image placée en haut de certains documents documents PDF.", + can_delete=False, + ) + self.footer.form.set_infos( + logo=find_logo("footer", dept_id=None).select(), + description="image placée en pied de page de certains documents documents PDF.", + can_delete=False, + ) + # Notes pour variables config: (valeurs par défaut des paramètres de département) # Chaines simples @@ -241,10 +265,10 @@ def configuration(): ) if form.validate_on_submit(): ScoDocSiteConfig.set_bonus_sport_func(form.bonus_sport_func_name.data) - if form.logo_header.data: - sco_logos.write_logo(stream=form.logo_header.data, name="header") - if form.logo_footer.data: - sco_logos.write_logo(stream=form.logo_footer.data, name="footer") + if form.header.data: + sco_logos.write_logo(stream=form.header.data, name="header") + if form.footer.data: + sco_logos.write_logo(stream=form.footer.data, name="footer") app.clear_scodoc_cache() flash(f"Configuration enregistrée") return redirect(url_for("scodoc.index")) @@ -313,20 +337,6 @@ def get_logo(name: str, dept_id: int): ) -# @bp.route("/ScoDoc/logo_header") -# @bp.route("/ScoDoc//logo_header") -# def logo_header(scodoc_dept=""): -# "Image logo header" -# return _return_logo(name="header", scodoc_dept=scodoc_dept) - - -# @bp.route("/ScoDoc/logo_footer") -# @bp.route("/ScoDoc//logo_footer") -# def logo_footer(scodoc_dept=""): -# "Image logo footer" -# return _return_logo(name="footer", scodoc_dept=scodoc_dept) - - # essais # @bp.route("/testlog") # def testlog(): diff --git a/app/views/scolar.py b/app/views/scolar.py index d284b04e..c708495a 100644 --- a/app/views/scolar.py +++ b/app/views/scolar.py @@ -39,7 +39,7 @@ from flask import current_app, g, request from flask_login import current_user from flask_wtf import FlaskForm from flask_wtf.file import FileField, FileAllowed -from wtforms import SubmitField, FormField +from wtforms import SubmitField from app.decorators import ( scodoc, @@ -49,7 +49,6 @@ from app.decorators import ( admin_required, login_required, ) -from app.scodoc.sco_logos import find_logo from app.views import scolar_bp as bp @@ -166,6 +165,66 @@ def doc_preferences(): return response +class DeptLogosConfigurationForm(FlaskForm): + "Panneau de configuration logos dept" + + logo_header = FileField( + label="Modifier l'image:", + description="logo placé en haut des documents PDF", + validators=[ + FileAllowed( + scu.LOGOS_IMAGES_ALLOWED_TYPES, + f"n'accepte que les fichiers image {','.join([e for e in scu.LOGOS_IMAGES_ALLOWED_TYPES])}", + ) + ], + ) + + logo_footer = FileField( + label="Modifier l'image:", + description="logo placé en pied des documents PDF", + validators=[ + FileAllowed( + scu.LOGOS_IMAGES_ALLOWED_TYPES, + f"n'accepte que les fichiers image {','.join([e for e in scu.LOGOS_IMAGES_ALLOWED_TYPES])}", + ) + ], + ) + + submit = SubmitField("Enregistrer") + + +@bp.route("/config_logos", methods=["GET", "POST"]) +@permission_required(Permission.ScoChangePreferences) +def config_logos(scodoc_dept): + "Panneau de configuration général" + form = DeptLogosConfigurationForm() + if form.validate_on_submit(): + if form.logo_header.data: + sco_logos.store_image( + form.logo_header.data, + os.path.join( + scu.SCODOC_LOGOS_DIR, "logos_" + scodoc_dept, "logo_header" + ), + ) + if form.logo_footer.data: + sco_logos.store_image( + form.logo_footer.data, + os.path.join( + scu.SCODOC_LOGOS_DIR, "logos_" + scodoc_dept, "logo_footer" + ), + ) + app.clear_scodoc_cache() + flash(f"Logos enregistrés") + return flask.redirect(url_for("scolar.index_html", scodoc_dept=scodoc_dept)) + + return render_template( + "configuration.html", + title="Configuration Logos du département", + form=form, + scodoc_dept=scodoc_dept, + ) + + # -------------------------------------------------------------------- # # ETUDIANTS diff --git a/tests/ressources/test_logos/logo_D.png b/tests/ressources/test_logos/logo_D.png index 7ac56e1681c3657043bf9dcbcaed5d8295fa745b..8239e535ca8f59df405f5064192135893a3b8669 100644 GIT binary patch delta 377 zcmV-<0fzq7dYF0&iBL{Q3K|Lk005B;?*_CZ7;kQhlLG<6lg9yH8hX>c9jJkjF+~H) z8i?v_KUbS${eC|-sw{1@Ljno`lTHJ!v;6~?0kf3_Jpq%`2Ga$yg%-#Zv#kgA0h6Q& z;S*K~kl4#%Pf{Z-XHnH)B9T4L_LG_l9kaL!HvzNo3&aDnwGS);lj0GivxgE_0<+%~ zzygz?7sZoi7-o~r7&(&%8N9Q^8QcN|KYvyOT$869bCVk#>$3+Qv;nic9}ohQ%^@U{ z@*z@_MIxS)8Ld5~$66MOc*hJS?ZP zAtqA+lXWMTlMN_LlUgY2vqmZ20h1*wP_uR`Dg(2jE?fhX{xP$&vNDGP6oJN;8dj)n z!^&#weFkKG0|HWLAps5d;5?Saqv<;Vlj=NcMsl1` delta 436 zcmbPHb+wA8Gr-TCmrII^fq{W{BG-Ek-Yv=!`FUNu6aOD(g4in zwr1jD1oB;(*D>;L{>(fb$l2V*Y6}uO&UTrdcT=m_2@&4S^Vz?Fgr;&n6y*)$oN(iG zqJNOGZhGL=1f_|#=f3ey?&OjM>RiTU4l>{__Yr0wwUA$j38?9;;1VFGP$(S8*?dCe z07$Gu{3ww0l}H1Unfzmw6`5;x*DhVLh-c|*<>4$1Ch~uq39Y{V>_W_c6 z8XzMjHSd8WzH0>lIquq8ASV~<#Q>?vXAL(4ISoebOk%u^$EHbzt;jumD(iJEXZjlR zwstui-p$gc28^P--Tuc{mj4SEJ7{+!{Vwad7grx>@@|eZ69$=EXl?@}*P6cslG7{} Z0!eert3XqEtQG>L7q7JjDOzij4FEB>iR%CW diff --git a/tests/unit/test_logos.py b/tests/unit/test_logos.py index 8cf2e275..1ed62a49 100644 --- a/tests/unit/test_logos.py +++ b/tests/unit/test_logos.py @@ -124,7 +124,7 @@ def test_get_jpg_data(create_dept, create_logos): assert logo.suffix == "jpg" assert logo.filename == "A.jpg" assert logo.size == (1200, 600) - assert logo.cm == approx((4.0, 3.0), 0.01) + assert logo.mm == approx((40, 30), 0.1) def test_get_png_without_data(create_dept, create_logos): @@ -136,7 +136,7 @@ def test_get_png_without_data(create_dept, create_logos): assert logo.filename == "D.png" assert logo.size == (121, 121) assert logo.density is None - assert logo.cm is None + assert logo.mm is None def test_create_globale_jpg_logo(create_dept, create_logos): From 2920c6f1316b03c1cbd4168c8aa50b8b56ea30d6 Mon Sep 17 00:00:00 2001 From: Place Jean-Marie Date: Fri, 19 Nov 2021 11:51:05 +0100 Subject: [PATCH 13/19] temp. le 19/11 12h --- app/__init__.py | 2 + app/scodoc/sco_config_actions.py | 181 ++++++++ app/scodoc/sco_config_form.py | 402 ++++++++++++++++++ app/scodoc/sco_logos.py | 28 +- app/scodoc/sco_pdf.py | 5 +- app/scodoc/sco_preferences.py | 8 +- app/static/css/scodoc.css | 17 + app/static/js/configuration.js | 6 + app/templates/config_dept.html | 81 ++++ app/templates/configuration.html | 148 ++++--- app/views/scodoc.py | 90 +--- app/views/scolar.py | 122 ++++-- tests/ressources/test_logos/logo_A.jpg | Bin 39157 -> 3755 bytes tests/ressources/test_logos/logo_A1.jpg | Bin 0 -> 3954 bytes tests/ressources/test_logos/logo_C.jpg | Bin 2417 -> 3071 bytes tests/ressources/test_logos/logo_D.png | Bin 15640 -> 21297 bytes tests/ressources/test_logos/logo_E.jpg | Bin 2935 -> 2938 bytes tests/ressources/test_logos/logo_F.jpeg | Bin 2882 -> 3002 bytes .../ressources/test_logos/logos_1/logo_A.jpg | Bin 2730 -> 3218 bytes .../ressources/test_logos/logos_1/logo_B.jpg | Bin 2832 -> 3061 bytes .../ressources/test_logos/logos_2/logo_A.jpg | Bin 2703 -> 3241 bytes .../ressources/test_logos/logos_2/logo_A1.jpg | Bin 0 -> 3501 bytes tests/unit/test_logos.py | 97 ++++- 23 files changed, 989 insertions(+), 198 deletions(-) create mode 100644 app/scodoc/sco_config_actions.py create mode 100644 app/scodoc/sco_config_form.py create mode 100644 app/static/js/configuration.js create mode 100644 app/templates/config_dept.html create mode 100644 tests/ressources/test_logos/logo_A1.jpg create mode 100644 tests/ressources/test_logos/logos_2/logo_A1.jpg diff --git a/app/__init__.py b/app/__init__.py index 0943a91f..969c9766 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -33,6 +33,7 @@ from app.scodoc.sco_exceptions import ( ) from config import DevConfig import sco_version +from flask_debugtoolbar import DebugToolbarExtension db = SQLAlchemy() migrate = Migrate(compare_type=True) @@ -187,6 +188,7 @@ def create_app(config_class=DevConfig): moment.init_app(app) cache.init_app(app) sco_cache.CACHE = cache + toolbar = DebugToolbarExtension(app) app.register_error_handler(ScoGenError, handle_sco_value_error) app.register_error_handler(ScoValueError, handle_sco_value_error) diff --git a/app/scodoc/sco_config_actions.py b/app/scodoc/sco_config_actions.py new file mode 100644 index 00000000..bf633a26 --- /dev/null +++ b/app/scodoc/sco_config_actions.py @@ -0,0 +1,181 @@ +# -*- mode: python -*- +# -*- coding: utf-8 -*- + +############################################################################## +# +# ScoDoc +# +# Copyright (c) 1999 - 2021 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@viennet.net +# +############################################################################## + +""" +Module main: page d'accueil, avec liste des départements + +Emmanuel Viennet, 2021 +""" +from app.models import ScoDocSiteConfig +from app.scodoc.sco_logos import write_logo, find_logo, delete_logo +import app +from flask import current_app + + +class Action: + """Base class for all classes describing an action from from config form.""" + + def __init__(self, message, parameters): + self.message = message + self.parameters = parameters + + @staticmethod + def build_action(parameters, stream=None): + """Check (from parameters) if some action has to be done and + then return list of action (or else return empty list).""" + raise NotImplementedError + + def display(self): + """return a str describing the action to be done""" + return self.message.format_map(self.parameters) + + def execute(self): + """Executes the action""" + raise NotImplementedError + + +GLOBAL = "_" + + +class LogoUpdate(Action): + """Action: change a logo + dept_id: dept_id or '_', + logo_id: logo_id, + upload: image file replacement + """ + + def __init__(self, parameters): + super().__init__( + f"Modification du logo {parameters['logo_id']} pour le département {parameters['dept_id']}", + parameters, + ) + + @staticmethod + def build_action(parameters): + dept_id = parameters["dept_key"] + if dept_id == GLOBAL: + dept_id = None + parameters["dept_id"] = dept_id + if parameters["upload"] is not None: + return LogoUpdate(parameters) + return None + + def execute(self): + current_app.logger.info(self.message) + write_logo( + stream=self.parameters["upload"], + dept_id=self.parameters["dept_id"], + name=self.parameters["logo_id"], + ) + + +class LogoDelete(Action): + """Action: Delete an existing logo + dept_id: dept_id or '_', + logo_id: logo_id + """ + + def __init__(self, parameters): + super().__init__( + f"Suppression du logo {parameters['logo_id']} pour le département {parameters['dept_id']}.", + parameters, + ) + + @staticmethod + def build_action(parameters): + parameters["dept_id"] = parameters["dept_key"] + if parameters["dept_key"] == GLOBAL: + parameters["dept_id"] = None + if parameters["do_delete"]: + return LogoDelete(parameters) + return None + + def execute(self): + current_app.logger.info(self.message) + delete_logo(name=self.parameters["logo_id"], dept_id=self.parameters["dept_id"]) + + +class LogoInsert(Action): + """Action: add a new logo + dept_key: dept_id or '_', + logo_id: logo_id, + upload: image file replacement + """ + + def __init__(self, parameters): + super().__init__( + f"Ajout du logo {parameters['name']} pour le département {parameters['dept_key']} ({parameters['upload']}).", + parameters, + ) + + @staticmethod + def build_action(parameters): + if parameters["dept_key"] == GLOBAL: + parameters["dept_id"] = None + if parameters["upload"] and parameters["name"]: + logo = find_logo( + logoname=parameters["name"], dept_id=parameters["dept_key"] + ) + if logo is None: + return LogoInsert(parameters) + return None + + def execute(self): + dept_id = self.parameters["dept_key"] + if dept_id == GLOBAL: + dept_id = None + current_app.logger.info(self.message) + write_logo( + stream=self.parameters["upload"], + name=self.parameters["name"], + dept_id=dept_id, + ) + + +class BonusSportUpdate(Action): + """Action: Change bonus_sport_function_name. + bonus_sport_function_name: the new value""" + + def __init__(self, parameters): + super().__init__( + f"Changement du calcul de bonus sport pour ({parameters['bonus_sport_func_name']}).", + parameters, + ) + + @staticmethod + def build_action(parameters): + if ( + parameters["bonus_sport_func_name"] + != ScoDocSiteConfig.get_bonus_sport_func_name() + ): + return [BonusSportUpdate(parameters)] + return [] + + def execute(self): + current_app.logger.info(self.message) + ScoDocSiteConfig.set_bonus_sport_func(self.parameters["bonus_sport_func_name"]) + app.clear_scodoc_cache() diff --git a/app/scodoc/sco_config_form.py b/app/scodoc/sco_config_form.py new file mode 100644 index 00000000..d5ee87a9 --- /dev/null +++ b/app/scodoc/sco_config_form.py @@ -0,0 +1,402 @@ +# -*- mode: python -*- +# -*- coding: utf-8 -*- + +############################################################################## +# +# ScoDoc +# +# Copyright (c) 1999 - 2021 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@viennet.net +# +############################################################################## + +""" +Module main: page d'accueil, avec liste des départements + +Emmanuel Viennet, 2021 +""" +import re + +from flask import flash, url_for, redirect, render_template +from flask_wtf import FlaskForm +from flask_wtf.file import FileField, FileAllowed +from wtforms import SelectField, SubmitField, FormField, validators, FieldList +from wtforms.fields.simple import BooleanField, StringField, HiddenField + +from app import AccessDenied +from app.models import Departement +from app.models import ScoDocSiteConfig +from app.scodoc import sco_logos, html_sco_header +from app.scodoc import sco_utils as scu +from app.scodoc.sco_config_actions import ( + LogoDelete, + LogoUpdate, + LogoInsert, + BonusSportUpdate, +) + +from flask_login import current_user + +from app.scodoc.sco_logos import find_logo + +JAVASCRIPTS = html_sco_header.BOOTSTRAP_MULTISELECT_JS + [] + +CSSSTYLES = html_sco_header.BOOTSTRAP_MULTISELECT_CSS + +# class ItemForm(FlaskForm): +# """Unused Generic class to document common behavior for classes +# * ScoConfigurationForm +# * DeptForm +# * LogoForm +# Some or all of these implements: +# * Composite design pattern (ScoConfigurationForm and DeptForm) +# - a FieldList(FormField(ItemForm)) +# - FieldListItem are created by browsing the model +# - index dictionnary to provide direct access to a SubItemForm +# - the direct access method (get_form) +# * have some information added to be displayed +# - information are collected from a model object +# Common methods: +# * build(model) (not for LogoForm who has no child) +# for each child: +# * create en entry in the FieldList for each subitem found +# * update self.index +# * fill_in additional information into the form +# * recursively calls build for each chid +# some spécific information may be added after standard processing +# (typically header/footer description) +# * preview(data) +# check the data from a post and build a list of operations that has to be done. +# for a two phase process: +# * phase 1 (list all opérations) +# * phase 2 (may be confirmation and execure) +# - if no op found: return to the form with a message 'Aucune modification trouvée' +# - only one operation found: execute and go to main page +# - more than 1 operation found. asked form confirmation (and execution if confirmed) +# +# Someday we'll have time to refactor as abstract classes but Abstract FieldList makes this a bit complicated +# """ + +# Terminology: +# dept_id : identifies a dept in modele (= list_logos()). None designates globals logos +# dept_key : identifies a dept in this form only (..index[dept_key], and fields 'dept_key'). +# 'GLOBAL' designates globals logos (we need a string value to set up HiddenField +GLOBAL = "_" + + +def dept_id_to_key(dept_id): + if dept_id is None: + return GLOBAL + return dept_id + + +def dept_key_to_id(dept_key): + if dept_key == GLOBAL: + return None + return dept_key + + +class AddLogoForm(FlaskForm): + """Formulaire permettant l'ajout d'un logo (dans un département)""" + + dept_key = HiddenField() + name = StringField( + label="Nom", + validators=[ + validators.regexp( + r"^[a-zA-Z0-9-]*$", + re.IGNORECASE, + "Ne doit comporter que lettres, chiffres ou -", + ), + validators.Length( + max=20, message="Un nom ne doit pas dépasser 20 caractères" + ), + validators.required("Nom de logo requis (alphanumériques ou '-')"), + ], + ) + upload = FileField( + label="Sélectionner l'image", + validators=[ + FileAllowed( + scu.LOGOS_IMAGES_ALLOWED_TYPES, + f"n'accepte que les fichiers image {', '.join(scu.LOGOS_IMAGES_ALLOWED_TYPES)}", + ), + validators.required("Fichier image manquant"), + ], + ) + do_insert = SubmitField("ajouter une image") + + def __init__(self, *args, **kwargs): + kwargs["meta"] = {"csrf": False} + super().__init__(*args, **kwargs) + + def validate_name(self, name): + dept_id = dept_key_to_id(self.dept_key.data) + if dept_id == GLOBAL: + dept_id = None + if find_logo(logoname=name.data, dept_id=dept_id) is not None: + raise validators.ValidationError("Un logo de même nom existe déjà") + + def select_action(self): + if self.data["do_insert"]: + if self.validate(): + return LogoInsert.build_action(self.data) + return None + + +class LogoForm(FlaskForm): + """Embed both presentation of a logo (cf. template file configuration.html) + and all its data and UI action (change, delete)""" + + dept_key = HiddenField() + logo_id = HiddenField() + upload = FileField( + label="Remplacer l'image", + validators=[ + FileAllowed( + scu.LOGOS_IMAGES_ALLOWED_TYPES, + f"n'accepte que les fichiers image {', '.join(scu.LOGOS_IMAGES_ALLOWED_TYPES)}", + ) + ], + ) + do_delete = SubmitField("Supprimer l'image") + + def __init__(self, *args, **kwargs): + kwargs["meta"] = {"csrf": False} + super().__init__(*args, **kwargs) + self.logo = find_logo( + logoname=self.logo_id.data, dept_id=dept_key_to_id(self.dept_key.data) + ).select() + self.description = None + self.titre = None + self.can_delete = True + if self.dept_key.data == GLOBAL: + if self.logo_id.data == "header": + self.can_delete = False + self.description = "" + self.titre = "Logo en-tête" + if self.logo_id.data == "footer": + self.can_delete = False + self.titre = "Logo pied de page" + self.description = "" + else: + if self.logo_id.data == "header": + self.description = "Se substitue au header défini au niveau global" + self.titre = "Logo en-tête" + if self.logo_id.data == "footer": + self.description = "Se substitue au footer défini au niveau global" + self.titre = "Logo pied de page" + + def select_action(self): + if self.do_delete.data and self.can_delete: + return LogoDelete.build_action(self.data) + if self.upload.data and self.validate(): + return LogoUpdate.build_action(self.data) + return None + + +class DeptForm(FlaskForm): + dept_key = HiddenField() + dept_name = HiddenField() + add_logo = FormField(AddLogoForm) + logos = FieldList(FormField(LogoForm)) + + def __init__(self, *args, **kwargs): + kwargs["meta"] = {"csrf": False} + super().__init__(*args, **kwargs) + + def is_local(self): + if self.dept_key.data == GLOBAL: + return None + return True + + def select_action(self): + action = self.add_logo.form.select_action() + if action: + return action + for logo_entry in self.logos.entries: + logo_form = logo_entry.form + action = logo_form.select_action() + if action: + return action + return None + + def get_form(self, logoname=None): + """Retourne le formulaire associé à un logo. None si pas trouvé""" + if logoname is None: # recherche de département + return self + return self.index.get(logoname, None) + + +def _make_dept_id_name(): + """Cette section assute que tous les départements sont traités (y compris ceux qu'ont pas de logo au départ) + et détermine l'ordre d'affichage des DeptForm (GLOBAL d'abord, puis par ordre alpha de nom de département) + -> [ (None, None), (dept_id, dept_name)... ]""" + depts = [(None, GLOBAL)] + for dept in ( + Departement.query.filter_by(visible=True).order_by(Departement.acronym).all() + ): + depts.append((dept.id, dept.acronym)) + return depts + + +def _ordered_logos(modele): + """sort logoname alphabetically but header and footer moved at start. (since there is no space in logoname)""" + + def sort(name): + if name == "header": + return " 0" + if name == "footer": + return " 1" + return name + + order = sorted(modele.keys(), key=sort) + return order + + +def _make_dept_data(dept_id, dept_name, modele): + dept_key = dept_id_to_key(dept_id) + data = { + "dept_key": dept_key, + "dept_name": dept_name, + "add_logo": {"dept_key": dept_key}, + } + logos = [] + if modele is not None: + for name in _ordered_logos(modele): + logos.append({"dept_key": dept_key, "logo_id": name}) + data["logos"] = logos + return data + + +def _make_depts_data(modele): + data = [] + for dept_id, dept_name in _make_dept_id_name(): + data.append( + _make_dept_data( + dept_id=dept_id, dept_name=dept_name, modele=modele.get(dept_id, None) + ) + ) + return data + + +def _make_data(bonus_sport, modele): + data = { + "bonus_sport_func_name": bonus_sport, + "depts": _make_depts_data(modele=modele), + } + return data + + +class ScoDocConfigurationForm(FlaskForm): + "Panneau de configuration général" + bonus_sport_func_name = SelectField( + label="Fonction de calcul des bonus sport&culture", + choices=[ + (x, x if x else "Aucune") + for x in ScoDocSiteConfig.get_bonus_sport_func_names() + ], + ) + depts = FieldList(FormField(DeptForm)) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # def _set_global_logos_infos(self): + # "specific processing for globals items" + # global_header = self.get_form(logoname="header") + # global_header.description = ( + # "image placée en haut de certains documents documents PDF." + # ) + # global_header.titre = "Logo en-tête" + # global_header.can_delete = False + # global_footer = self.get_form(logoname="footer") + # global_footer.description = ( + # "image placée en pied de page de certains documents documents PDF." + # ) + # global_footer.titre = "Logo pied de page" + # global_footer.can_delete = False + + # def _build_dept(self, dept_id, dept_name, modele): + # dept_key = dept_id or GLOBAL + # data = {"dept_key": dept_key} + # entry = self.depts.append_entry(data) + # entry.form.build(dept_name, modele.get(dept_id, {})) + # self.index[str(dept_key)] = entry.form + + # def build(self, modele): + # "Build the Form hierachy (DeptForm, LogoForm) and add extra data (from modele)" + # # if entries already initialized (POST). keep subforms + # self.index = {} + # # create entries in FieldList (one entry per dept + # for dept_id, dept_name in self.dept_id_name: + # self._build_dept(dept_id=dept_id, dept_name=dept_name, modele=modele) + # self._set_global_logos_infos() + + def get_form(self, dept_key=GLOBAL, logoname=None): + """Retourne un formulaire: + * pour un département (get_form(dept_id)) ou à un logo (get_form(dept_id, logname)) + * propre à un département (get_form(dept_id, logoname) ou global (get_form(logoname)) + retourne None si le formulaire cherché ne peut être trouvé + """ + dept_form = self.index.get(dept_key, None) + if dept_form is None: # département non trouvé + return None + return dept_form.get_form(logoname) + + def select_action(self): + if ( + self.data["bonus_sport_func_name"] + != ScoDocSiteConfig.get_bonus_sport_func_name() + ): + return BonusSportUpdate(self.data) + for dept_entry in self.depts: + dept_form = dept_entry.form + action = dept_form.select_action() + if action: + return action + return None + + +def configuration(): + """Panneau de configuration général""" + auth_name = str(current_user) + if not current_user.is_administrator(): + raise AccessDenied("invalid user (%s) must be SuperAdmin" % auth_name) + form = ScoDocConfigurationForm( + data=_make_data( + bonus_sport=ScoDocSiteConfig.get_bonus_sport_func_name(), + modele=sco_logos.list_logos(), + ) + ) + if form.is_submitted(): + action = form.select_action() + if action: + action.execute() + flash(action.message) + return redirect( + url_for( + "scodoc.configuration", + ) + ) + return render_template( + "configuration.html", + scodoc_dept=None, + title="Configuration ScoDoc", + form=form, + ) diff --git a/app/scodoc/sco_logos.py b/app/scodoc/sco_logos.py index 5808945f..3f5917bb 100644 --- a/app/scodoc/sco_logos.py +++ b/app/scodoc/sco_logos.py @@ -46,7 +46,7 @@ from app import Departement, ScoValueError from app.scodoc import sco_utils as scu from PIL import Image as PILImage -GLOBAL = "_GLOBAL" # category for server level logos +GLOBAL = "_" # category for server level logos def find_logo(logoname, dept_id=None, strict=False, prefix=scu.LOGO_FILE_PREFIX): @@ -70,6 +70,18 @@ def find_logo(logoname, dept_id=None, strict=False, prefix=scu.LOGO_FILE_PREFIX) return logo +def delete_logo(name, dept_id=None): + """Delete all files matching logo (dept_id, name) (including all allowed extensions) + Args: + name: The name of the logo + dept_id: the dept_id (if local). Use None to destroy globals logos + """ + logo = find_logo(logoname=name, dept_id=dept_id) + while logo is not None: + os.unlink(logo.select().filepath) + logo = find_logo(logoname=name, dept_id=dept_id) + + def write_logo(stream, name, dept_id=None): """Crée le fichier logo sur le serveur. Le suffixe du fichier (parmi LOGO_IMAGES_ALLOWED_TYPES) est déduit du contenu du stream""" @@ -79,14 +91,15 @@ def write_logo(stream, name, dept_id=None): def list_logos(): """Crée l'inventaire de tous les logos existants. L'inventaire se présente comme un dictionnaire de dictionnaire de Logo: - [GLOBAL][name] pour les logos globaux + [None][name] pour les logos globaux [dept_id][name] pour les logos propres à un département (attention id numérique du dept) + Les départements sans logos sont absents du résultat """ - inventory = {GLOBAL: _list_dept_logos()} # logos globaux (header / footer) + inventory = {None: _list_dept_logos()} # logos globaux (header / footer) for dept in Departement.query.filter_by(visible=True).all(): logos_dept = _list_dept_logos(dept_id=dept.id) if logos_dept: - inventory[dept.acronym] = _list_dept_logos(dept.id) + inventory[dept.id] = _list_dept_logos(dept.id) return inventory @@ -236,7 +249,7 @@ class Logo: """Retourne l'URL permettant d'obtenir l'image du logo""" return url_for( "scodoc.get_logo", - scodoc_dept=self.scodoc_dept_id, + dept_id=self.scodoc_dept_id, name=self.logoname, global_if_not_found=False, ) @@ -245,7 +258,7 @@ class Logo: """Retourne l'URL permettant d'obtenir l'image du logo sous forme de miniature""" return url_for( "scodoc.get_logo_small", - scodoc_dept=self.scodoc_dept_id, + dept_id=self.scodoc_dept_id, name=self.logoname, global_if_not_found=False, ) @@ -254,7 +267,7 @@ class Logo: if self.mm is None: return f'' else: - return f'' + return f'' def guess_image_type(stream) -> str: @@ -268,7 +281,6 @@ def guess_image_type(stream) -> str: def make_logo_local(logoname, dept_name): - breakpoint() depts = Departement.query.filter_by(acronym=dept_name).all() if len(depts) == 0: print(f"no dept {dept_name} found. aborting") diff --git a/app/scodoc/sco_pdf.py b/app/scodoc/sco_pdf.py index 54ed29b2..85ead167 100755 --- a/app/scodoc/sco_pdf.py +++ b/app/scodoc/sco_pdf.py @@ -60,7 +60,6 @@ from reportlab.lib.pagesizes import letter, A4, landscape from flask import g import app.scodoc.sco_utils as scu -from app.scodoc.sco_logos import find_logo from app.scodoc.sco_utils import CONFIG from app import log from app.scodoc.sco_exceptions import ScoGenError, ScoValueError @@ -193,6 +192,10 @@ class ScolarsPageTemplate(PageTemplate): preferences=None, # dictionnary with preferences, required ): """Initialise our page template.""" + from app.scodoc.sco_logos import ( + find_logo, + ) # defered import (solve circular dependency ->sco_logo ->scodoc, ->sco_pdf + self.preferences = preferences self.pagesbookmarks = pagesbookmarks self.pdfmeta_author = author diff --git a/app/scodoc/sco_preferences.py b/app/scodoc/sco_preferences.py index d83ca14f..7e08d355 100644 --- a/app/scodoc/sco_preferences.py +++ b/app/scodoc/sco_preferences.py @@ -2017,10 +2017,10 @@ class BasePreferences(object): H = [ html_sco_header.sco_header(page_title="Préférences"), "

Préférences globales pour %s

" % scu.ScoURL(), - f"""

modification des logos du département (pour documents pdf)

""" - if current_user.is_administrator() - else "", + # f"""

modification des logos du département (pour documents pdf)

""" + # if current_user.is_administrator() + # else "", """

Ces paramètres s'appliquent par défaut à tous les semestres, sauf si ceux-ci définissent des valeurs spécifiques.

Attention: cliquez sur "Enregistrer les modifications" en bas de page pour appliquer vos changements !

""", diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index cc2ea27a..8dcfc779 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -867,6 +867,9 @@ div.sco_help { span.wtf-field ul.errors li { color: red; +} +.configuration_logo div.img { + } .configuration_logo div.img-container { width: 256px; @@ -874,6 +877,20 @@ span.wtf-field ul.errors li { .configuration_logo div.img-container img { max-width: 100%; } +.configuration_logo div.img-data { + vertical-align: top; +} +.configuration_logo logo-edit titre { + background-color:lightblue; +} +.configuration_logo logo-edit nom { + float: left; + vertical-align: baseline; +} +.configuration_logo logo-edit description { + float:right; + vertical-align:baseline; +} p.indent { padding-left: 2em; diff --git a/app/static/js/configuration.js b/app/static/js/configuration.js new file mode 100644 index 00000000..b537d572 --- /dev/null +++ b/app/static/js/configuration.js @@ -0,0 +1,6 @@ +function submit_form() { + $("#configuration_form").submit(); +} + +$(function () { +}) \ No newline at end of file diff --git a/app/templates/config_dept.html b/app/templates/config_dept.html new file mode 100644 index 00000000..c602c5f3 --- /dev/null +++ b/app/templates/config_dept.html @@ -0,0 +1,81 @@ +{% macro render_field(field) %} +
+ {{ field.label }} : + {{ field()|safe }} + {% if field.errors %} +
    + {% for error in field.errors %} +
  • {{ error }}
  • + {% endfor %} +
+ {% endif %} +
+
+{% endmacro %} + +{% macro render_logo(logo_form, titre=None) %} + {% if titre %} + + +

{{ titre }}

+
+ + + {% endif %} + + +

{{ logo_form.form.description }} Image actuelle:

+
pas de logo chargé
+ + + {{ logo_form.form.dept_id() }} + {{ logo_form.form.logo_id() }} + Nom: {{ logo_form.form.logo.logoname }}
+{# {{ logo_form.form.description }}
#} + Format: {{ logo_form.logo.suffix }}
+ Taille en px: {{ logo_form.logo.size }}
+ {% if logo_form.logo.mm %} + Taile en mm: {{ logo_form.logo.mm }}
+ {% endif %} + Aspect ratio: {{ logo_form.logo.aspect_ratio }}
+
+ Usage: {{ logo_form.logo.get_usage() }} +
+ {{ render_field(logo_form.upload) }} + {% if logo_form.can_delete %} + {{ render_field(logo_form.do_delete) }} + {% endif %} + + +{% endmacro %} + + +{#{% block app_content %}#} + +{% if scodoc_dept %} +

Logos du département {{ scodoc_dept }}

+{% else %} +

Configuration générale

+{% endif %} + +
+ {{ form.hidden_tag() }} + {% if not scodoc_dept %} +
Les paramètres donnés ici s'appliquent à tout ScoDoc (tous les départements):
+ {{ render_field(form.bonus_sport_func_name)}} + + {% endif %} + + + +
{{ form.submit() }}
+
+{#{% endblock %}#} \ No newline at end of file diff --git a/app/templates/configuration.html b/app/templates/configuration.html index 077c094d..b874d48d 100644 --- a/app/templates/configuration.html +++ b/app/templates/configuration.html @@ -1,10 +1,12 @@ {% extends 'base.html' %} {% import 'bootstrap/wtf.html' as wtf %} -{% macro render_field(field) %} +{% macro render_field(field, with_label=True) %}
- {{ field.label }} : - {{ field()|safe }} + {% if with_label %} + {{ field.label }} : + {% endif %} + {{ field(**kwargs)|safe }} {% if field.errors %}
    {% for error in field.errors %} @@ -16,66 +18,102 @@
{% endmacro %} -{% macro render_logo(logo_form, titre=None) %} - {% if titre %} - - -

{{ titre }}

- - - {% endif %} - - -

{{ logo_form.form.description }} Image actuelle:

-
pas de logo chargé
- - - Nom: {{ logo_form.form.logo.logoname }}
- {{ logo_form.form.description }}
- Format: {{ logo_form.logo.suffix }}
- Taille en px: {{ logo_form.logo.size }}
- {% if logo_form.logo.mm %} - Taile en mm: {{ logo_form.logo.mm }}
- {% endif %} - Aspect ratio: {{ logo_form.logo.aspect_ratio }}
- Usage: {{ logo_form.logo.get_usage() }} - {{ logo_form.action()|safe }} - {{ render_field(logo_form.upload) }} - {% if logo_form.can_delete %} - {{ render_field(logo_form.do_delete) }} - {% endif %} - - +{% macro render_add_logo(add_logo_form) %} +
+

Ajouter un logo

+ {{ add_logo_form.hidden_tag() }} + {{ render_field(add_logo_form.name) }} + {{ render_field(add_logo_form.upload) }} + {{ render_field(add_logo_form.do_insert, False, onSubmit="submit_form") }} +
{% endmacro %} +{% macro render_logo(dept_form, logo_form) %} +
+ {{ logo_form.hidden_tag() }} + {% if logo_form.titre %} + + +

{{ logo_form.titre }}

+
{{ logo_form.description or "" }}
+ + + {% else %} + + +

Logo personalisé: {{ logo_form.logo_id.data }}

+ {{ logo_form.description or "" }} + + + {% endif %} + + +
+ pas de logo chargé
+ +

{{ logo_form.logo.logoname }} (Format: {{ logo_form.logo.suffix }})

+ Taille: {{ logo_form.logo.size }} px + {% if logo_form.logo.mm %}   /   {{ logo_form.logo.mm }} mm {% endif %}
+ Aspect ratio: {{ logo_form.logo.aspect_ratio }}
+ Usage: {{ logo_form.logo.get_usage() }} + +

Modifier l'image

+ {{ render_field(logo_form.upload, False, onchange="submit_form()") }} + {% if logo_form.can_delete %} +

Supprimer l'image

+ {{ render_field(logo_form.do_delete, False, onSubmit="submit_form()") }} + {% endif %} + + +
+{% endmacro %} + +{% macro render_logos(dept_form) %} + + {% for logo_entry in dept_form.logos.entries %} + {% set logo_form = logo_entry.form %} + {{ render_logo(dept_form, logo_form) }} + {% else %} +

Aucun logo défini en propre à ce département

+ {% endfor %} +
+{% endmacro %} {% block app_content %} -{% if scodoc_dept %} -

Logos du département {{ scodoc_dept }}

-{% else %} -

Configuration générale

-{% endif %} + + -
+ {{ form.hidden_tag() }} - {% if not scodoc_dept %} -
Les paramètres donnés ici s'appliquent à tout ScoDoc (tous les départements):
- {{ render_field(form.bonus_sport_func_name)}} - - {% endif %} - +
{% endblock %} \ No newline at end of file diff --git a/app/views/scodoc.py b/app/views/scodoc.py index 70433025..d875dc8e 100644 --- a/app/views/scodoc.py +++ b/app/views/scodoc.py @@ -46,7 +46,7 @@ from flask_login.utils import login_required, current_user from flask_wtf import FlaskForm from flask_wtf.file import FileField, FileAllowed from werkzeug.exceptions import BadRequest, NotFound -from wtforms import SelectField, SubmitField, FormField, validators, Form +from wtforms import SelectField, SubmitField, FormField, validators, Form, FieldList from wtforms.fields import IntegerField from wtforms.fields.simple import BooleanField, StringField, TextAreaField, HiddenField from wtforms.validators import ValidationError, DataRequired, Email, EqualTo @@ -56,7 +56,7 @@ from app.models import Departement, Identite from app.models import FormSemestre, FormsemestreInscription from app.models import ScoDocSiteConfig import sco_version -from app.scodoc import sco_logos +from app.scodoc import sco_logos, sco_config_form from app.scodoc import sco_find_etud from app.scodoc import sco_utils as scu from app.decorators import ( @@ -64,7 +64,9 @@ from app.decorators import ( scodoc7func, scodoc, permission_required_compat_scodoc7, + permission_required, ) +from app.scodoc.sco_config_form import configuration from app.scodoc.sco_exceptions import AccessDenied from app.scodoc.sco_logos import find_logo from app.scodoc.sco_permissions import Permission @@ -180,64 +182,6 @@ def about(scodoc_dept=None): # ---- CONFIGURATION - -class LogoForm(FlaskForm): - action = HiddenField("action") - upload = FileField( - label="Modifier l'image:", - validators=[ - FileAllowed( - scu.LOGOS_IMAGES_ALLOWED_TYPES, - f"n'accepte que les fichiers image {','.join([e for e in scu.LOGOS_IMAGES_ALLOWED_TYPES])}", - ) - ], - ) - do_delete = SubmitField("Supprimer") - - def set_infos(self, logo, description=None, can_delete=None): - self.logo = logo - self.description = description - self.can_delete = can_delete - - def breakpoint(self, form): - breakpoint() - - def __init__(self, *args, **kwargs): - super(LogoForm, self).__init__(*args, **kwargs) - self.logo = None - self.description = None - self.can_delete = None - - -class ScoDocConfigurationForm(FlaskForm): - "Panneau de configuration général" - - bonus_sport_func_name = SelectField( - label="Fonction de calcul des bonus sport&culture", - choices=[ - (x, x if x else "Aucune") - for x in ScoDocSiteConfig.get_bonus_sport_func_names() - ], - ) - header = FormField(LogoForm) - footer = FormField(LogoForm) - submit = SubmitField("Enregistrer") - - def __init__(self, *args, **kwargs): - super(ScoDocConfigurationForm, self).__init__(*args, **kwargs) - breakpoint() - self.header.form.set_infos( - logo=find_logo("header", dept_id=None).select(), - description="image placée en haut de certains documents documents PDF.", - can_delete=False, - ) - self.footer.form.set_infos( - logo=find_logo("footer", dept_id=None).select(), - description="image placée en pied de page de certains documents documents PDF.", - can_delete=False, - ) - - # Notes pour variables config: (valeurs par défaut des paramètres de département) # Chaines simples # SCOLAR_FONT = "Helvetica" @@ -259,29 +203,13 @@ class ScoDocConfigurationForm(FlaskForm): @bp.route("/ScoDoc/configuration", methods=["GET", "POST"]) @admin_required def configuration(): - "Panneau de configuration général" - form = ScoDocConfigurationForm( - bonus_sport_func_name=ScoDocSiteConfig.get_bonus_sport_func_name(), - ) - if form.validate_on_submit(): - ScoDocSiteConfig.set_bonus_sport_func(form.bonus_sport_func_name.data) - if form.header.data: - sco_logos.write_logo(stream=form.header.data, name="header") - if form.footer.data: - sco_logos.write_logo(stream=form.footer.data, name="footer") - app.clear_scodoc_cache() - flash(f"Configuration enregistrée") - return redirect(url_for("scodoc.index")) - - return render_template( - "configuration.html", - title="Configuration ScoDoc", - form=form, - scodoc_dept=None, - ) + auth_name = str(current_user) + if not current_user.is_administrator(): + raise AccessDenied("invalid user (%s) must be SuperAdmin" % auth_name) + return sco_config_form.configuration() -SMALL_SIZE = (300, 300) +SMALL_SIZE = (200, 200) def _return_logo(name="header", dept_id="", small=False, strict: bool = True): diff --git a/app/views/scolar.py b/app/views/scolar.py index c708495a..ebfd4d1a 100644 --- a/app/views/scolar.py +++ b/app/views/scolar.py @@ -174,7 +174,7 @@ class DeptLogosConfigurationForm(FlaskForm): validators=[ FileAllowed( scu.LOGOS_IMAGES_ALLOWED_TYPES, - f"n'accepte que les fichiers image {','.join([e for e in scu.LOGOS_IMAGES_ALLOWED_TYPES])}", + f"n'accepte que les fichiers image {','.join(scu.LOGOS_IMAGES_ALLOWED_TYPES)}", ) ], ) @@ -185,7 +185,7 @@ class DeptLogosConfigurationForm(FlaskForm): validators=[ FileAllowed( scu.LOGOS_IMAGES_ALLOWED_TYPES, - f"n'accepte que les fichiers image {','.join([e for e in scu.LOGOS_IMAGES_ALLOWED_TYPES])}", + f"n'accepte que les fichiers image {','.join(scu.LOGOS_IMAGES_ALLOWED_TYPES)}", ) ], ) @@ -193,36 +193,96 @@ class DeptLogosConfigurationForm(FlaskForm): submit = SubmitField("Enregistrer") -@bp.route("/config_logos", methods=["GET", "POST"]) -@permission_required(Permission.ScoChangePreferences) -def config_logos(scodoc_dept): - "Panneau de configuration général" - form = DeptLogosConfigurationForm() - if form.validate_on_submit(): - if form.logo_header.data: - sco_logos.store_image( - form.logo_header.data, - os.path.join( - scu.SCODOC_LOGOS_DIR, "logos_" + scodoc_dept, "logo_header" - ), - ) - if form.logo_footer.data: - sco_logos.store_image( - form.logo_footer.data, - os.path.join( - scu.SCODOC_LOGOS_DIR, "logos_" + scodoc_dept, "logo_footer" - ), - ) - app.clear_scodoc_cache() - flash(f"Logos enregistrés") - return flask.redirect(url_for("scolar.index_html", scodoc_dept=scodoc_dept)) +# @bp.route("/config_logos", methods=["GET", "POST"]) +# @permission_required(Permission.ScoChangePreferences) +# def config_logos(scodoc_dept): +# "Panneau de configuration général" +# form = DeptLogosConfigurationForm() +# if form.validate_on_submit(): +# if form.logo_header.data: +# sco_logos.store_image( +# form.logo_header.data, +# os.path.join( +# scu.SCODOC_LOGOS_DIR, "logos_" + scodoc_dept, "logo_header" +# ), +# ) +# if form.logo_footer.data: +# sco_logos.store_image( +# form.logo_footer.data, +# os.path.join( +# scu.SCODOC_LOGOS_DIR, "logos_" + scodoc_dept, "logo_footer" +# ), +# ) +# app.clear_scodoc_cache() +# flash(f"Logos enregistrés") +# return flask.redirect(url_for("scolar.index_html", scodoc_dept=scodoc_dept)) +# +# return render_template( +# "configuration.html", +# title="Configuration Logos du département", +# form=form, +# scodoc_dept=scodoc_dept, +# ) +# +# +# class DeptLogosConfigurationForm(FlaskForm): +# "Panneau de configuration logos dept" +# +# logo_header = FileField( +# label="Modifier l'image:", +# description="logo placé en haut des documents PDF", +# validators=[ +# FileAllowed( +# scu.LOGOS_IMAGES_ALLOWED_TYPES, +# f"n'accepte que les fichiers image {','.join([e for e in scu.LOGOS_IMAGES_ALLOWED_TYPES])}", +# ) +# ], +# ) +# +# logo_footer = FileField( +# label="Modifier l'image:", +# description="logo placé en pied des documents PDF", +# validators=[ +# FileAllowed( +# scu.LOGOS_IMAGES_ALLOWED_TYPES, +# f"n'accepte que les fichiers image {','.join([e for e in scu.LOGOS_IMAGES_ALLOWED_TYPES])}", +# ) +# ], +# ) +# +# submit = SubmitField("Enregistrer") - return render_template( - "configuration.html", - title="Configuration Logos du département", - form=form, - scodoc_dept=scodoc_dept, - ) + +# @bp.route("/config_logos", methods=["GET", "POST"]) +# @permission_required(Permission.ScoChangePreferences) +# def config_logos(scodoc_dept): +# "Panneau de configuration général" +# form = DeptLogosConfigurationForm() +# if form.validate_on_submit(): +# if form.logo_header.data: +# sco_logos.store_image( +# form.logo_header.data, +# os.path.join( +# scu.SCODOC_LOGOS_DIR, "logos_" + scodoc_dept, "logo_header" +# ), +# ) +# if form.logo_footer.data: +# sco_logos.store_image( +# form.logo_footer.data, +# os.path.join( +# scu.SCODOC_LOGOS_DIR, "logos_" + scodoc_dept, "logo_footer" +# ), +# ) +# app.clear_scodoc_cache() +# flash(f"Logos enregistrés") +# return flask.redirect(url_for("scolar.index_html", scodoc_dept=scodoc_dept)) +# +# return render_template( +# "configuration.html", +# title="Configuration Logos du département", +# form=form, +# scodoc_dept=scodoc_dept, +# ) # -------------------------------------------------------------------- diff --git a/tests/ressources/test_logos/logo_A.jpg b/tests/ressources/test_logos/logo_A.jpg index 7f107a05b6075d40551a7f7e695b06ec9f0763bf..7770372c7fe210abc450119be75920133a5eba98 100644 GIT binary patch literal 3755 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 literal 39157 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 literal 0 HcmV?d00001 diff --git a/tests/ressources/test_logos/logo_C.jpg b/tests/ressources/test_logos/logo_C.jpg index bdca2b7360bc69c8a9fdbb0950233b369416d7a4..108afc713d582959a8d58d469e96c04bf5a38c9d 100644 GIT binary patch literal 3071 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 literal 2417 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�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 literal 15640 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 F{{>~3qf!6> diff --git a/tests/ressources/test_logos/logo_F.jpeg b/tests/ressources/test_logos/logo_F.jpeg index c3750e29f2e44516d028f96c0a1a3ce181a42f63..20728130a97fdc5af7463fcd4e7ccf4da3038c64 100644 GIT binary patch literal 3002 zcmb7Fc|25m8$Yv{8JQ2ql8~6mHcFTo*9r8Z9i0*@TtCiilukB_ze9w#g|fY?G6h$0=*7;kIii$;%V;)HSq; zIyyRvY6iyoBqL3-4r%=#FkxX~tO!;{hJ=^Llm2g7dj^POfJh(#0oxA1#bF3>*jh6n z2LNycg!CVRBLEl*DFo?cAQRTD_=n&y0D)W^07 ztz)C#o6`H{zx1y+=!d{;zcQ%AJ2&%*-rAKRLqJ5t0Mbaf+S->B#$pFHH8JS7!tJgj2j zW}$mWwb!DuL$_A-jjxwGz8>?CiRsFplYe5`RIf%DU@uCYf4Et<)Z|-Os1*Ry_VLUS=_6=gLv# z?3YnW2bcNtCuG{HJUtKcPG)uWNzZUn@ff;z(fO|zWLk+cDXUqZq9DO;v8PE*kRW}2 zcHm|i2#NYBKd9IfG7AwH?yvWavzuBa!@UD}mg9JB=N(qzl`Ki8% zjOPZXcFZ2=knQ473LxQ7sr@P@C

l35YA)eN30pt$NFH0h)O37v zoElin(7$v%_NhJL=H&XKHg`Qv_4|=LpFbD&rWVNu|53_2&`T=@q03Gdbcj(hJXg&d`B+hCKO4Z~DqC7{ zS0kRh;}bH9%iKTioGCD@_Ivv7X2>7C*^agE#1SE6Id`sZ>J+!9p$d9-3*6E@zN972 zROCC>j(ju>0S7au6OMu$oAMI6r-%dpN`=K|;j0PuEn;#jk~yNd=myu3F5WGEPTgLn z7vc46K%Gf+P3f6SpJpN!YWT`mU__pnqi9R4rSHC`e2mGMt}7$z_op1Sp(R^`X+s~~ zRL=mizxDaZY4sxQrV5jD4|1ke1|Hq6dG@uuaAhz3%PGi6icp1;0@6&tQ98axql4gg){F|1m zsN9FEGi{4L)S4l~*7?`EqNPuFr<-Fs+O&A~@fxkfMSzag=H}b`c50M$oo@hLqPJE} zJs-5vC|5j%p+(C&UA7My39^Ee?$Ios*ldqekKL5QznW>$HrL;t^GdmIlUmj_Qm>GU zlC|oesjC`$!?#H-C|!}v=BPi{F>rbzB5k-{lDrD+(JXmeo zs}ONL1~10iAcuv6`b=ct0H}J3U)`0!xxM zKN_$8X}<=2P^p4biF^9+ zyfDS~sUY)FQE*X;D@VgSp+^>g3ygYWOt z9}I(LXkm@APFw`I!_&Xo)1M37)gkKfKIpi(D!FScMOr{` zOk+~3pHxBb(MbY6X|ZR@&&B=cbDMkA8&20aXdm-st+4t>hlo}Kx59{`xe3QR#*(F2 zj+!D~jb1fX!;an}ZjEOG@hp?lFbJv=2>ETDehm!Bb2vOmq%mB=wb|ElOMlfG7_4%8 z6hs0DQ)K%Inu3fj!aV@BvIZV4>(O3I&@O(tLI5; zfRNd2o7M|TrJYH4YIfJ=?bTk_D?>la{Z6t``LRDE_WiTaFCO1y0Y1unsVMtxCBLa1 z*R^Fl&;Pi2q!=zzTQTnP(#Zq%iTw+u%yF&Z&OGOggH7_peFt~M$Ty6``bX=ch9(Dg N_-{6;T*+A*{0|En0fhhn literal 2882 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 AEdT%j literal 2730 zcmb7FdpuO>8$V}mhz~PvjZBy!x5Ug44KeOkL{^iKVa&P?)o3hA?5w1b*wHpaC22%a zxkvb|+zSzMY3ZUZVkaTWrgrv>t=-?}_uub1pYwj6_kG^;p6~fS&-Xc!amjN4z01|z z6#zjH2!Sp@G66UNvM7`c3MnfiBO@m#yFo!!L0(=#Ls>;hb@L`|t<9ToI2}Dx108~i zE)HjCWn@AmSy)(T8&GU_kawDzTacw^fO2wj3i1kAEE$i(lmBm%v;i1dAPLBVg9HGE z0pS=>(gA1!AOM3yQ2#Ly21g)KfD9DW00AiUUok)$1dx(n0R=ep92$-W0C2pore~RD zBEQd*?H}@@`d*8Dz5|TSR(4uu$1QNepZM>-WZ6OR2iq{0?Nb~rWEIhdwi)MmF%$VC zRD(eD76oH@vp+tBxkn&lJxG6u)pH_|WgWO3cd(Bh)Sru6s=G`|q6_)O3BP6+@)=`( z8sApFvVQ6z8a=_Gf$c7}zr+@o3xzkqY~h~xn}rv;e-C+cZ89!sYy786W8bJ$W|uR| z&caGZdSonnXuS_c+uLymE?=pV5wlEiA^9Rx^#I?zbPw&sh=HGNXJ+j~Pek2QM4y4d zmmsuNwd~#?WM;Q8lEgO!yV z*qoij)Y}^(LruIc8Oi?OX>oX)p1eCOoDrMWJ*Z`On!T)Hb;$mh$MjQM32>z_10u!% z0~HJc^)D|_wZPzLj50zMPxfJu%vFd42gh|UO$X<4JrF3a!6!AH3_QO-?$0@Frswcj=Cwy34@CBPY6F7M+dgHfyKUBbpIZH}6? zkB;#pb@^euajR&Lj%(@He3bW}NlP{uJT)FZdsSh^^^nMF@ciosobX2TAjQ*5TS_6Q zx)^C@V5onkAk_s%8BZYjFrY*t$m=j+QkcCa4VTygI-|Fbl9XXae5Kj-KIdb#b)GI( z=SSJDVanspo=of{F&Wz$>fSHAO{A$?iw=Z5t6!SD>(0kjxz(z>Z9T}&Vj@CZ1$HK! z_FDC#;aCI)T0jTPIBRQ8CQQt&{U=y7^6_{;%V2l*L@BU zbC3L_A2n$1#cSm39*R8Tug$4<5iwtBOcdYxKFRcZzsu*v3p?er!2{LkXjXE84d>i# zr!DvUHxJ*)PL}sV28H|(OqnQ7?*=NGCKH1XrtWcQhj-7|aDMX7I#BUA7hG}J_x26F zR*yTEX_am(5V8ZkR}sVY>z9`i-^r(Ew%*$jVK|(nHcgiCH=|TpmuF+5 zMR)cl>`mB%m2-2p#t$m;{`~EEO0!Ctw8YTs4GMuoKn>|{!+>Z!iOnyn8eDYnVJzqn zxl>=2N5%K;v`Szl1aC~HEp)eO%O>L&Wjd$@3K3g_H?0Z?q;OHn#zEQ_8x$`Ux~%(j zH5xRbx=cYczjPXn)3mdIMhnb%KBPwT-*c3?okj~%e6Si%^P8%j_hmG4R`c6SrsC_L ztw@0R+FvIsX|$=?<{}!cL?pEPFv?s>DVaQ5V7nM(E_cHYXP5W-gK=5;FsseD{EAWh z7%F^iLu*_O{kIYloAz*iisr|ykS!|{A0Qz3@ee+huJ1)`VJj!kfP!THu3i>N0r2W7~OaX*iq@y(9(;x6bPPSSV0 zoxx33I&zH1QmKkoS>R_78!`;vLFpu5qz6b<_%)jVh{2N>{HX;8A9fYDXg!nCW`XWi z5T?0T6^{?aL|Ix~*tGkMe!G)8-+S#b$Z-GSuX{NLkY1wB6Xgz+ZUleil=m ziG3%I5375fS0mjJ+Q-Cp7qZH?4LCx3@s2nF`%M6e<=2~Ggy03fW>s6kN4KIiURx~1D3^)t6nITOj%`%I zc10Y(_BV^Vs*a%i&F0?`W%t@ry0cdEFE%%O;q*=SyDFwsB#b^?lj3=~`^m$+F2ntS zM-eZpkKanxV!vQiam5ikwc-md=1l987VBRLAjJGPF{Wp+{V0w7-%Ef4iRmgclpo%n zlmNAVc7kJ+v|GrAkx;vq=0EZtlr}*uXnEH)S$i_cMh>&W)8(5I0kMjbLW9j20YmP| z5{HqXj^B8iR|&scY5&}Bl$l1r-Kp$js;8)T%#!l?ce8PakAHIw$bb#lu}E3paljX# zSimNtyh}|feH|8_w{`6``e96U|JaeCudJJNDTYnW{+$4b0A&qNdH?_b diff --git a/tests/ressources/test_logos/logos_1/logo_B.jpg b/tests/ressources/test_logos/logos_1/logo_B.jpg index 2ebb82823e3db7f757b5d29322b1e9efb0db2c94..2428dc4521df51b21fd3279a7dbcf4edf90dbdd9 100644 GIT binary patch literal 3061 zcmb7Fc|4Ts7k_6lGcq4uOJy5ov6V5Ip|ohS6qlk@6tYt)C7PQ=Bzuu8WhrWuEpCf_ zxzuniS>ujmY**QhNn9E0{9fE{_ut=n-_QA+=bZO>&Uw!AeFdKce*vJWk%u9V7RGGCN?}Drurd;oVp2F+MFpI!ygXisqKYSyN%Hb)I@`#a zRBdf-Mb$m~chht!TH3U=Gho8P!dMZkv^0$%PoVwZB&Y_&F+ea7g@6$OxHt?U4io$V z$N~Ty0Y&aPN@v#25Cr zoUX3q4z=$Y)nz$8+T8EK^wA2Xp@c-`2g>iqPQ-SFc9(ZNY?#bEeF-&tA5?wJJ2H6$fM`fA*{O zN!ly6!*$7ECG??lospbxs<{KRo%X19_SI9$=+-+C_i{JO`H`bCBnx&WpY>?GIAdoJ zt6C9~L(Wm@RN|QIDpToF;_%G!hIF80s)7IZ2!n7OdO6t3Q2(yl6aanRckX* zSqaK52Ff^02%a8lGuFY#*?Tfoo6+8I@1Qp+Rz70Ln7G${{8@%c$Md$A6*^r0l{gcn z^_RhzmKJ;`z&?LB4Dc8}Qo#UTWY5p%wMx{exk7G0up z*!B{|_ZXyvKBr4V$}B}nXWN+EnOt5=ibszD7(zO>G{$$4uCKzkcvT~}P7 z*D8=*KDSnmMtb$XxtcPy#4qZ5DFB?svzN9kJGjbNGQM4~JSG4<^6u*L=e36>3UJx% z%DVx&j=?0{kX+!Leqp(7ky-8)Hb$ihL+5xl7CO zH}89T^Ynjj&`E3=p7r(bchE^j&ExxrT_aV1*)|U6o89e%>)03TLb$cLtnQVrPT_Yc(KjvNNIK7@ zrYv$4#UrubCu^y=HYdl^&Z-QYgM5%6A20;+Kb-*ifJ;yj1eyg>5+rKrue*V-xw-V7 z{P3`ja@_l~A?5Q52ccW)wKJQ2!C1Lyqc4UsYhqYc1H&MmPfgLSkHr)pHrfzMOJk!x zjOa!@Nme=+)9ky^403;ONu>sN9X*urxAg`yewh|yhZ9Y)8droO+XVtJI6?@CL8AZ3 z033jT!dn7QW4@tk#l<5*NrIk%g%?pjJ)fkh?c^N$ZRVezMIC?>iIMxmm~+mN56QvjnuGfz`m?)Eo`TVDLjw2?!8j# z?VT}4()o9nD%3o?lMKTLz7=LpXzEI;8@t;*Dw8+noc-#5{;2hZ`6NFR!&z+cOHP0- zxzC)`5y^bpcp`8LBU)Fbt(sR92#5IkjUxc>EjkMUlfLm9BS%V-Y5lHE3P;`y{fGU> z0*PNc8hbq>ig!T8QvuhI1BUsD=P*Eg8c(3=TQJ|WgDfYnpxD750CRxQ_qv|-7{%yH zW-FZ2TS#>o={Ruh*;0{W-_zA8v3d6c6&5R;HlB7&(nq?B-a@q3h@Cu1W%+Hg4Y@$N z-W?7)g-t0~H<9z3s2m^|tJzo5r0zz}Z@#8yfYof|(yb?bhasbuPyt1gCjiKEpuNRn z&GN8Dw{Dbv*zpIS{EH*IwwYSHrR64152EE)Y_sR@*KG4oEor)d3l!RBY8$o5Cyd)> zmitYFetFmA63UKer(7u7GhB49^8WE6UV%`<=;}ZtwHWO-!>PCbBom&j760oZo9pmk zk!bAImizSDfJWZ0Q!u)7f6uWfvL-@xSY5&3hlMSqOmZ7_sVGhAd|Y9L{Pl_{y6 zj%=wZ#IvN^4vSAU$*#r?I_-9}e+^AsD98VG$85EUQZhft*@};@9TWhrEg6qu8(6BS zN6l@g9z@Ww0)Pl1kVx@@i$|OZHGKQ9g1tqs?@Yhk?6g-IipRZ165tq0=WvO$f%9yDhN5N0L{X(x_>0ato7G&0&hv}E1zP4# z6z;bwdC@UvQw6EqZ{WQhd#_(!4fqO`bp3w}ARw>;C+_Y0lQ0sf*f8O1{Ff*=Z}-h6 zNR0E9io*DM-H7h$mhO2{jq;R^AG|Anvd89`-}}?M$0e>{=nM4(F{p_Y#n=A$YtXcI z+8RMkM4*BePG0(~>GkeegFg_vJHErPGqm;(boPF-EMbsb+~c9yD2B{$kXw*!$7=HL zW!kDNr!ssnF*59CrdDa-J8|(ZdzIpLU7z;G=)zb^iLfWUtBarND@q)1Ywk92duWXW zgHG5^3|K7=tnsD%q+%!_A`}pWK@eJQgtHNlQ!Gfe@M2EmS?%jFtzjkDHt1KM8l$dY zOTWflu%0boS(9G%5^<>gFn*0gDMLPz=Mww@43Qla!TUvu`ch$ zuo+f2jH#TmdPYll(Z#b6Y7H2F(wo!s)pID=z&Wut?X8z6Gs?MrJWlG0Ek)!K>r!cP zukB?KH_zV&bO=-d9WIg@x;@79~qpq~k|L&)wo}Dd-iOxZbQ` z+U~PG=JVud;p{WIx3m3K=$j}>y*O7y#Ag}n^d>Yrec(^;K^ysHodIGWMQ4^MU}pMqDeWbibGLC zc$e{Z=s|`-PGKV(cOQ(SpWzb@tSs23jlXR8=~C!E3kCuhb|YNiY114 zhp@>djVGp$7G}B<`<+&|t6tN%p*e??xxIYSy!n@DrwNP}jGrBN)^*N(>ZtnW5#n-l jx$;6NmRcia3C|8{7RAe(i$s{cv^$>VbaIb_mZ1B8HxVjM literal 2832 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 Xrl?*&eLha9q*r$u@mGi+S9|{lD=b$E diff --git a/tests/ressources/test_logos/logos_2/logo_A1.jpg b/tests/ressources/test_logos/logos_2/logo_A1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5bdf7500cc42ae84339f70eb11df4ad8731df05d GIT binary patch literal 3501 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 literal 0 HcmV?d00001 diff --git a/tests/unit/test_logos.py b/tests/unit/test_logos.py index 1ed62a49..6af36648 100644 --- a/tests/unit/test_logos.py +++ b/tests/unit/test_logos.py @@ -7,7 +7,6 @@ Utiliser comme: pytest tests/unit/test_logos.py """ -from io import BytesIO from pathlib import Path from shutil import copytree, copy, rmtree @@ -18,7 +17,14 @@ import app from app import db from app.models import Departement import app.scodoc.sco_utils as scu -from app.scodoc.sco_logos import find_logo, Logo, list_logos +from app.scodoc.sco_logos import ( + find_logo, + Logo, + list_logos, + GLOBAL, + write_logo, + delete_logo, +) RESOURCES_DIR = "/opt/scodoc/tests/ressources/test_logos" @@ -30,12 +36,15 @@ def create_dept(test_client): """ dept1 = Departement(acronym="RT") dept2 = Departement(acronym="INFO") + dept3 = Departement(acronym="GEA") db.session.add(dept1) db.session.add(dept2) + db.session.add(dept3) db.session.commit() - yield dept1, dept2 + yield dept1, dept2, dept3 db.session.delete(dept1) db.session.delete(dept2) + db.session.delete(dept3) db.session.commit() @@ -52,9 +61,10 @@ def create_logos(create_dept): +-- logos_{d2} --+-- logo_A.jpg """ - dept1, dept2 = create_dept + dept1, dept2, dept3 = create_dept d1 = dept1.id d2 = dept2.id + d3 = dept3.id FILE_LIST = ["logo_A.jpg", "logo_C.jpg", "logo_D.png", "logo_E.jpg", "logo_F.jpeg"] for fn in FILE_LIST: from_path = Path(RESOURCES_DIR).joinpath(fn) @@ -83,33 +93,33 @@ def test_select_global_only(create_logos): def test_select_local_only(create_dept, create_logos): - dept1, dept2 = create_dept + dept1, dept2, dept3 = create_dept B_logo = app.scodoc.sco_logos.find_logo(logoname="B", dept_id=dept1.id) assert B_logo.filepath == f"{scu.SCODOC_LOGOS_DIR}/logos_{dept1.id}/logo_B.jpg" def test_select_local_override_global(create_dept, create_logos): - dept1, dept2 = create_dept + dept1, dept2, dept3 = create_dept A1_logo = app.scodoc.sco_logos.find_logo(logoname="A", dept_id=dept1.id) assert A1_logo.filepath == f"{scu.SCODOC_LOGOS_DIR}/logos_{dept1.id}/logo_A.jpg" def test_select_global_with_strict(create_dept, create_logos): - dept1, dept2 = create_dept + dept1, dept2, dept3 = create_dept A_logo = app.scodoc.sco_logos.find_logo(logoname="A", dept_id=dept1.id, strict=True) assert A_logo.filepath == f"{scu.SCODOC_LOGOS_DIR}/logos_{dept1.id}/logo_A.jpg" def test_looks_for_non_existant_should_give_none(create_dept, create_logos): # search for a local non-existant logo returns None - dept1, dept2 = create_dept + dept1, dept2, dept3 = create_dept no_logo = app.scodoc.sco_logos.find_logo(logoname="Z", dept_id=dept1.id) assert no_logo is None def test_looks_localy_for_a_global_should_give_none(create_dept, create_logos): # search for a local non-existant logo returns None - dept1, dept2 = create_dept + dept1, dept2, dept3 = create_dept no_logo = app.scodoc.sco_logos.find_logo( logoname="C", dept_id=dept1.id, strict=True ) @@ -123,8 +133,8 @@ def test_get_jpg_data(create_dept, create_logos): assert logo.logoname == "A" assert logo.suffix == "jpg" assert logo.filename == "A.jpg" - assert logo.size == (1200, 600) - assert logo.mm == approx((40, 30), 0.1) + assert logo.size == (140, 121) + assert logo.mm == approx((5.74, 4.96), 0.1) def test_get_png_without_data(create_dept, create_logos): @@ -139,10 +149,59 @@ def test_get_png_without_data(create_dept, create_logos): assert logo.mm is None -def test_create_globale_jpg_logo(create_dept, create_logos): +def test_delete_unique_global_jpg_logo(create_dept, create_logos): + from_path = Path(RESOURCES_DIR).joinpath("logo_A.jpg") + to_path = Path(scu.SCODOC_LOGOS_DIR).joinpath("logo_W.jpg") + copy(from_path.absolute(), to_path.absolute()) + assert to_path.exists() + delete_logo(name="W") + assert not to_path.exists() + + +def test_delete_unique_local_jpg_logo(create_dept, create_logos): + dept1, dept2, dept3 = create_dept + from_path = Path(RESOURCES_DIR).joinpath("logo_A.jpg") + to_path = Path(scu.SCODOC_LOGOS_DIR).joinpath(f"logos_{dept1.id}", "logo_W.jpg") + copy(from_path.absolute(), to_path.absolute()) + assert to_path.exists() + delete_logo(name="W", dept_id=dept1.id) + assert not to_path.exists() + + +def test_delete_multiple_local_jpg_logo(create_dept, create_logos): + dept1, dept2, dept3 = create_dept + from_path_A = Path(RESOURCES_DIR).joinpath("logo_A.jpg") + to_path_A = Path(scu.SCODOC_LOGOS_DIR).joinpath(f"logos_{dept1.id}", "logo_V.jpg") + from_path_B = Path(RESOURCES_DIR).joinpath("logo_D.png") + to_path_B = Path(scu.SCODOC_LOGOS_DIR).joinpath(f"logos_{dept1.id}", "logo_V.png") + copy(from_path_A.absolute(), to_path_A.absolute()) + copy(from_path_B.absolute(), to_path_B.absolute()) + assert to_path_A.exists() + assert to_path_B.exists() + delete_logo(name="V", dept_id=dept1.id) + assert not to_path_A.exists() + assert not to_path_B.exists() + + +def test_create_global_jpg_logo(create_dept, create_logos): path = Path(f"{RESOURCES_DIR}/logo_C.jpg") - logo = Logo("X") # create global logo stream = path.open("rb") + logo_path = Path(scu.SCODOC_LOGOS_DIR).joinpath("logo_X.jpg") + assert not logo_path.exists() + write_logo(stream, name="X") # create global logo + assert logo_path.exists() + logo_path.unlink(missing_ok=True) + + +def test_create_locale_jpg_logo(create_dept, create_logos): + dept1, dept2, dept3 = create_dept + path = Path(f"{RESOURCES_DIR}/logo_C.jpg") + stream = path.open("rb") + logo_path = Path(scu.SCODOC_LOGOS_DIR).joinpath(f"logos_{dept1.id}", "logo_Y.jpg") + assert not logo_path.exists() + write_logo(stream, name="Y", dept_id=dept1.id) # create global logo + assert logo_path.exists() + logo_path.unlink(missing_ok=True) def test_create_jpg_instead_of_png_logo(create_dept, create_logos): @@ -169,15 +228,17 @@ def test_create_jpg_instead_of_png_logo(create_dept, create_logos): def test_list_logo(create_dept, create_logos): # test only existence of copied logos. We assumes that they are OK - dept1, dept2 = create_dept + dept1, dept2, dept3 = create_dept logos = list_logos() - assert logos.keys() == {"_GLOBAL", "RT", "INFO"} + assert set(logos.keys()) == {dept1.id, dept2.id, None} assert {"A", "C", "D", "E", "F", "header", "footer"}.issubset( - set(logos["_GLOBAL"].keys()) + set(logos[None].keys()) ) - rt = logos.get("RT", None) + rt = logos.get(dept1.id, None) assert rt is not None assert {"A", "B"}.issubset(set(rt.keys())) - info = logos.get("INFO", None) + info = logos.get(dept2.id, None) assert info is not None assert {"A"}.issubset(set(rt.keys())) + gea = logos.get(dept3.id, None) + assert gea is None From 23f1dc4ed23d6938bc39fe068f53b62ccd0a51ed Mon Sep 17 00:00:00 2001 From: Place Jean-Marie Date: Sat, 11 Dec 2021 16:07:25 +0100 Subject: [PATCH 14/19] remove toolbar --- app/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/__init__.py b/app/__init__.py index 969c9766..0943a91f 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -33,7 +33,6 @@ from app.scodoc.sco_exceptions import ( ) from config import DevConfig import sco_version -from flask_debugtoolbar import DebugToolbarExtension db = SQLAlchemy() migrate = Migrate(compare_type=True) @@ -188,7 +187,6 @@ def create_app(config_class=DevConfig): moment.init_app(app) cache.init_app(app) sco_cache.CACHE = cache - toolbar = DebugToolbarExtension(app) app.register_error_handler(ScoGenError, handle_sco_value_error) app.register_error_handler(ScoValueError, handle_sco_value_error) From 8a5c4b5cedd864b9266f76964493185cfcc8cc01 Mon Sep 17 00:00:00 2001 From: Place Jean-Marie Date: Sat, 11 Dec 2021 17:05:38 +0100 Subject: [PATCH 15/19] mise a jour tests + images de test --- app/scodoc/sco_config_form.py | 4 ++-- tests/ressources/test_logos/logo_A.jpg | Bin 3755 -> 3767 bytes tests/ressources/test_logos/logo_A1.jpg | Bin 3954 -> 3969 bytes tests/ressources/test_logos/logo_C.jpg | Bin 3071 -> 3084 bytes tests/ressources/test_logos/logo_D.png | Bin 21297 -> 21393 bytes tests/ressources/test_logos/logo_E.jpg | Bin 2938 -> 2948 bytes .../ressources/test_logos/logos_1/logo_A.jpg | Bin 3218 -> 3234 bytes .../ressources/test_logos/logos_1/logo_B.jpg | Bin 3061 -> 3073 bytes .../ressources/test_logos/logos_2/logo_A.jpg | Bin 3241 -> 3260 bytes .../ressources/test_logos/logos_2/logo_A1.jpg | Bin 3501 -> 3519 bytes tests/unit/test_logos.py | 6 +++--- 11 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/scodoc/sco_config_form.py b/app/scodoc/sco_config_form.py index d5ee87a9..f7ff47d7 100644 --- a/app/scodoc/sco_config_form.py +++ b/app/scodoc/sco_config_form.py @@ -126,7 +126,7 @@ class AddLogoForm(FlaskForm): validators.Length( max=20, message="Un nom ne doit pas dépasser 20 caractères" ), - validators.required("Nom de logo requis (alphanumériques ou '-')"), + validators.DataRequired("Nom de logo requis (alphanumériques ou '-')"), ], ) upload = FileField( @@ -136,7 +136,7 @@ class AddLogoForm(FlaskForm): scu.LOGOS_IMAGES_ALLOWED_TYPES, f"n'accepte que les fichiers image {', '.join(scu.LOGOS_IMAGES_ALLOWED_TYPES)}", ), - validators.required("Fichier image manquant"), + validators.DataRequired("Fichier image manquant"), ], ) do_insert = SubmitField("ajouter une image") diff --git a/tests/ressources/test_logos/logo_A.jpg b/tests/ressources/test_logos/logo_A.jpg index 7770372c7fe210abc450119be75920133a5eba98..1da64b66b645288b1ad96255f2f2293e4468a2f3 100644 GIT binary patch delta 84 zcmZ22yIppICL`}eO>1^uUI`;b8O4c(S`!;MHqPc|WaOQ^jM1Bsck@Tak8D8d5l075 hY;zWu07$Be`vVJaMH27k93B-Ae**6cAZfu@3;@{t7Zd;h delta 64 zcmV-G0KfmY9jhIXDUm8W1sF0R93hc$Es=l)v7QS7ld1trv-Sb;2D9i0hyk;43J?L4 Wf(!5hcw)153?u=Qn+>RwL=JP&z!t9n diff --git a/tests/ressources/test_logos/logo_A1.jpg b/tests/ressources/test_logos/logo_A1.jpg index e645ef63d2a340711d54626328df0b72b5d186d8..66ecf45f2b80ecbb56657586b506613fd1d9dcca 100644 GIT binary patch delta 127 zcmew)*C;jKrPShj}M=^1TEACk!gE delta 104 zcmV-u0GI!PAMzfMDUm8W1sF0R93hc$Es=l)v7VO!vo-=z0<(w&KLNAI1-t>Xg$E@8 zv#SV80<-rD1Ok)%3{(h~sH>)D{{Zl_S`8rq8e_6%JPvBLo1DJ zUTdThWTJ)+To(y73t!7D)2zTG%_)SUDE}@_ZDa%BL4x_8~zLHxN{F3qgt-3J!2p;0E^%*u|5A<2(b% z=EFN3$``aWqmBC=Mfj|#NlPR&6%_^}~(c)^LMVY3iel;JpM zKT?VRi6FwsJcP#9=N@IAb(fYW)LTS-t3xAiJEnqH&m(F+N(%@5qw&DyXjqtPiMas^ zR14siYD9$IAqnSie2?3k{N`m5$uF}+gJor{h;qf&LWwD^0;ag)lz9EE_>*87c@!)q zn?Y~&Ij}^%AVG7A-0jg@dHKhxYj6J>JGlL_q3Xb|{asR5gs7r~i%>R|-~cNUd%zG) z2K2q6vEjai_669k&5$EIv)a$a2}0?@ChP2I;dmkyp}y94sd?a|u{d}>7Ev7CJ*aKc zJqDHfPH;va2JYx7us$glTujpNP}-9R!PEP@A(4CQcipdGVsaHsY)fv(6Vp5r!?>X>}eIUIz(HuYw%ZAj*|Ef9aZ-X delta 546 zcmV+-0^R+Qrvb610SbvwPDctF3IG5Akqqv!`@sUUy#u-dlXwMHv(5$S0+YB1Ewknb z!~qo?R4moXs3IQOF_Hs#u%<%e$gLC5v%v~k1CtvLB9lH2u#@i&Hj^d~ACpE8T$7^@ zCzHz$GL!xfypy{S9h2D*MUx*99kV?V5&^SN5zCtFBg*sFg%k)FwBz?F}ah=F?W+g zGToCQGr|?W91Q$5tl-SCfA>E0e1> znv+I1VUwsg-5S^;Q}M%nRaQHGhJXLW2l1(od<5VA`8GXclc6|jlMFenle#&qlV>_g zv$8so0h2^Lqm$@7N*+l0T80*p^j4EKzl{OA-yP@lHjwSMUMlMFG)p{bxEGH6iTrHv&Kt50h0zzD6=n3 kg8{R%P96de8iC?W1^uUI`;b8O4c(S`!;MCeB{R$h+B-QI-)%r7>*-Q8FwOfYjtQ ztU8*!8mslT=Qv58mtEyQ^V|OHAN9UJ&ffuv?sLg>jV#^gn=&ij?D$>Y$^TfN1NA&* g+YKZau-gL3yX;Xw!^}B&n1GZs7bB3G?84&*03?$w9{>OV delta 95 zcmV-l0HFV(8Il>0DUm8W1sF0R93hc$Es=l)k)E!zO#vMNvugsj0ka|mkO7mk1uT=` z1r-q}t1iAoX+>FezD;DBUpB|xlL!Xqlkf(+ldK0llj;XrvqcCD0<%&I0Rod$3{EbG BA*lcW diff --git a/tests/ressources/test_logos/logos_1/logo_B.jpg b/tests/ressources/test_logos/logos_1/logo_B.jpg index 2428dc4521df51b21fd3279a7dbcf4edf90dbdd9..7c1708f15a85d104683f1677327619413a634af8 100644 GIT binary patch delta 86 zcmew=-Y79alaY6#rZqb+uY{4JjN-&Xt%(gB8)q{zGxAQJ!yL-UyZIw?IU|tDV08sj ilh?A{2a-MPWBqS delta 64 zcmV-G0Kfl%81)yBDUm8W1sF0R93hc$Es=l)v7P|~lb{1tv-Sgb0kdueMFEqv2H%sE W2R4(-2iB9K2zs*}36}zswF^wqoEJy{ diff --git a/tests/ressources/test_logos/logos_2/logo_A.jpg b/tests/ressources/test_logos/logos_2/logo_A.jpg index 28d2a9c14a720169ca65555edd038e18292699c4..130269a63e74322b6f8fa1b1c37cf2178e08d78b 100644 GIT binary patch delta 141 zcmZ1}xkqw>CL`}eO>1^uUI`;b8O4c(S`!;MCeGf#$UE7a@idSWWAb3+-5kesfDuT^ zu}ooP?0VW{D#3{=t%v;dI7VUqMB`o>b;msfP-n%jL mPB!2=095;dOAkoOaR&j(0`3@2-sA%UCC5}2@lL+XeGLHkFe&)} delta 111 zcmV-#0FeK@8L1hNDUm8W1sF0R93hc$Es=l)k)E)VPXWu58v;nPWCFkevn2$S0R;a5 z6xNf!1#6Ql2GO(q1`q93NDi_3sRGT R3tR_dzz}rCB%+h%3)Yk&DTDw3 diff --git a/tests/ressources/test_logos/logos_2/logo_A1.jpg b/tests/ressources/test_logos/logos_2/logo_A1.jpg index 5bdf7500cc42ae84339f70eb11df4ad8731df05d..ee9da2ec48e3beb3c74b53e16f5cbc17185f7a40 100644 GIT binary patch delta 165 zcmZ20y1^uUI`;b8O4c(S`!;MCeB{R$UE7SF%d|vVmt&SbC|9GNeAXx z!n|(}?%iQNo$cf2il2LG?%Cz*@os+2ypRzn-NWhuB(JdU2a=Q7E&$17_6Ll-o1Hl( zumCxidFu6e3rtVkefD>Y;3ZDaRNZ+}Q_Vj=T_3*h!+!=*;oaGBJyuP;5B7@+Z_bqC L0Wv4o@)`gDskl81 delta 136 zcmV;30C)et8?76VDUm8W1sF0R93hc$Es=l)k)E!TO#xw(uK~i7ast$oM+2G>?ZLgc zHcH}mDMSHAH702LFvYhH{xg5bXu6SHX^3_607j}0) Date: Sat, 11 Dec 2021 17:20:34 +0100 Subject: [PATCH 16/19] disable cache for logos objects --- app/views/scodoc.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/views/scodoc.py b/app/views/scodoc.py index d875dc8e..c46c4b39 100644 --- a/app/views/scodoc.py +++ b/app/views/scodoc.py @@ -30,6 +30,7 @@ Module main: page d'accueil, avec liste des départements Emmanuel Viennet, 2021 """ +import datetime import io import wtforms.validators @@ -231,7 +232,11 @@ def _return_logo(name="header", dept_id="", small=False, strict: bool = True): stream.seek(0) return send_file(stream, mimetype=f"image/{fmt}") else: - return send_file(logo.filepath, mimetype=f"image/{suffix}") + return send_file( + logo.filepath, + mimetype=f"image/{suffix}", + last_modified=datetime.datetime.now(), + ) else: abort(404) From 235ca69a8230a56b4711ffca15feee7a5f2f2740 Mon Sep 17 00:00:00 2001 From: Place Jean-Marie Date: Sat, 11 Dec 2021 17:55:08 +0100 Subject: [PATCH 17/19] essai avec last_modified --- app/scodoc/sco_logos.py | 5 +++++ app/views/scodoc.py | 6 +++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/app/scodoc/sco_logos.py b/app/scodoc/sco_logos.py index 3f5917bb..8aa88c53 100644 --- a/app/scodoc/sco_logos.py +++ b/app/scodoc/sco_logos.py @@ -269,6 +269,11 @@ class Logo: else: return f'' + def last_modified(self): + path = Path(self.filepath) + dt = path.stat().st_mtime + return path.stat().st_mtime + def guess_image_type(stream) -> str: "guess image type from header in stream" diff --git a/app/views/scodoc.py b/app/views/scodoc.py index c46c4b39..301dfbb4 100644 --- a/app/views/scodoc.py +++ b/app/views/scodoc.py @@ -215,7 +215,9 @@ SMALL_SIZE = (200, 200) def _return_logo(name="header", dept_id="", small=False, strict: bool = True): # stockée dans /opt/scodoc-data/config/logos donc servie manuellement ici - logo = sco_logos.find_logo(name, dept_id, strict) + # from app.scodoc.sco_photos import _http_jpeg_file + + logo = sco_logos.find_logo(name, dept_id, strict).select() if logo is not None: suffix = logo.suffix if small: @@ -232,6 +234,8 @@ def _return_logo(name="header", dept_id="", small=False, strict: bool = True): stream.seek(0) return send_file(stream, mimetype=f"image/{fmt}") else: + # return _http_jpeg_file(logo.filepath) + # ... replaces ... return send_file( logo.filepath, mimetype=f"image/{suffix}", From 4aa4ab316cb5207c9d3b835fa42b9eceeba86860 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 12 Dec 2021 22:11:11 +0100 Subject: [PATCH 18/19] =?UTF-8?q?petits=20d=C3=A9tails=20(messages)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/scodoc/sco_config_actions.py | 6 ++---- app/scodoc/sco_config_form.py | 6 +++--- app/scodoc/sco_logos.py | 34 ++++++++++++++++++++------------ 3 files changed, 26 insertions(+), 20 deletions(-) diff --git a/app/scodoc/sco_config_actions.py b/app/scodoc/sco_config_actions.py index bf633a26..b9157489 100644 --- a/app/scodoc/sco_config_actions.py +++ b/app/scodoc/sco_config_actions.py @@ -26,9 +26,7 @@ ############################################################################## """ -Module main: page d'accueil, avec liste des départements -Emmanuel Viennet, 2021 """ from app.models import ScoDocSiteConfig from app.scodoc.sco_logos import write_logo, find_logo, delete_logo @@ -101,7 +99,7 @@ class LogoDelete(Action): def __init__(self, parameters): super().__init__( - f"Suppression du logo {parameters['logo_id']} pour le département {parameters['dept_id']}.", + f"Suppression du logo {parameters['logo_id']} pour le département {parameters['dept_id'] or 'tous'}.", parameters, ) @@ -128,7 +126,7 @@ class LogoInsert(Action): def __init__(self, parameters): super().__init__( - f"Ajout du logo {parameters['name']} pour le département {parameters['dept_key']} ({parameters['upload']}).", + f"Ajout du logo {parameters['name']} pour le département {parameters['dept_key']} ({parameters['upload'].filename}).", parameters, ) diff --git a/app/scodoc/sco_config_form.py b/app/scodoc/sco_config_form.py index f7ff47d7..993382de 100644 --- a/app/scodoc/sco_config_form.py +++ b/app/scodoc/sco_config_form.py @@ -26,9 +26,9 @@ ############################################################################## """ -Module main: page d'accueil, avec liste des départements +Formulaires configuration logos -Emmanuel Viennet, 2021 +Contrib @jmp, dec 21 """ import re @@ -36,7 +36,7 @@ from flask import flash, url_for, redirect, render_template from flask_wtf import FlaskForm from flask_wtf.file import FileField, FileAllowed from wtforms import SelectField, SubmitField, FormField, validators, FieldList -from wtforms.fields.simple import BooleanField, StringField, HiddenField +from wtforms.fields.simple import StringField, HiddenField from app import AccessDenied from app.models import Departement diff --git a/app/scodoc/sco_logos.py b/app/scodoc/sco_logos.py index 8aa88c53..0d73e301 100644 --- a/app/scodoc/sco_logos.py +++ b/app/scodoc/sco_logos.py @@ -25,7 +25,7 @@ # ############################################################################## -"""Gestion des images logos (nouveau ScoDoc 9) +"""Gestion des images logos (nouveau ScoDoc 9.1) Les logos sont `logo_header.` et `logo_footer.` avec `ext` membre de LOGOS_IMAGES_ALLOWED_TYPES (= jpg, png) @@ -104,7 +104,7 @@ def list_logos(): def _list_dept_logos(dept_id=None, prefix=scu.LOGO_FILE_PREFIX): - """nventorie toutes les images existantes pour un niveau (GLOBAL ou un département). + """Inventorie toutes les images existantes pour un niveau (GLOBAL ou un département). retourne un dictionnaire de Logo [logoname] -> Logo les noms des fichiers concernés doivent être de la forme: /. : répertoire de recherche (déduit du dept_id) @@ -164,16 +164,24 @@ class Logo: self.basepath = os.path.sep.join( [self.dirpath, self.prefix + secure_filename(self.logoname)] ) - # next attributes are computer by the select function - self.suffix = "Not inited: call the select or create function before access" - self.filepath = "Not inited: call the select or create function before access" - self.filename = "Not inited: call the select or create function before access" - self.size = "Not inited: call the select or create function before access" - self.aspect_ratio = ( - "Not inited: call the select or create function before access" + # next attributes are computed by the select function + self.suffix = ( + "Not initialized: call the select or create function before access" ) - self.density = "Not inited: call the select or create function before access" - self.mm = "Not inited: call the select or create function before access" + self.filepath = ( + "Not initialized: call the select or create function before access" + ) + self.filename = ( + "Not initialized: call the select or create function before access" + ) + self.size = "Not initialized: call the select or create function before access" + self.aspect_ratio = ( + "Not initialized: call the select or create function before access" + ) + self.density = ( + "Not initialized: call the select or create function before access" + ) + self.mm = "Not initialized: call the select or create function before access" def _set_format(self, fmt): self.suffix = fmt @@ -183,7 +191,7 @@ class Logo: def _ensure_directory_exists(self): "create enclosing directory if necessary" if not Path(self.dirpath).exists(): - current_app.logger.info(f"sco_logos creating directory %s", self.dirpath) + current_app.logger.info("sco_logos creating directory %s", self.dirpath) os.mkdir(self.dirpath) def create(self, stream): @@ -195,7 +203,7 @@ class Logo: filename = self.basepath + "." + self.suffix with open(filename, "wb") as f: f.write(stream.read()) - current_app.logger.info(f"sco_logos.store_image %s", self.filename) + current_app.logger.info("sco_logos.store_image %s", self.filename) # erase other formats if they exists for suffix in set(scu.LOGOS_IMAGES_ALLOWED_TYPES) - set([img_type]): try: From 13d0e462cccfa890437691b8b729e20ce3f3764c Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 12 Dec 2021 22:27:06 +0100 Subject: [PATCH 19/19] msg erreur si upload fichier invalide --- app/scodoc/sco_logos.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/scodoc/sco_logos.py b/app/scodoc/sco_logos.py index 0d73e301..d66646d4 100644 --- a/app/scodoc/sco_logos.py +++ b/app/scodoc/sco_logos.py @@ -44,6 +44,7 @@ from werkzeug.utils import secure_filename from app import Departement, ScoValueError from app.scodoc import sco_utils as scu +from app.scodoc.sco_exceptions import ScoValueError from PIL import Image as PILImage GLOBAL = "_" # category for server level logos @@ -136,7 +137,7 @@ def _list_dept_logos(dept_id=None, prefix=scu.LOGO_FILE_PREFIX): class Logo: """Responsable des opérations (select, create), du calcul des chemins et url - ainsi que de la récupération des informations sur un logp. + ainsi que de la récupération des informations sur un logo. Usage: logo existant: Logo(, , ...).select() (retourne None si fichier non trouvé) logo en création: Logo(, , ...).create(stream) @@ -197,7 +198,7 @@ class Logo: def create(self, stream): img_type = guess_image_type(stream) if img_type not in scu.LOGOS_IMAGES_ALLOWED_TYPES: - abort(400, "type d'image invalide") + raise ScoValueError("type d'image invalide") self._set_format(img_type) self._ensure_directory_exists() filename = self.basepath + "." + self.suffix