Merge pull request 'livrable_logos' (#200) from jmplace/ScoDoc-Lille:livrable_logos into PNBUT
Reviewed-on: https://scodoc.org/git/ScoDoc/ScoDoc/pulls/200
@ -44,6 +44,7 @@ import unicodedata
|
|||||||
|
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
from app import log
|
from app import log
|
||||||
|
from app.scodoc.sco_logos import find_logo
|
||||||
|
|
||||||
PE_DEBUG = 0
|
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)
|
add_local_file_to_zip(zipfile, ziproot, pathname, "avis/" + filename)
|
||||||
|
|
||||||
# Logos: (add to logos/ directory in zip)
|
# Logos: (add to logos/ directory in zip)
|
||||||
logos_names = ["logo_header.jpg", "logo_footer.jpg"]
|
logos_names = ["header", "footer"]
|
||||||
for f in logos_names:
|
for name in logos_names:
|
||||||
logo = os.path.join(scu.SCODOC_LOGOS_DIR, f)
|
logo = find_logo(logoname=name, dept_id=g.scodoc_dept_id)
|
||||||
if os.path.isfile(logo):
|
if logo is not None:
|
||||||
add_local_file_to_zip(zipfile, ziproot, logo, "avis/logos/" + f)
|
add_local_file_to_zip(zipfile, ziproot, logo, "avis/logos/" + logo.filename)
|
||||||
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------------------
|
# ----------------------------------------------------------------------------------------
|
||||||
|
@ -51,23 +51,24 @@ Chaque semestre peut si nécessaire utiliser un type de bulletin différent.
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
import io
|
import io
|
||||||
import os
|
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
|
from pydoc import html
|
||||||
|
|
||||||
from reportlab.platypus.doctemplate import PageTemplate, BaseDocTemplate
|
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
|
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_cache
|
||||||
from app.scodoc import sco_formsemestre
|
from app.scodoc import sco_formsemestre
|
||||||
from app.scodoc import sco_pdf
|
from app.scodoc import sco_pdf
|
||||||
from app.scodoc import sco_preferences
|
from app.scodoc import sco_preferences
|
||||||
from app.scodoc import sco_etud
|
from app.scodoc import sco_etud
|
||||||
import sco_version
|
import sco_version
|
||||||
|
from app.scodoc.sco_logos import find_logo
|
||||||
|
|
||||||
|
|
||||||
def pdfassemblebulletins(
|
def pdfassemblebulletins(
|
||||||
@ -110,6 +111,17 @@ def pdfassemblebulletins(
|
|||||||
return data
|
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'<img %s src="%s"%s/>' % (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"):
|
def process_field(field, cdict, style, suppress_empty_pars=False, format="pdf"):
|
||||||
"""Process a field given in preferences, returns
|
"""Process a field given in preferences, returns
|
||||||
- if format = 'pdf': a list of Platypus objects
|
- 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
|
return text
|
||||||
# --- PDF format:
|
# --- PDF format:
|
||||||
# handle logos:
|
# 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(
|
text = re.sub(
|
||||||
r"<(\s*)logo(.*?)src\s*=\s*(.*?)>", r"<\1logo\2\3>", text
|
r"<(\s*)logo(.*?)src\s*=\s*(.*?)>", r"<\1logo\2\3>", text
|
||||||
) # remove forbidden src attribute
|
) # remove forbidden src attribute
|
||||||
if image_dir is not None:
|
text = re.sub(
|
||||||
text = re.sub(
|
r'(<\s*logo(.*?)name\s*=\s*"(\w*?)"(.*?)/?>)',
|
||||||
r'<\s*logo(.*?)name\s*=\s*"(\w*?)"(.*?)/?>',
|
replacement_function,
|
||||||
r'<img\1src="%s/logo_\2.jpg"\3/>' % image_dir,
|
text,
|
||||||
text,
|
)
|
||||||
)
|
# nota: le match sur \w*? donne le nom du logo et interdit les .. et autres
|
||||||
# nota: le match sur \w*? donne le nom du logo et interdit les .. et autres
|
# tentatives d'acceder à d'autres fichiers !
|
||||||
# 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))
|
# log('field: %s' % (text))
|
||||||
return sco_pdf.makeParas(text, style, suppress_empty=suppress_empty_pars)
|
return sco_pdf.makeParas(text, style, suppress_empty=suppress_empty_pars)
|
||||||
|
181
app/scodoc/sco_config_actions.py
Normal file
@ -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()
|
402
app/scodoc/sco_config_form.py
Normal file
@ -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.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 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,
|
||||||
|
)
|
@ -32,32 +32,247 @@ avec `ext` membre de LOGOS_IMAGES_ALLOWED_TYPES (= jpg, png)
|
|||||||
|
|
||||||
SCODOC_LOGOS_DIR /opt/scodoc-data/config/logos
|
SCODOC_LOGOS_DIR /opt/scodoc-data/config/logos
|
||||||
"""
|
"""
|
||||||
|
import glob
|
||||||
import imghdr
|
import imghdr
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
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 app.scodoc import sco_utils as scu
|
||||||
|
from PIL import Image as PILImage
|
||||||
|
|
||||||
|
GLOBAL = "_" # category for server level logos
|
||||||
|
|
||||||
|
|
||||||
def get_logo_filename(logo_type: str, scodoc_dept: str) -> str:
|
def find_logo(logoname, dept_id=None, strict=False, prefix=scu.LOGO_FILE_PREFIX):
|
||||||
"""return full filename for this logo, or "" if not found
|
|
||||||
an existing file with extension.
|
|
||||||
logo_type: "header" or "footer"
|
|
||||||
scodoc-dept: acronym
|
|
||||||
"""
|
"""
|
||||||
# Search logos in dept specific dir (/opt/scodoc-data/config/logos/logos_<dept>),
|
"Recherche un logo 'name' existant.
|
||||||
# then in config dir /opt/scodoc-data/config/logos/
|
Deux strategies:
|
||||||
for image_dir in (
|
si strict:
|
||||||
scu.SCODOC_LOGOS_DIR + "/logos_" + scodoc_dept,
|
reherche uniquement dans le département puis si non trouvé au niveau global
|
||||||
scu.SCODOC_LOGOS_DIR, # global logos
|
sinon
|
||||||
):
|
On recherche en local au dept d'abord puis si pas trouvé recherche globale
|
||||||
for suffix in scu.LOGOS_IMAGES_ALLOWED_TYPES:
|
quelque soit la stratégie, retourne None si pas trouvé
|
||||||
filename = os.path.join(image_dir, f"logo_{logo_type}.{suffix}")
|
:param logoname: le nom recherche
|
||||||
if os.path.isfile(filename) and os.access(filename, os.R_OK):
|
:param dept_id: l'id du département dans lequel se fait la recherche (None si global)
|
||||||
return filename
|
: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 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"""
|
||||||
|
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:
|
||||||
|
[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 = {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.id] = _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: <rep>/<prefix><name>.<suffixe>
|
||||||
|
<rep> : répertoire de recherche (déduit du dept_id)
|
||||||
|
<prefix>: le prefix (LOGO_FILE_PREFIX pour les logos)
|
||||||
|
<suffix>: 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(<name>, <dept_id>, ...).select() (retourne None si fichier non trouvé)
|
||||||
|
logo en création: Logo(<name>, <dept_id>, ...).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 ""
|
||||||
|
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)]
|
||||||
|
)
|
||||||
|
# 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.mm = "Not inited: call the select or create function before access"
|
||||||
|
|
||||||
|
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 _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 = mm
|
||||||
|
if self.density is not None:
|
||||||
|
x_density, y_density = self.density
|
||||||
|
if unit != 0:
|
||||||
|
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.mm = None
|
||||||
|
else:
|
||||||
|
self.mm = None
|
||||||
|
|
||||||
|
self.size = (x_size, y_size)
|
||||||
|
self.aspect_ratio = round(float(x_size) / y_size, 2)
|
||||||
|
|
||||||
|
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._read_info(img)
|
||||||
|
return self
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_url(self):
|
||||||
|
"""Retourne l'URL permettant d'obtenir l'image du logo"""
|
||||||
|
return url_for(
|
||||||
|
"scodoc.get_logo",
|
||||||
|
dept_id=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",
|
||||||
|
dept_id=self.scodoc_dept_id,
|
||||||
|
name=self.logoname,
|
||||||
|
global_if_not_found=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_usage(self):
|
||||||
|
if self.mm is None:
|
||||||
|
return f'<logo name="{self.logoname}" width="?? mm" height="?? mm">'
|
||||||
|
else:
|
||||||
|
return f'<logo name="{self.logoname}" width="{self.mm[0]}mm"">'
|
||||||
|
|
||||||
|
def last_modified(self):
|
||||||
|
path = Path(self.filepath)
|
||||||
|
dt = path.stat().st_mtime
|
||||||
|
return path.stat().st_mtime
|
||||||
|
|
||||||
|
|
||||||
def guess_image_type(stream) -> str:
|
def guess_image_type(stream) -> str:
|
||||||
@ -70,26 +285,33 @@ def guess_image_type(stream) -> str:
|
|||||||
return fmt if fmt != "jpeg" else "jpg"
|
return fmt if fmt != "jpeg" else "jpg"
|
||||||
|
|
||||||
|
|
||||||
def _ensure_directory_exists(filename):
|
def make_logo_local(logoname, dept_name):
|
||||||
"create enclosing directory if necessary"
|
depts = Departement.query.filter_by(acronym=dept_name).all()
|
||||||
directory = os.path.split(filename)[0]
|
if len(depts) == 0:
|
||||||
if not os.path.exists(directory):
|
print(f"no dept {dept_name} found. aborting")
|
||||||
current_app.logger.info(f"sco_logos creating directory %s", directory)
|
return
|
||||||
os.mkdir(directory)
|
if len(depts) > 1:
|
||||||
|
print(f"several depts {dept_name} found. aborting")
|
||||||
|
return
|
||||||
def store_image(stream, basename):
|
dept = depts[0]
|
||||||
img_type = guess_image_type(stream)
|
print(f"Move logo {logoname}' from global to {dept.acronym}")
|
||||||
if img_type not in scu.LOGOS_IMAGES_ALLOWED_TYPES:
|
old_path_wild = f"/opt/scodoc-data/config/logos/logo_{logoname}.*"
|
||||||
abort(400, "type d'image invalide")
|
new_dir = f"/opt/scodoc-data/config/logos/logos_{dept.id}"
|
||||||
filename = basename + "." + img_type
|
logos = glob.glob(old_path_wild)
|
||||||
_ensure_directory_exists(filename)
|
# checks that there is non local already present
|
||||||
with open(filename, "wb") as f:
|
for logo in logos:
|
||||||
f.write(stream.read())
|
filename = os.path.split(logo)[1]
|
||||||
current_app.logger.info(f"sco_logos.store_image %s", filename)
|
new_name = os.path.sep.join([new_dir, filename])
|
||||||
# erase other formats if they exists
|
if os.path.exists(new_name):
|
||||||
for extension in set(scu.LOGOS_IMAGES_ALLOWED_TYPES) - set([img_type]):
|
print("local version of global logo already exists. aborting")
|
||||||
try:
|
return
|
||||||
os.unlink(basename + "." + extension)
|
# create new__dir if necessary
|
||||||
except IOError:
|
if not os.path.exists(new_dir):
|
||||||
pass
|
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")
|
||||||
|
@ -60,11 +60,7 @@ from reportlab.lib.pagesizes import letter, A4, landscape
|
|||||||
from flask import g
|
from flask import g
|
||||||
|
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
from app.scodoc.sco_utils import (
|
from app.scodoc.sco_utils import CONFIG
|
||||||
CONFIG,
|
|
||||||
SCODOC_LOGOS_DIR,
|
|
||||||
LOGOS_IMAGES_ALLOWED_TYPES,
|
|
||||||
)
|
|
||||||
from app import log
|
from app import log
|
||||||
from app.scodoc.sco_exceptions import ScoGenError, ScoValueError
|
from app.scodoc.sco_exceptions import ScoGenError, ScoValueError
|
||||||
import sco_version
|
import sco_version
|
||||||
@ -196,6 +192,10 @@ class ScolarsPageTemplate(PageTemplate):
|
|||||||
preferences=None, # dictionnary with preferences, required
|
preferences=None, # dictionnary with preferences, required
|
||||||
):
|
):
|
||||||
"""Initialise our page template."""
|
"""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.preferences = preferences
|
||||||
self.pagesbookmarks = pagesbookmarks
|
self.pagesbookmarks = pagesbookmarks
|
||||||
self.pdfmeta_author = author
|
self.pdfmeta_author = author
|
||||||
@ -219,20 +219,16 @@ class ScolarsPageTemplate(PageTemplate):
|
|||||||
)
|
)
|
||||||
PageTemplate.__init__(self, "ScolarsPageTemplate", [content])
|
PageTemplate.__init__(self, "ScolarsPageTemplate", [content])
|
||||||
self.logo = None
|
self.logo = None
|
||||||
# XXX COPIED from sco_pvpdf, to be refactored (no time now)
|
logo = find_logo(
|
||||||
# Search background in dept specific dir, then in global config dir
|
logoname="bul_pdf_background", dept_id=g.scodoc_dept_id, prefix=None
|
||||||
for image_dir in (
|
)
|
||||||
SCODOC_LOGOS_DIR + "/logos_" + g.scodoc_dept + "/",
|
if logo is None:
|
||||||
SCODOC_LOGOS_DIR + "/", # global logos
|
# Also try to use PV background
|
||||||
):
|
logo = find_logo(
|
||||||
for suffix in LOGOS_IMAGES_ALLOWED_TYPES:
|
logoname="letter_background", dept_id=g.scodoc_dept_id, prefix=None
|
||||||
fn = image_dir + "/bul_pdf_background" + "." + suffix
|
)
|
||||||
if not self.background_image_filename and os.path.exists(fn):
|
if logo is not None:
|
||||||
self.background_image_filename = fn
|
self.background_image_filename = logo.filepath
|
||||||
# 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
|
|
||||||
|
|
||||||
def beforeDrawPage(self, canvas, doc):
|
def beforeDrawPage(self, canvas, doc):
|
||||||
"""Draws (optional) background, logo and contribution message on each page.
|
"""Draws (optional) background, logo and contribution message on each page.
|
||||||
|
@ -2017,10 +2017,10 @@ class BasePreferences(object):
|
|||||||
H = [
|
H = [
|
||||||
html_sco_header.sco_header(page_title="Préférences"),
|
html_sco_header.sco_header(page_title="Préférences"),
|
||||||
"<h2>Préférences globales pour %s</h2>" % scu.ScoURL(),
|
"<h2>Préférences globales pour %s</h2>" % scu.ScoURL(),
|
||||||
f"""<p><a href="{url_for("scolar.config_logos", scodoc_dept=g.scodoc_dept)
|
# f"""<p><a href="{url_for("scolar.config_logos", scodoc_dept=g.scodoc_dept)
|
||||||
}">modification des logos du département (pour documents pdf)</a></p>"""
|
# }">modification des logos du département (pour documents pdf)</a></p>"""
|
||||||
if current_user.is_administrator()
|
# if current_user.is_administrator()
|
||||||
else "",
|
# else "",
|
||||||
"""<p class="help">Ces paramètres s'appliquent par défaut à tous les semestres, sauf si ceux-ci définissent des valeurs spécifiques.</p>
|
"""<p class="help">Ces paramètres s'appliquent par défaut à tous les semestres, sauf si ceux-ci définissent des valeurs spécifiques.</p>
|
||||||
<p class="msg">Attention: cliquez sur "Enregistrer les modifications" en bas de page pour appliquer vos changements !</p>
|
<p class="msg">Attention: cliquez sur "Enregistrer les modifications" en bas de page pour appliquer vos changements !</p>
|
||||||
""",
|
""",
|
||||||
|
@ -52,6 +52,7 @@ from app.scodoc import sco_pdf
|
|||||||
from app.scodoc import sco_preferences
|
from app.scodoc import sco_preferences
|
||||||
from app.scodoc import sco_etud
|
from app.scodoc import sco_etud
|
||||||
import sco_version
|
import sco_version
|
||||||
|
from app.scodoc.sco_logos import find_logo
|
||||||
from app.scodoc.sco_pdf import PDFLOCK
|
from app.scodoc.sco_pdf import PDFLOCK
|
||||||
from app.scodoc.sco_pdf import SU
|
from app.scodoc.sco_pdf import SU
|
||||||
|
|
||||||
@ -201,33 +202,36 @@ class CourrierIndividuelTemplate(PageTemplate):
|
|||||||
self.logo_footer = None
|
self.logo_footer = None
|
||||||
self.logo_header = None
|
self.logo_header = None
|
||||||
# Search logos in dept specific dir, then in global scu.CONFIG dir
|
# Search logos in dept specific dir, then in global scu.CONFIG dir
|
||||||
for image_dir in (
|
if template_name == "PVJuryTemplate":
|
||||||
scu.SCODOC_LOGOS_DIR + "/logos_" + g.scodoc_dept,
|
background = find_logo(
|
||||||
scu.SCODOC_LOGOS_DIR, # global logos
|
logoname="pvjury_background",
|
||||||
):
|
dept_id=g.scodoc_dept_id,
|
||||||
for suffix in scu.LOGOS_IMAGES_ALLOWED_TYPES:
|
prefix="",
|
||||||
if template_name == "PVJuryTemplate":
|
)
|
||||||
fn = image_dir + "/pvjury_background" + "." + suffix
|
else:
|
||||||
else:
|
background = find_logo(
|
||||||
fn = image_dir + "/letter_background" + "." + suffix
|
logoname="letter_background",
|
||||||
if not self.background_image_filename and os.path.exists(fn):
|
dept_id=g.scodoc_dept_id,
|
||||||
self.background_image_filename = fn
|
prefix="",
|
||||||
|
)
|
||||||
|
if not self.background_image_filename and background is not None:
|
||||||
|
self.background_image_filename = background.filepath
|
||||||
|
|
||||||
fn = image_dir + "/logo_footer" + "." + suffix
|
footer = find_logo(logoname="footer", dept_id=g.scodoc_dept_id)
|
||||||
if not self.logo_footer and os.path.exists(fn):
|
if footer is not None:
|
||||||
self.logo_footer = Image(
|
self.logo_footer = Image(
|
||||||
fn,
|
footer.filepath,
|
||||||
height=LOGO_FOOTER_HEIGHT,
|
height=LOGO_FOOTER_HEIGHT,
|
||||||
width=LOGO_FOOTER_WIDTH,
|
width=LOGO_FOOTER_WIDTH,
|
||||||
)
|
)
|
||||||
|
|
||||||
fn = image_dir + "/logo_header" + "." + suffix
|
header = find_logo(logoname="header", dept_id=g.scodoc_dept_id)
|
||||||
if not self.logo_header and os.path.exists(fn):
|
if header is not None:
|
||||||
self.logo_header = Image(
|
self.logo_header = Image(
|
||||||
fn,
|
header.filepath,
|
||||||
height=LOGO_HEADER_HEIGHT,
|
height=LOGO_HEADER_HEIGHT,
|
||||||
width=LOGO_HEADER_WIDTH,
|
width=LOGO_HEADER_WIDTH,
|
||||||
)
|
)
|
||||||
|
|
||||||
def beforeDrawPage(self, canvas, doc):
|
def beforeDrawPage(self, canvas, doc):
|
||||||
"""Draws a logo and an contribution message on each page."""
|
"""Draws a logo and an contribution message on each page."""
|
||||||
|
@ -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
|
# ----- Les logos: /opt/scodoc-data/config/logos
|
||||||
SCODOC_LOGOS_DIR = os.path.join(SCODOC_CFG_DIR, "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_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<name>.<suffix> (fichier global) ou
|
||||||
|
# SCODOC_LOGO_DIR/LOGOS_DIR_PREFIX<dept_id>/LOGO_FILE_PREFIX<name>.<suffix> (fichier départemental)
|
||||||
|
|
||||||
# ----- Les outils distribués
|
# ----- Les outils distribués
|
||||||
SCO_TOOLS_DIR = os.path.join(Config.SCODOC_DIR, "tools")
|
SCO_TOOLS_DIR = os.path.join(Config.SCODOC_DIR, "tools")
|
||||||
|
@ -867,6 +867,9 @@ div.sco_help {
|
|||||||
|
|
||||||
span.wtf-field ul.errors li {
|
span.wtf-field ul.errors li {
|
||||||
color: red;
|
color: red;
|
||||||
|
}
|
||||||
|
.configuration_logo div.img {
|
||||||
|
|
||||||
}
|
}
|
||||||
.configuration_logo div.img-container {
|
.configuration_logo div.img-container {
|
||||||
width: 256px;
|
width: 256px;
|
||||||
@ -874,6 +877,20 @@ span.wtf-field ul.errors li {
|
|||||||
.configuration_logo div.img-container img {
|
.configuration_logo div.img-container img {
|
||||||
max-width: 100%;
|
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 {
|
p.indent {
|
||||||
padding-left: 2em;
|
padding-left: 2em;
|
||||||
|
6
app/static/js/configuration.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
function submit_form() {
|
||||||
|
$("#configuration_form").submit();
|
||||||
|
}
|
||||||
|
|
||||||
|
$(function () {
|
||||||
|
})
|
81
app/templates/config_dept.html
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
{% macro render_field(field) %}
|
||||||
|
<div>
|
||||||
|
<span class="wtf-field">{{ field.label }} :</span>
|
||||||
|
<span class="wtf-field">{{ field()|safe }}
|
||||||
|
{% if field.errors %}
|
||||||
|
<ul class=errors>
|
||||||
|
{% for error in field.errors %}
|
||||||
|
<li>{{ error }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
{% macro render_logo(logo_form, titre=None) %}
|
||||||
|
{% if titre %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="2">
|
||||||
|
<h3>{{ titre }}</h3>
|
||||||
|
<hr/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
<tr>
|
||||||
|
<td style="padding-right: 20px; vertical-align: top;">
|
||||||
|
<p class="help">{{ logo_form.form.description }} Image actuelle:</p>
|
||||||
|
<div class="img-container"><img src="{{ logo_form.logo.get_url_small() }}"
|
||||||
|
alt="pas de logo chargé" /></div>
|
||||||
|
</td>
|
||||||
|
<td style="vertical-align: top;">
|
||||||
|
{{ logo_form.form.dept_id() }}
|
||||||
|
{{ logo_form.form.logo_id() }}
|
||||||
|
Nom: {{ logo_form.form.logo.logoname }}<br/>
|
||||||
|
{# {{ logo_form.form.description }}<br/>#}
|
||||||
|
Format: {{ logo_form.logo.suffix }}<br/>
|
||||||
|
Taille en px: {{ logo_form.logo.size }}<br/>
|
||||||
|
{% if logo_form.logo.mm %}
|
||||||
|
Taile en mm: {{ logo_form.logo.mm }}<br/>
|
||||||
|
{% endif %}
|
||||||
|
Aspect ratio: {{ logo_form.logo.aspect_ratio }}<br/>
|
||||||
|
<hr/>
|
||||||
|
Usage: {{ logo_form.logo.get_usage() }}
|
||||||
|
<hr/>
|
||||||
|
<span class="wtf-field">{{ render_field(logo_form.upload) }}</span>
|
||||||
|
{% if logo_form.can_delete %}
|
||||||
|
{{ render_field(logo_form.do_delete) }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
|
||||||
|
{#{% block app_content %}#}
|
||||||
|
|
||||||
|
{% if scodoc_dept %}
|
||||||
|
<h1>Logos du département {{ scodoc_dept }}</h1>
|
||||||
|
{% else %}
|
||||||
|
<h1>Configuration générale</h1>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form class="sco-form" action="" method="post" enctype="multipart/form-data" novalidate>
|
||||||
|
{{ form.hidden_tag() }}
|
||||||
|
{% if not scodoc_dept %}
|
||||||
|
<div class="sco_help">Les paramètres donnés ici s'appliquent à tout ScoDoc (tous les départements):</div>
|
||||||
|
{{ render_field(form.bonus_sport_func_name)}}
|
||||||
|
<div class="configuration_logo">
|
||||||
|
<table>
|
||||||
|
{{ render_logo(form.header, 'Logo en-tête') }}
|
||||||
|
{{ render_logo(form.footer, 'Logo pied de page') }}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- <div class="sco_help">Les paramètres ci-dessous peuvent être changés dans chaque département
|
||||||
|
(paramétrage).<br />On indique ici les valeurs initiales par défaut:
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
<div class="sco-submit">{{ form.submit() }}</div>
|
||||||
|
</form>
|
||||||
|
{#{% endblock %}#}
|
@ -1,10 +1,12 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% import 'bootstrap/wtf.html' as wtf %}
|
{% import 'bootstrap/wtf.html' as wtf %}
|
||||||
|
|
||||||
{% macro render_field(field) %}
|
{% macro render_field(field, with_label=True) %}
|
||||||
<div>
|
<div>
|
||||||
<span class="wtf-field">{{ field.label }} :</span>
|
{% if with_label %}
|
||||||
<span class="wtf-field">{{ field()|safe }}
|
<span class="wtf-field">{{ field.label }} :</span>
|
||||||
|
{% endif %}
|
||||||
|
<span class="wtf-field">{{ field(**kwargs)|safe }}
|
||||||
{% if field.errors %}
|
{% if field.errors %}
|
||||||
<ul class=errors>
|
<ul class=errors>
|
||||||
{% for error in field.errors %}
|
{% for error in field.errors %}
|
||||||
@ -16,38 +18,102 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
|
{% macro render_add_logo(add_logo_form) %}
|
||||||
|
<div class="logo-add">
|
||||||
|
<h3>Ajouter un logo</h3>
|
||||||
|
{{ 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") }}
|
||||||
|
</div>
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
{% macro render_logo(dept_form, logo_form) %}
|
||||||
|
<div class="logo-edit">
|
||||||
|
{{ logo_form.hidden_tag() }}
|
||||||
|
{% if logo_form.titre %}
|
||||||
|
<tr class="logo-edit">
|
||||||
|
<td colspan="3" class="titre">
|
||||||
|
<div class="nom"><h3>{{ logo_form.titre }}</h3></div>
|
||||||
|
<div class="description">{{ logo_form.description or "" }}</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% else %}
|
||||||
|
<tr class="logo-edit">
|
||||||
|
<td colspan="3" class="titre">
|
||||||
|
<span class="nom"><h3>Logo personalisé: {{ logo_form.logo_id.data }}</h3></span>
|
||||||
|
<span class="description">{{ logo_form.description or "" }}</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
<tr>
|
||||||
|
<td style="padding-right: 20px; ">
|
||||||
|
<div class="img-container">
|
||||||
|
<img src="{{ logo_form.logo.get_url_small() }}" alt="pas de logo chargé" /></div>
|
||||||
|
</td><td class="img-data">
|
||||||
|
<h3>{{ logo_form.logo.logoname }} (Format: {{ logo_form.logo.suffix }})</h3>
|
||||||
|
Taille: {{ logo_form.logo.size }} px
|
||||||
|
{% if logo_form.logo.mm %} / {{ logo_form.logo.mm }} mm {% endif %}<br/>
|
||||||
|
Aspect ratio: {{ logo_form.logo.aspect_ratio }}<br/>
|
||||||
|
Usage: <span style="font-family: system-ui">{{ logo_form.logo.get_usage() }}</span>
|
||||||
|
</td><td class=""img-action">
|
||||||
|
<p>Modifier l'image</p>
|
||||||
|
<span class="wtf-field">{{ render_field(logo_form.upload, False, onchange="submit_form()") }}</span>
|
||||||
|
{% if logo_form.can_delete %}
|
||||||
|
<p>Supprimer l'image</p>
|
||||||
|
{{ render_field(logo_form.do_delete, False, onSubmit="submit_form()") }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</div>
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
{% macro render_logos(dept_form) %}
|
||||||
|
<table>
|
||||||
|
{% for logo_entry in dept_form.logos.entries %}
|
||||||
|
{% set logo_form = logo_entry.form %}
|
||||||
|
{{ render_logo(dept_form, logo_form) }}
|
||||||
|
{% else %}
|
||||||
|
<p class="logo-edit"><h3>Aucun logo défini en propre à ce département</h3></p>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
{% block app_content %}
|
{% block app_content %}
|
||||||
|
|
||||||
{% if scodoc_dept %}
|
<script src="/ScoDoc/static/jQuery/jquery.js"></script>
|
||||||
<h1>Logos du département {{ scodoc_dept }}</h1>
|
<script src="/ScoDoc/static/js/configuration.js"></script>
|
||||||
{% else %}
|
|
||||||
<h1>Configuration générale {{ scodoc_dept }}</h1>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<form class="sco-form" action="" method="post" enctype="multipart/form-data" novalidate>
|
<form id="configuration_form" class="sco-form" action="" method="post" enctype="multipart/form-data" novalidate>
|
||||||
{{ form.hidden_tag() }}
|
{{ form.hidden_tag() }}
|
||||||
|
|
||||||
{% if not scodoc_dept %}
|
|
||||||
<div class="sco_help">Les paramètres donnés ici s'appliquent à tout ScoDoc (tous les départements):</div>
|
|
||||||
|
|
||||||
{{ render_field(form.bonus_sport_func_name)}}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="configuration_logo">
|
<div class="configuration_logo">
|
||||||
<h3>Logo en-tête</h3>
|
<h1>Configuration générale</h1>
|
||||||
<p class="help">image placée en haut de certains documents documents PDF. Image actuelle:</p>
|
<div class="sco_help">Les paramètres donnés ici s'appliquent à tout ScoDoc (tous les départements):</div>
|
||||||
<div class="img-container"><img src="{{ url_for('scodoc.logo_header', scodoc_dept=scodoc_dept) }}"
|
{{ render_field(form.bonus_sport_func_name, onChange="submit_form()")}}
|
||||||
alt="pas de logo chargé" /></div>
|
|
||||||
{{ render_field(form.logo_header) }}
|
<h1>Bibliothèque de logos</h1>
|
||||||
<h3>Logo pied de page</h3>
|
{% for dept_entry in form.depts.entries %}
|
||||||
<p class="help">image placée en pied de page de certains documents documents PDF. Image actuelle:</p>
|
{% set dept_form = dept_entry.form %}
|
||||||
<div class="img-container"><img src="{{ url_for('scodoc.logo_footer', scodoc_dept=g.scodoc_dept) }}"
|
{{ dept_entry.form.hidden_tag() }}
|
||||||
alt="pas de logo chargé" /></div>
|
{% if dept_entry.form.is_local() %}
|
||||||
{{ render_field(form.logo_footer) }}
|
<div class="departement">
|
||||||
|
<h2>Département {{ dept_form.dept_name.data }}</h2>
|
||||||
|
<h3>Logos locaux</h3>
|
||||||
|
<div class="sco_help">Les paramètres donnés sont spécifiques à ce département.<br/>
|
||||||
|
Les logos du département se substituent aux logos de même nom définis globalement:</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="departement">
|
||||||
|
<h2>Logos généraux</h2>
|
||||||
|
<div class="sco_help">Les images de cette section sont utilisé pour tous les départements,
|
||||||
|
mais peuvent être redéfinies localement au niveau de chaque département
|
||||||
|
(il suffit de définir un logo local de même nom)</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{{ render_logos(dept_form) }}
|
||||||
|
{{ render_add_logo(dept_form.add_logo.form) }}
|
||||||
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
<!-- <div class="sco_help">Les paramètres ci-dessous peuvent être changés dans chaque département
|
|
||||||
(paramétrage).<br />On indique ici les valeurs initiales par défaut:
|
|
||||||
</div> -->
|
|
||||||
<div class="sco-submit">{{ form.submit() }}</div>
|
|
||||||
</form>
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -30,6 +30,11 @@ Module main: page d'accueil, avec liste des départements
|
|||||||
|
|
||||||
Emmanuel Viennet, 2021
|
Emmanuel Viennet, 2021
|
||||||
"""
|
"""
|
||||||
|
import datetime
|
||||||
|
import io
|
||||||
|
|
||||||
|
import wtforms.validators
|
||||||
|
|
||||||
from app.auth.models import User
|
from app.auth.models import User
|
||||||
import os
|
import os
|
||||||
|
|
||||||
@ -38,13 +43,13 @@ from flask import abort, flash, url_for, redirect, render_template, send_file
|
|||||||
from flask import request
|
from flask import request
|
||||||
from flask.app import Flask
|
from flask.app import Flask
|
||||||
import flask_login
|
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 import FlaskForm
|
||||||
from flask_wtf.file import FileField, FileAllowed
|
from flask_wtf.file import FileField, FileAllowed
|
||||||
from werkzeug.exceptions import BadRequest, NotFound
|
from werkzeug.exceptions import BadRequest, NotFound
|
||||||
from wtforms import SelectField, SubmitField
|
from wtforms import SelectField, SubmitField, FormField, validators, Form, FieldList
|
||||||
from wtforms.fields import IntegerField
|
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
|
from wtforms.validators import ValidationError, DataRequired, Email, EqualTo
|
||||||
|
|
||||||
import app
|
import app
|
||||||
@ -52,7 +57,7 @@ from app.models import Departement, Identite
|
|||||||
from app.models import FormSemestre, FormsemestreInscription
|
from app.models import FormSemestre, FormsemestreInscription
|
||||||
from app.models import ScoDocSiteConfig
|
from app.models import ScoDocSiteConfig
|
||||||
import sco_version
|
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_find_etud
|
||||||
from app.scodoc import sco_utils as scu
|
from app.scodoc import sco_utils as scu
|
||||||
from app.decorators import (
|
from app.decorators import (
|
||||||
@ -60,11 +65,16 @@ from app.decorators import (
|
|||||||
scodoc7func,
|
scodoc7func,
|
||||||
scodoc,
|
scodoc,
|
||||||
permission_required_compat_scodoc7,
|
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_exceptions import AccessDenied
|
||||||
|
from app.scodoc.sco_logos import find_logo
|
||||||
from app.scodoc.sco_permissions import Permission
|
from app.scodoc.sco_permissions import Permission
|
||||||
from app.views import scodoc_bp as bp
|
from app.views import scodoc_bp as bp
|
||||||
|
|
||||||
|
from PIL import Image as PILImage
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/")
|
@bp.route("/")
|
||||||
@bp.route("/ScoDoc")
|
@bp.route("/ScoDoc")
|
||||||
@ -173,43 +183,6 @@ def about(scodoc_dept=None):
|
|||||||
|
|
||||||
# ---- CONFIGURATION
|
# ---- CONFIGURATION
|
||||||
|
|
||||||
|
|
||||||
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()
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
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 <tt>{','.join([e for e in scu.LOGOS_IMAGES_ALLOWED_TYPES])}</tt>",
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
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 <tt>{','.join([e for e in scu.LOGOS_IMAGES_ALLOWED_TYPES])}</tt>",
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
submit = SubmitField("Enregistrer")
|
|
||||||
|
|
||||||
|
|
||||||
# Notes pour variables config: (valeurs par défaut des paramètres de département)
|
# Notes pour variables config: (valeurs par défaut des paramètres de département)
|
||||||
# Chaines simples
|
# Chaines simples
|
||||||
# SCOLAR_FONT = "Helvetica"
|
# SCOLAR_FONT = "Helvetica"
|
||||||
@ -231,55 +204,74 @@ class ScoDocConfigurationForm(FlaskForm):
|
|||||||
@bp.route("/ScoDoc/configuration", methods=["GET", "POST"])
|
@bp.route("/ScoDoc/configuration", methods=["GET", "POST"])
|
||||||
@admin_required
|
@admin_required
|
||||||
def configuration():
|
def configuration():
|
||||||
"Panneau de configuration général"
|
auth_name = str(current_user)
|
||||||
form = ScoDocConfigurationForm(
|
if not current_user.is_administrator():
|
||||||
bonus_sport_func_name=ScoDocSiteConfig.get_bonus_sport_func_name(),
|
raise AccessDenied("invalid user (%s) must be SuperAdmin" % auth_name)
|
||||||
)
|
return sco_config_form.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")
|
|
||||||
)
|
|
||||||
if form.logo_footer.data:
|
|
||||||
sco_logos.store_image(
|
|
||||||
form.logo_footer.data, os.path.join(scu.SCODOC_LOGOS_DIR, "logo_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,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _return_logo(logo_type="header", scodoc_dept=""):
|
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
|
# stockée dans /opt/scodoc-data/config/logos donc servie manuellement ici
|
||||||
filename = sco_logos.get_logo_filename(logo_type, scodoc_dept)
|
# from app.scodoc.sco_photos import _http_jpeg_file
|
||||||
if filename:
|
|
||||||
extension = os.path.splitext(filename)[1]
|
logo = sco_logos.find_logo(name, dept_id, strict).select()
|
||||||
return send_file(filename, mimetype=f"image/{extension}")
|
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 _http_jpeg_file(logo.filepath)
|
||||||
|
# ... replaces ...
|
||||||
|
return send_file(
|
||||||
|
logo.filepath,
|
||||||
|
mimetype=f"image/{suffix}",
|
||||||
|
last_modified=datetime.datetime.now(),
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
return ""
|
abort(404)
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/ScoDoc/logo_header")
|
# small version (copy/paste from get_logo
|
||||||
@bp.route("/ScoDoc/<scodoc_dept>/logo_header")
|
@bp.route("/ScoDoc/logos/<name>/small", defaults={"dept_id": None})
|
||||||
def logo_header(scodoc_dept=""):
|
@bp.route("/ScoDoc/<int:dept_id>/logos/<name>/small")
|
||||||
"Image logo header"
|
@admin_required
|
||||||
# "/opt/scodoc-data/config/logos/logo_header")
|
def get_logo_small(name: str, dept_id: int):
|
||||||
return _return_logo(logo_type="header", scodoc_dept=scodoc_dept)
|
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(
|
||||||
@bp.route("/ScoDoc/<scodoc_dept>/logo_footer")
|
"/ScoDoc/logos/<name>", defaults={"dept_id": None}
|
||||||
def logo_footer(scodoc_dept=""):
|
) # if dept not specified, take global logo
|
||||||
"Image logo footer"
|
@bp.route("/ScoDoc/<int:dept_id>/logos/<name>")
|
||||||
return _return_logo(logo_type="footer", scodoc_dept=scodoc_dept)
|
@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"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# essais
|
# essais
|
||||||
|
@ -174,7 +174,7 @@ class DeptLogosConfigurationForm(FlaskForm):
|
|||||||
validators=[
|
validators=[
|
||||||
FileAllowed(
|
FileAllowed(
|
||||||
scu.LOGOS_IMAGES_ALLOWED_TYPES,
|
scu.LOGOS_IMAGES_ALLOWED_TYPES,
|
||||||
f"n'accepte que les fichiers image <tt>{','.join([e for e in scu.LOGOS_IMAGES_ALLOWED_TYPES])}</tt>",
|
f"n'accepte que les fichiers image <tt>{','.join(scu.LOGOS_IMAGES_ALLOWED_TYPES)}</tt>",
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@ -185,7 +185,7 @@ class DeptLogosConfigurationForm(FlaskForm):
|
|||||||
validators=[
|
validators=[
|
||||||
FileAllowed(
|
FileAllowed(
|
||||||
scu.LOGOS_IMAGES_ALLOWED_TYPES,
|
scu.LOGOS_IMAGES_ALLOWED_TYPES,
|
||||||
f"n'accepte que les fichiers image <tt>{','.join([e for e in scu.LOGOS_IMAGES_ALLOWED_TYPES])}</tt>",
|
f"n'accepte que les fichiers image <tt>{','.join(scu.LOGOS_IMAGES_ALLOWED_TYPES)}</tt>",
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@ -193,36 +193,96 @@ class DeptLogosConfigurationForm(FlaskForm):
|
|||||||
submit = SubmitField("Enregistrer")
|
submit = SubmitField("Enregistrer")
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/config_logos", methods=["GET", "POST"])
|
# @bp.route("/config_logos", methods=["GET", "POST"])
|
||||||
@permission_required(Permission.ScoChangePreferences)
|
# @permission_required(Permission.ScoChangePreferences)
|
||||||
def config_logos(scodoc_dept):
|
# def config_logos(scodoc_dept):
|
||||||
"Panneau de configuration général"
|
# "Panneau de configuration général"
|
||||||
form = DeptLogosConfigurationForm()
|
# form = DeptLogosConfigurationForm()
|
||||||
if form.validate_on_submit():
|
# if form.validate_on_submit():
|
||||||
if form.logo_header.data:
|
# if form.logo_header.data:
|
||||||
sco_logos.store_image(
|
# sco_logos.store_image(
|
||||||
form.logo_header.data,
|
# form.logo_header.data,
|
||||||
os.path.join(
|
# os.path.join(
|
||||||
scu.SCODOC_LOGOS_DIR, "logos_" + scodoc_dept, "logo_header"
|
# scu.SCODOC_LOGOS_DIR, "logos_" + scodoc_dept, "logo_header"
|
||||||
),
|
# ),
|
||||||
)
|
# )
|
||||||
if form.logo_footer.data:
|
# if form.logo_footer.data:
|
||||||
sco_logos.store_image(
|
# sco_logos.store_image(
|
||||||
form.logo_footer.data,
|
# form.logo_footer.data,
|
||||||
os.path.join(
|
# os.path.join(
|
||||||
scu.SCODOC_LOGOS_DIR, "logos_" + scodoc_dept, "logo_footer"
|
# scu.SCODOC_LOGOS_DIR, "logos_" + scodoc_dept, "logo_footer"
|
||||||
),
|
# ),
|
||||||
)
|
# )
|
||||||
app.clear_scodoc_cache()
|
# app.clear_scodoc_cache()
|
||||||
flash(f"Logos enregistrés")
|
# flash(f"Logos enregistrés")
|
||||||
return flask.redirect(url_for("scolar.index_html", scodoc_dept=scodoc_dept))
|
# 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 <tt>{','.join([e for e in scu.LOGOS_IMAGES_ALLOWED_TYPES])}</tt>",
|
||||||
|
# )
|
||||||
|
# ],
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# 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 <tt>{','.join([e for e in scu.LOGOS_IMAGES_ALLOWED_TYPES])}</tt>",
|
||||||
|
# )
|
||||||
|
# ],
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# submit = SubmitField("Enregistrer")
|
||||||
|
|
||||||
return render_template(
|
|
||||||
"configuration.html",
|
# @bp.route("/config_logos", methods=["GET", "POST"])
|
||||||
title="Configuration Logos du département",
|
# @permission_required(Permission.ScoChangePreferences)
|
||||||
form=form,
|
# def config_logos(scodoc_dept):
|
||||||
scodoc_dept=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,
|
||||||
|
# )
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------
|
# --------------------------------------------------------------------
|
||||||
|
23
scodoc.py
@ -22,6 +22,7 @@ from app import models
|
|||||||
|
|
||||||
from app.auth.models import User, Role, UserRole
|
from app.auth.models import User, Role, UserRole
|
||||||
from app.models import ScoPreference
|
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 Formation, UniteEns, Module
|
||||||
from app.models import FormSemestre, FormsemestreInscription
|
from app.models import FormSemestre, FormsemestreInscription
|
||||||
from app.models import ModuleImpl, ModuleImplInscription
|
from app.models import ModuleImpl, ModuleImplInscription
|
||||||
@ -340,6 +341,28 @@ def migrate_scodoc7_dept_archives(dept: str): # migrate-scodoc7-dept-archives
|
|||||||
tools.migrate_scodoc7_dept_archives(dept)
|
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("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()
|
@app.cli.command()
|
||||||
@click.argument("formsemestre_id", type=click.INT)
|
@click.argument("formsemestre_id", type=click.INT)
|
||||||
@click.argument("xlsfile", type=click.File("rb"))
|
@click.argument("xlsfile", type=click.File("rb"))
|
||||||
|
BIN
tests/ressources/test_logos/logo_A.jpg
Normal file
After Width: | Height: | Size: 3.7 KiB |
BIN
tests/ressources/test_logos/logo_A1.jpg
Normal file
After Width: | Height: | Size: 3.9 KiB |
BIN
tests/ressources/test_logos/logo_C.jpg
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
tests/ressources/test_logos/logo_D.png
Normal file
After Width: | Height: | Size: 21 KiB |
BIN
tests/ressources/test_logos/logo_E.jpg
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
tests/ressources/test_logos/logo_F.jpeg
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
tests/ressources/test_logos/logos_1/logo_A.jpg
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
tests/ressources/test_logos/logos_1/logo_B.jpg
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
tests/ressources/test_logos/logos_2/logo_A.jpg
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
tests/ressources/test_logos/logos_2/logo_A1.jpg
Normal file
After Width: | Height: | Size: 3.4 KiB |
244
tests/unit/test_logos.py
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""Test Logos
|
||||||
|
|
||||||
|
|
||||||
|
Utiliser comme:
|
||||||
|
pytest tests/unit/test_logos.py
|
||||||
|
|
||||||
|
"""
|
||||||
|
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,
|
||||||
|
GLOBAL,
|
||||||
|
write_logo,
|
||||||
|
delete_logo,
|
||||||
|
)
|
||||||
|
|
||||||
|
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")
|
||||||
|
dept3 = Departement(acronym="GEA")
|
||||||
|
db.session.add(dept1)
|
||||||
|
db.session.add(dept2)
|
||||||
|
db.session.add(dept3)
|
||||||
|
db.session.commit()
|
||||||
|
yield dept1, dept2, dept3
|
||||||
|
db.session.delete(dept1)
|
||||||
|
db.session.delete(dept2)
|
||||||
|
db.session.delete(dept3)
|
||||||
|
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, 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)
|
||||||
|
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, 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, 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, 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, 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, dept3 = 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 == (224, 131)
|
||||||
|
assert logo.mm == approx((9.38, 5.49), 0.1)
|
||||||
|
|
||||||
|
|
||||||
|
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 == (140, 131)
|
||||||
|
assert logo.density is None
|
||||||
|
assert logo.mm is None
|
||||||
|
|
||||||
|
|
||||||
|
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")
|
||||||
|
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):
|
||||||
|
# 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, dept3 = create_dept
|
||||||
|
logos = list_logos()
|
||||||
|
assert set(logos.keys()) == {dept1.id, dept2.id, None}
|
||||||
|
assert {"A", "C", "D", "E", "F", "header", "footer"}.issubset(
|
||||||
|
set(logos[None].keys())
|
||||||
|
)
|
||||||
|
rt = logos.get(dept1.id, None)
|
||||||
|
assert rt is not None
|
||||||
|
assert {"A", "B"}.issubset(set(rt.keys()))
|
||||||
|
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
|
@ -7,3 +7,4 @@
|
|||||||
from tools.import_scodoc7_user_db import import_scodoc7_user_db
|
from tools.import_scodoc7_user_db import import_scodoc7_user_db
|
||||||
from tools.import_scodoc7_dept import import_scodoc7_dept
|
from tools.import_scodoc7_dept import import_scodoc7_dept
|
||||||
from tools.migrate_scodoc7_archives import migrate_scodoc7_dept_archives
|
from tools.migrate_scodoc7_archives import migrate_scodoc7_dept_archives
|
||||||
|
from tools.migrate_scodoc7_logos import migrate_scodoc7_dept_logos
|
||||||
|
@ -274,6 +274,9 @@ done
|
|||||||
# ----- Post-Migration: renomme archives en fonction des nouveaux ids
|
# ----- 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"
|
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
|
# --- Si migration "en place", désactive ScoDoc 7
|
||||||
if [ "$INPLACE" == 1 ]
|
if [ "$INPLACE" == 1 ]
|
||||||
|
53
tools/migrate_scodoc7_logos.py
Normal file
@ -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")
|