From 9fd019554fd17bfa65bd184832c9145585732705 Mon Sep 17 00:00:00 2001 From: jmpla Date: Thu, 16 Feb 2023 05:31:36 +0100 Subject: [PATCH 1/3] conserve le visuel sur les pages preferences --- app/forms/config_logos.py | 451 ++++++++++++++++++++ app/scodoc/sco_preferences.py | 11 +- app/static/js/detail_summary_persistence.js | 82 ++++ app/templates/config_logos.j2 | 7 +- 4 files changed, 546 insertions(+), 5 deletions(-) create mode 100644 app/forms/config_logos.py create mode 100644 app/static/js/detail_summary_persistence.js diff --git a/app/forms/config_logos.py b/app/forms/config_logos.py new file mode 100644 index 00000000..d01cc5cc --- /dev/null +++ b/app/forms/config_logos.py @@ -0,0 +1,451 @@ +# -*- mode: python -*- +# -*- coding: utf-8 -*- + +############################################################################## +# +# ScoDoc +# +# Copyright (c) 1999 - 2023 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 +# +############################################################################## + +""" +Formulaires configuration logos + +Contrib @jmp, dec 21 +""" + +from flask import flash, url_for, redirect, render_template +from flask_wtf import FlaskForm +from flask_wtf.file import FileField, FileAllowed +from wtforms import SubmitField, FormField, validators, FieldList +from wtforms import ValidationError +from wtforms.fields.simple import StringField, HiddenField + +from app.models import Departement +from app.scodoc import sco_logos, html_sco_header +from app.scodoc import sco_utils as scu + +from app.scodoc.sco_config_actions import LogoInsert +from app.scodoc.sco_exceptions import ScoValueError +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 + + +def logo_name_validator(message=None): + def validate_logo_name(form, field): + name = field.data if field.data else "" + if "." in name: + raise ValidationError(message) + if not scu.is_valid_filename(name): + raise ValidationError(message) + + return validate_logo_name + + +class AddLogoForm(FlaskForm): + """Formulaire permettant l'ajout d'un logo (dans un département)""" + + from app.scodoc.sco_config_actions import LogoInsert + + dept_key = HiddenField() + name = StringField( + label="Nom", + validators=[ + logo_name_validator("Nom de logo invalide (alphanumérique, _)"), + validators.Length( + max=20, message="Un nom ne doit pas dépasser 20 caractères" + ), + validators.DataRequired("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.DataRequired("Fichier image manquant"), + ], + ) + do_insert = SubmitField("ajouter une image") + + def __init__(self, *args, **kwargs): + kwargs["meta"] = {"csrf": False} + super().__init__(*args, **kwargs) + + def id(self): + return f"id=add_{self.dept_key.data}" + + 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, strict=True) 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 + + def opened(self): + if self.do_insert.data: + if self.name.errors: + return "open" + if self.upload.errors: + return "open" + return "" + + +class LogoForm(FlaskForm): + """Embed both presentation of a logo (cf. template file configuration.j2) + 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") + do_rename = SubmitField("Renommer") + new_name = StringField( + label="Nom", + validators=[ + logo_name_validator("Nom de logo invalide (alphanumérique, _)"), + validators.Length( + max=20, message="Un nom ne doit pas dépasser 20 caractères" + ), + validators.DataRequired("Nom de logo requis (alphanumériques ou '-')"), + ], + ) + + def __init__(self, *args, **kwargs): + kwargs["meta"] = {"csrf": False} + super().__init__(*args, **kwargs) + logo = find_logo( + logoname=self.logo_id.data, dept_id=dept_key_to_id(self.dept_key.data) + ) + if logo is None: + raise ScoValueError("logo introuvable") + self.logo = logo.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 id(self): + idstring = f"{self.dept_key.data}_{self.logo_id.data}" + return f"id={idstring}" + + def select_action(self): + from app.scodoc.sco_config_actions import LogoRename + from app.scodoc.sco_config_actions import LogoUpdate + from app.scodoc.sco_config_actions import LogoDelete + + 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) + if self.do_rename.data and self.validate(): + return LogoRename.build_action(self.data) + return None + + def opened(self): + if self.upload.data and self.upload.errors: + return "open" + if self.new_name.data and self.new_name.errors: + return "open" + return "" + + +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 id(self): + return f"id=DEPT_{self.dept_key.data}" + + 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 opened(self): + if self.add_logo.opened(): + return "open" + for logo_form in self.logos: + if logo_form.opened(): + return "open" + return "" + + def count(self): + compte = len(self.logos.entries) + if compte == 0: + return "vide" + elif compte == 1: + return "1 élément" + else: + return f"{compte} éléments" + + +def _make_dept_id_name(): + """Cette section assure 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(modele): + data = { + "depts": _make_depts_data(modele=modele), + } + return data + + +class LogosConfigurationForm(FlaskForm): + "Panneau de configuration des logos" + 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): + for dept_entry in self.depts: + dept_form = dept_entry.form + action = dept_form.select_action() + if action: + return action + return None + + +def config_logos(): + "Page de configuration des logos" + # nb: le contrôle d'accès (SuperAdmin) doit être fait dans la vue + form = LogosConfigurationForm( + data=_make_data( + 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.configure_logos")) + else: + if not form.validate(): + scu.flash_errors(form) + + return render_template( + "config_logos.j2", + scodoc_dept=None, + title="Configuration ScoDoc", + form=form, + ) diff --git a/app/scodoc/sco_preferences.py b/app/scodoc/sco_preferences.py index 5c86d9ff..eb629edc 100644 --- a/app/scodoc/sco_preferences.py +++ b/app/scodoc/sco_preferences.py @@ -2069,7 +2069,10 @@ class BasePreferences(object): self.load() H = [ - html_sco_header.sco_header(page_title="Préférences"), + html_sco_header.sco_header( + page_title="Préférences", + javascripts=["js/detail_summary_persistence.js"], + ), f"

Préférences globales pour {scu.ScoURL()}

", # f"""

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

""" @@ -2210,10 +2213,14 @@ class SemPreferences: ) # a bug ! sem = sco_formsemestre.get_formsemestre(self.formsemestre_id) H = [ - html_sco_header.html_sem_header("Préférences du semestre"), + html_sco_header.html_sem_header( + "Préférences du semestre", + javascripts=["js/detail_summary_persistence.js"], + ), """

Les paramètres définis ici ne s'appliqueront qu'à ce semestre.

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

+ pour reinitialisés l'état de toutes +les balises (fermées par défaut sauf si attribut open déjà activé dans le code source de la page) + +*/ + +const ID_ATTRIBUTE = "ds_id" + +function genere_id(detail, idnum) { + let id = "ds_" + idnum; + if (detail.getAttribute("id")) { + id = "#" + detail.getAttribute("id"); + } + detail.setAttribute(ID_ATTRIBUTE, id); + return id; +} + +// remise à l'état initial. doit être exécuté dès le chargement de la page pour que l'état 'open' +// des balises soit celui indiqué par le serveur (et donc indépendant du localstorage) +function reset_detail(detail, id) { + let opened = detail.getAttribute("open"); + if (opened) { + detail.setAttribute("open", true); + localStorage.setItem(id, true); + } else { + detail.removeAttribute("open"); + localStorage.setItem(id, false); + } +} + +function restore_detail(detail, id) { + let status = localStorage.getItem(id); + if (status == "true") { + detail.setAttribute("open", true); + } else { + detail.removeAttribute("open"); + } +} + +function add_listener(detail) { + detail.addEventListener('toggle', (e) => { + let id = e.target.getAttribute(ID_ATTRIBUTE); + let ante = e.target.getAttribute("open"); + if (ante == null) { + localStorage.setItem(id, false); + } else { + localStorage.setItem(id, true); + } + e.stopPropagation(); + }) +} + +function reset_ds() { + let idnum = 0; + keepDetails = true; + details = document.querySelectorAll("details") + details.forEach(function (detail) { + let id = genere_id(detail, idnum); + console.log("Processing " + id) + if (keepDetails) { + restore_detail(detail, id); + } else { + reset_detail(detail, id); + } + add_listener(detail); + idnum++; + }); +} + +window.addEventListener('load', function() { + console.log("details/summary persistence ON"); + reset_ds(); +}) diff --git a/app/templates/config_logos.j2 b/app/templates/config_logos.j2 index 03808c1b..918c1f9a 100644 --- a/app/templates/config_logos.j2 +++ b/app/templates/config_logos.j2 @@ -20,7 +20,7 @@ {% endmacro %} {% macro render_add_logo(add_logo_form) %} -
+

Ajouter un logo

@@ -33,7 +33,7 @@ {% endmacro %} {% macro render_logo(dept_form, logo_form) %} -
+
{{ logo_form.hidden_tag() }} {% if logo_form.titre %} @@ -95,6 +95,7 @@ +
{{ form.hidden_tag() }} @@ -104,7 +105,7 @@ {% for dept_entry in form.depts.entries %} {% set dept_form = dept_entry.form %} {{ dept_entry.form.hidden_tag() }} -
+
{% if dept_entry.form.is_local() %} -- 2.40.1 From e64dc97ecf307b33a83aadc70be66a19cbc1dbdc Mon Sep 17 00:00:00 2001 From: jmpla Date: Sat, 18 Feb 2023 11:51:57 +0100 Subject: [PATCH 2/3] fix bug "cannot revert preference to global" --- app/scodoc/sco_preferences.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/scodoc/sco_preferences.py b/app/scodoc/sco_preferences.py index eb629edc..85ab9ea4 100644 --- a/app/scodoc/sco_preferences.py +++ b/app/scodoc/sco_preferences.py @@ -2128,7 +2128,7 @@ class BasePreferences(object): if formsemestre_id: descr[ "explanation" - ] = """ou utiliser paramètre global""" if formsemestre_id and self.is_global(formsemestre_id, pref_name): -- 2.40.1 From debe07d2ca820b8e5652e39691ce8dcc540124ba Mon Sep 17 00:00:00 2001 From: jmpla Date: Sat, 18 Feb 2023 13:24:40 +0100 Subject: [PATCH 3/3] prefs detail persistence --- app/forms/config_logos.py | 451 --------------------------------- app/forms/main/config_logos.py | 10 + 2 files changed, 10 insertions(+), 451 deletions(-) delete mode 100644 app/forms/config_logos.py diff --git a/app/forms/config_logos.py b/app/forms/config_logos.py deleted file mode 100644 index d01cc5cc..00000000 --- a/app/forms/config_logos.py +++ /dev/null @@ -1,451 +0,0 @@ -# -*- mode: python -*- -# -*- coding: utf-8 -*- - -############################################################################## -# -# ScoDoc -# -# Copyright (c) 1999 - 2023 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 -# -############################################################################## - -""" -Formulaires configuration logos - -Contrib @jmp, dec 21 -""" - -from flask import flash, url_for, redirect, render_template -from flask_wtf import FlaskForm -from flask_wtf.file import FileField, FileAllowed -from wtforms import SubmitField, FormField, validators, FieldList -from wtforms import ValidationError -from wtforms.fields.simple import StringField, HiddenField - -from app.models import Departement -from app.scodoc import sco_logos, html_sco_header -from app.scodoc import sco_utils as scu - -from app.scodoc.sco_config_actions import LogoInsert -from app.scodoc.sco_exceptions import ScoValueError -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 - - -def logo_name_validator(message=None): - def validate_logo_name(form, field): - name = field.data if field.data else "" - if "." in name: - raise ValidationError(message) - if not scu.is_valid_filename(name): - raise ValidationError(message) - - return validate_logo_name - - -class AddLogoForm(FlaskForm): - """Formulaire permettant l'ajout d'un logo (dans un département)""" - - from app.scodoc.sco_config_actions import LogoInsert - - dept_key = HiddenField() - name = StringField( - label="Nom", - validators=[ - logo_name_validator("Nom de logo invalide (alphanumérique, _)"), - validators.Length( - max=20, message="Un nom ne doit pas dépasser 20 caractères" - ), - validators.DataRequired("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.DataRequired("Fichier image manquant"), - ], - ) - do_insert = SubmitField("ajouter une image") - - def __init__(self, *args, **kwargs): - kwargs["meta"] = {"csrf": False} - super().__init__(*args, **kwargs) - - def id(self): - return f"id=add_{self.dept_key.data}" - - 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, strict=True) 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 - - def opened(self): - if self.do_insert.data: - if self.name.errors: - return "open" - if self.upload.errors: - return "open" - return "" - - -class LogoForm(FlaskForm): - """Embed both presentation of a logo (cf. template file configuration.j2) - 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") - do_rename = SubmitField("Renommer") - new_name = StringField( - label="Nom", - validators=[ - logo_name_validator("Nom de logo invalide (alphanumérique, _)"), - validators.Length( - max=20, message="Un nom ne doit pas dépasser 20 caractères" - ), - validators.DataRequired("Nom de logo requis (alphanumériques ou '-')"), - ], - ) - - def __init__(self, *args, **kwargs): - kwargs["meta"] = {"csrf": False} - super().__init__(*args, **kwargs) - logo = find_logo( - logoname=self.logo_id.data, dept_id=dept_key_to_id(self.dept_key.data) - ) - if logo is None: - raise ScoValueError("logo introuvable") - self.logo = logo.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 id(self): - idstring = f"{self.dept_key.data}_{self.logo_id.data}" - return f"id={idstring}" - - def select_action(self): - from app.scodoc.sco_config_actions import LogoRename - from app.scodoc.sco_config_actions import LogoUpdate - from app.scodoc.sco_config_actions import LogoDelete - - 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) - if self.do_rename.data and self.validate(): - return LogoRename.build_action(self.data) - return None - - def opened(self): - if self.upload.data and self.upload.errors: - return "open" - if self.new_name.data and self.new_name.errors: - return "open" - return "" - - -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 id(self): - return f"id=DEPT_{self.dept_key.data}" - - 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 opened(self): - if self.add_logo.opened(): - return "open" - for logo_form in self.logos: - if logo_form.opened(): - return "open" - return "" - - def count(self): - compte = len(self.logos.entries) - if compte == 0: - return "vide" - elif compte == 1: - return "1 élément" - else: - return f"{compte} éléments" - - -def _make_dept_id_name(): - """Cette section assure 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(modele): - data = { - "depts": _make_depts_data(modele=modele), - } - return data - - -class LogosConfigurationForm(FlaskForm): - "Panneau de configuration des logos" - 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): - for dept_entry in self.depts: - dept_form = dept_entry.form - action = dept_form.select_action() - if action: - return action - return None - - -def config_logos(): - "Page de configuration des logos" - # nb: le contrôle d'accès (SuperAdmin) doit être fait dans la vue - form = LogosConfigurationForm( - data=_make_data( - 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.configure_logos")) - else: - if not form.validate(): - scu.flash_errors(form) - - return render_template( - "config_logos.j2", - scodoc_dept=None, - title="Configuration ScoDoc", - form=form, - ) diff --git a/app/forms/main/config_logos.py b/app/forms/main/config_logos.py index 2a54dd7c..d01cc5cc 100644 --- a/app/forms/main/config_logos.py +++ b/app/forms/main/config_logos.py @@ -148,6 +148,9 @@ class AddLogoForm(FlaskForm): kwargs["meta"] = {"csrf": False} super().__init__(*args, **kwargs) + def id(self): + return f"id=add_{self.dept_key.data}" + def validate_name(self, name): dept_id = dept_key_to_id(self.dept_key.data) if dept_id == GLOBAL: @@ -227,6 +230,10 @@ class LogoForm(FlaskForm): self.description = "Se substitue au footer défini au niveau global" self.titre = "Logo pied de page" + def id(self): + idstring = f"{self.dept_key.data}_{self.logo_id.data}" + return f"id={idstring}" + def select_action(self): from app.scodoc.sco_config_actions import LogoRename from app.scodoc.sco_config_actions import LogoUpdate @@ -258,6 +265,9 @@ class DeptForm(FlaskForm): kwargs["meta"] = {"csrf": False} super().__init__(*args, **kwargs) + def id(self): + return f"id=DEPT_{self.dept_key.data}" + def is_local(self): if self.dept_key.data == GLOBAL: return None -- 2.40.1