ScoDoc/app/forms/main/config_logos.py

442 lines
15 KiB
Python
Raw Normal View History

2021-11-19 11:51:05 +01:00
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# ScoDoc
#
2023-01-02 13:16:27 +01:00
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
2021-11-19 11:51:05 +01:00
#
# 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
#
##############################################################################
"""
2021-12-12 22:11:11 +01:00
Formulaires configuration logos
2021-11-19 11:51:05 +01:00
2021-12-12 22:11:11 +01:00
Contrib @jmp, dec 21
2021-11-19 11:51:05 +01:00
"""
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
2021-12-12 22:11:11 +01:00
from wtforms.fields.simple import StringField, HiddenField
2021-11-19 11:51:05 +01:00
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
2021-11-19 11:51:05 +01:00
from app.scodoc.sco_logos import find_logo
2021-11-19 11:51:05 +01:00
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
2021-11-19 11:51:05 +01:00
# """
# 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 ""
2022-07-02 09:18:31 +02:00
if "." in name:
raise ValidationError(message)
if not scu.is_valid_filename(name):
raise ValidationError(message)
return validate_logo_name
2021-11-19 11:51:05 +01:00
class AddLogoForm(FlaskForm):
"""Formulaire permettant l'ajout d'un logo (dans un département)"""
from app.scodoc.sco_config_actions import LogoInsert
2021-11-19 11:51:05 +01:00
dept_key = HiddenField()
name = StringField(
label="Nom",
validators=[
logo_name_validator("Nom de logo invalide (alphanumérique, _)"),
2021-11-19 11:51:05 +01:00
validators.Length(
max=20, message="Un nom ne doit pas dépasser 20 caractères"
),
2021-12-11 17:05:38 +01:00
validators.DataRequired("Nom de logo requis (alphanumériques ou '-')"),
2021-11-19 11:51:05 +01:00
],
)
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)}",
),
2021-12-11 17:05:38 +01:00
validators.DataRequired("Fichier image manquant"),
2021-11-19 11:51:05 +01:00
],
)
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, strict=True) is not None:
2021-11-19 11:51:05 +01:00
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 ""
2021-11-19 11:51:05 +01:00
class LogoForm(FlaskForm):
"""Embed both presentation of a logo (cf. template file configuration.j2)
2021-11-19 11:51:05 +01:00
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 '-')"),
],
)
2021-11-19 11:51:05 +01:00
def __init__(self, *args, **kwargs):
kwargs["meta"] = {"csrf": False}
super().__init__(*args, **kwargs)
logo = find_logo(
2021-11-19 11:51:05 +01:00
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()
2021-11-19 11:51:05 +01:00
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):
from app.scodoc.sco_config_actions import LogoRename
from app.scodoc.sco_config_actions import LogoUpdate
from app.scodoc.sco_config_actions import LogoDelete
2021-11-19 11:51:05 +01:00
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)
2021-11-19 11:51:05 +01:00
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 ""
2021-11-19 11:51:05 +01:00
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 opened(self):
if self.add_logo.opened():
return "open"
for logo_form in self.logos:
if logo_form.opened():
return "open"
return ""
2022-04-15 08:54:10 +02:00
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"
2021-11-19 11:51:05 +01:00
def _make_dept_id_name():
2022-01-04 20:03:38 +01:00
"""Cette section assure que tous les départements sont traités (y compris ceux qu'ont pas de logo au départ)
2021-11-19 11:51:05 +01:00
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):
2021-11-19 11:51:05 +01:00
data = {
"depts": _make_depts_data(modele=modele),
}
return data
class LogosConfigurationForm(FlaskForm):
"Panneau de configuration des logos"
2021-11-19 11:51:05 +01:00
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(
2021-11-19 11:51:05 +01:00
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)
2021-11-19 11:51:05 +01:00
return render_template(
"config_logos.j2",
2021-11-19 11:51:05 +01:00
scodoc_dept=None,
title="Configuration ScoDoc",
form=form,
)