temp. le 19/11 12h

This commit is contained in:
Jean-Marie Place 2021-11-19 11:51:05 +01:00
parent 483c22678a
commit 2920c6f131
23 changed files with 989 additions and 198 deletions

View File

@ -33,6 +33,7 @@ from app.scodoc.sco_exceptions import (
)
from config import DevConfig
import sco_version
from flask_debugtoolbar import DebugToolbarExtension
db = SQLAlchemy()
migrate = Migrate(compare_type=True)
@ -187,6 +188,7 @@ def create_app(config_class=DevConfig):
moment.init_app(app)
cache.init_app(app)
sco_cache.CACHE = cache
toolbar = DebugToolbarExtension(app)
app.register_error_handler(ScoGenError, handle_sco_value_error)
app.register_error_handler(ScoValueError, handle_sco_value_error)

View 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()

View 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.required("Nom de logo requis (alphanumériques ou '-')"),
],
)
upload = FileField(
label="Sélectionner l'image",
validators=[
FileAllowed(
scu.LOGOS_IMAGES_ALLOWED_TYPES,
f"n'accepte que les fichiers image {', '.join(scu.LOGOS_IMAGES_ALLOWED_TYPES)}",
),
validators.required("Fichier image manquant"),
],
)
do_insert = SubmitField("ajouter une image")
def __init__(self, *args, **kwargs):
kwargs["meta"] = {"csrf": False}
super().__init__(*args, **kwargs)
def validate_name(self, name):
dept_id = dept_key_to_id(self.dept_key.data)
if dept_id == GLOBAL:
dept_id = None
if find_logo(logoname=name.data, dept_id=dept_id) is not None:
raise validators.ValidationError("Un logo de même nom existe déjà")
def select_action(self):
if self.data["do_insert"]:
if self.validate():
return LogoInsert.build_action(self.data)
return None
class LogoForm(FlaskForm):
"""Embed both presentation of a logo (cf. template file configuration.html)
and all its data and UI action (change, delete)"""
dept_key = HiddenField()
logo_id = HiddenField()
upload = FileField(
label="Remplacer l'image",
validators=[
FileAllowed(
scu.LOGOS_IMAGES_ALLOWED_TYPES,
f"n'accepte que les fichiers image {', '.join(scu.LOGOS_IMAGES_ALLOWED_TYPES)}",
)
],
)
do_delete = SubmitField("Supprimer l'image")
def __init__(self, *args, **kwargs):
kwargs["meta"] = {"csrf": False}
super().__init__(*args, **kwargs)
self.logo = find_logo(
logoname=self.logo_id.data, dept_id=dept_key_to_id(self.dept_key.data)
).select()
self.description = None
self.titre = None
self.can_delete = True
if self.dept_key.data == GLOBAL:
if self.logo_id.data == "header":
self.can_delete = False
self.description = ""
self.titre = "Logo en-tête"
if self.logo_id.data == "footer":
self.can_delete = False
self.titre = "Logo pied de page"
self.description = ""
else:
if self.logo_id.data == "header":
self.description = "Se substitue au header défini au niveau global"
self.titre = "Logo en-tête"
if self.logo_id.data == "footer":
self.description = "Se substitue au footer défini au niveau global"
self.titre = "Logo pied de page"
def select_action(self):
if self.do_delete.data and self.can_delete:
return LogoDelete.build_action(self.data)
if self.upload.data and self.validate():
return LogoUpdate.build_action(self.data)
return None
class DeptForm(FlaskForm):
dept_key = HiddenField()
dept_name = HiddenField()
add_logo = FormField(AddLogoForm)
logos = FieldList(FormField(LogoForm))
def __init__(self, *args, **kwargs):
kwargs["meta"] = {"csrf": False}
super().__init__(*args, **kwargs)
def is_local(self):
if self.dept_key.data == GLOBAL:
return None
return True
def select_action(self):
action = self.add_logo.form.select_action()
if action:
return action
for logo_entry in self.logos.entries:
logo_form = logo_entry.form
action = logo_form.select_action()
if action:
return action
return None
def get_form(self, logoname=None):
"""Retourne le formulaire associé à un logo. None si pas trouvé"""
if logoname is None: # recherche de département
return self
return self.index.get(logoname, None)
def _make_dept_id_name():
"""Cette section assute que tous les départements sont traités (y compris ceux qu'ont pas de logo au départ)
et détermine l'ordre d'affichage des DeptForm (GLOBAL d'abord, puis par ordre alpha de nom de département)
-> [ (None, None), (dept_id, dept_name)... ]"""
depts = [(None, GLOBAL)]
for dept in (
Departement.query.filter_by(visible=True).order_by(Departement.acronym).all()
):
depts.append((dept.id, dept.acronym))
return depts
def _ordered_logos(modele):
"""sort logoname alphabetically but header and footer moved at start. (since there is no space in logoname)"""
def sort(name):
if name == "header":
return " 0"
if name == "footer":
return " 1"
return name
order = sorted(modele.keys(), key=sort)
return order
def _make_dept_data(dept_id, dept_name, modele):
dept_key = dept_id_to_key(dept_id)
data = {
"dept_key": dept_key,
"dept_name": dept_name,
"add_logo": {"dept_key": dept_key},
}
logos = []
if modele is not None:
for name in _ordered_logos(modele):
logos.append({"dept_key": dept_key, "logo_id": name})
data["logos"] = logos
return data
def _make_depts_data(modele):
data = []
for dept_id, dept_name in _make_dept_id_name():
data.append(
_make_dept_data(
dept_id=dept_id, dept_name=dept_name, modele=modele.get(dept_id, None)
)
)
return data
def _make_data(bonus_sport, modele):
data = {
"bonus_sport_func_name": bonus_sport,
"depts": _make_depts_data(modele=modele),
}
return data
class ScoDocConfigurationForm(FlaskForm):
"Panneau de configuration général"
bonus_sport_func_name = SelectField(
label="Fonction de calcul des bonus sport&culture",
choices=[
(x, x if x else "Aucune")
for x in ScoDocSiteConfig.get_bonus_sport_func_names()
],
)
depts = FieldList(FormField(DeptForm))
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# def _set_global_logos_infos(self):
# "specific processing for globals items"
# global_header = self.get_form(logoname="header")
# global_header.description = (
# "image placée en haut de certains documents documents PDF."
# )
# global_header.titre = "Logo en-tête"
# global_header.can_delete = False
# global_footer = self.get_form(logoname="footer")
# global_footer.description = (
# "image placée en pied de page de certains documents documents PDF."
# )
# global_footer.titre = "Logo pied de page"
# global_footer.can_delete = False
# def _build_dept(self, dept_id, dept_name, modele):
# dept_key = dept_id or GLOBAL
# data = {"dept_key": dept_key}
# entry = self.depts.append_entry(data)
# entry.form.build(dept_name, modele.get(dept_id, {}))
# self.index[str(dept_key)] = entry.form
# def build(self, modele):
# "Build the Form hierachy (DeptForm, LogoForm) and add extra data (from modele)"
# # if entries already initialized (POST). keep subforms
# self.index = {}
# # create entries in FieldList (one entry per dept
# for dept_id, dept_name in self.dept_id_name:
# self._build_dept(dept_id=dept_id, dept_name=dept_name, modele=modele)
# self._set_global_logos_infos()
def get_form(self, dept_key=GLOBAL, logoname=None):
"""Retourne un formulaire:
* pour un département (get_form(dept_id)) ou à un logo (get_form(dept_id, logname))
* propre à un département (get_form(dept_id, logoname) ou global (get_form(logoname))
retourne None si le formulaire cherché ne peut être trouvé
"""
dept_form = self.index.get(dept_key, None)
if dept_form is None: # département non trouvé
return None
return dept_form.get_form(logoname)
def select_action(self):
if (
self.data["bonus_sport_func_name"]
!= ScoDocSiteConfig.get_bonus_sport_func_name()
):
return BonusSportUpdate(self.data)
for dept_entry in self.depts:
dept_form = dept_entry.form
action = dept_form.select_action()
if action:
return action
return None
def configuration():
"""Panneau de configuration général"""
auth_name = str(current_user)
if not current_user.is_administrator():
raise AccessDenied("invalid user (%s) must be SuperAdmin" % auth_name)
form = ScoDocConfigurationForm(
data=_make_data(
bonus_sport=ScoDocSiteConfig.get_bonus_sport_func_name(),
modele=sco_logos.list_logos(),
)
)
if form.is_submitted():
action = form.select_action()
if action:
action.execute()
flash(action.message)
return redirect(
url_for(
"scodoc.configuration",
)
)
return render_template(
"configuration.html",
scodoc_dept=None,
title="Configuration ScoDoc",
form=form,
)

View File

@ -46,7 +46,7 @@ from app import Departement, ScoValueError
from app.scodoc import sco_utils as scu
from PIL import Image as PILImage
GLOBAL = "_GLOBAL" # category for server level logos
GLOBAL = "_" # category for server level logos
def find_logo(logoname, dept_id=None, strict=False, prefix=scu.LOGO_FILE_PREFIX):
@ -70,6 +70,18 @@ def find_logo(logoname, dept_id=None, strict=False, prefix=scu.LOGO_FILE_PREFIX)
return logo
def delete_logo(name, dept_id=None):
"""Delete all files matching logo (dept_id, name) (including all allowed extensions)
Args:
name: The name of the logo
dept_id: the dept_id (if local). Use None to destroy globals logos
"""
logo = find_logo(logoname=name, dept_id=dept_id)
while logo is not None:
os.unlink(logo.select().filepath)
logo = find_logo(logoname=name, dept_id=dept_id)
def write_logo(stream, name, dept_id=None):
"""Crée le fichier logo sur le serveur.
Le suffixe du fichier (parmi LOGO_IMAGES_ALLOWED_TYPES) est déduit du contenu du stream"""
@ -79,14 +91,15 @@ def write_logo(stream, name, dept_id=None):
def list_logos():
"""Crée l'inventaire de tous les logos existants.
L'inventaire se présente comme un dictionnaire de dictionnaire de Logo:
[GLOBAL][name] pour les logos globaux
[None][name] pour les logos globaux
[dept_id][name] pour les logos propres à un département (attention id numérique du dept)
Les départements sans logos sont absents du résultat
"""
inventory = {GLOBAL: _list_dept_logos()} # logos globaux (header / footer)
inventory = {None: _list_dept_logos()} # logos globaux (header / footer)
for dept in Departement.query.filter_by(visible=True).all():
logos_dept = _list_dept_logos(dept_id=dept.id)
if logos_dept:
inventory[dept.acronym] = _list_dept_logos(dept.id)
inventory[dept.id] = _list_dept_logos(dept.id)
return inventory
@ -236,7 +249,7 @@ class Logo:
"""Retourne l'URL permettant d'obtenir l'image du logo"""
return url_for(
"scodoc.get_logo",
scodoc_dept=self.scodoc_dept_id,
dept_id=self.scodoc_dept_id,
name=self.logoname,
global_if_not_found=False,
)
@ -245,7 +258,7 @@ class Logo:
"""Retourne l'URL permettant d'obtenir l'image du logo sous forme de miniature"""
return url_for(
"scodoc.get_logo_small",
scodoc_dept=self.scodoc_dept_id,
dept_id=self.scodoc_dept_id,
name=self.logoname,
global_if_not_found=False,
)
@ -254,7 +267,7 @@ class Logo:
if self.mm is None:
return f'<logo name="{self.logoname}" width="?? mm" height="?? mm">'
else:
return f'<logo name="{self.logoname}" width="{self.mm[0]}mm" height="{self.mm[1]}mm">'
return f'<logo name="{self.logoname}" width="{self.mm[0]}mm"">'
def guess_image_type(stream) -> str:
@ -268,7 +281,6 @@ def guess_image_type(stream) -> str:
def make_logo_local(logoname, dept_name):
breakpoint()
depts = Departement.query.filter_by(acronym=dept_name).all()
if len(depts) == 0:
print(f"no dept {dept_name} found. aborting")

View File

@ -60,7 +60,6 @@ from reportlab.lib.pagesizes import letter, A4, landscape
from flask import g
import app.scodoc.sco_utils as scu
from app.scodoc.sco_logos import find_logo
from app.scodoc.sco_utils import CONFIG
from app import log
from app.scodoc.sco_exceptions import ScoGenError, ScoValueError
@ -193,6 +192,10 @@ class ScolarsPageTemplate(PageTemplate):
preferences=None, # dictionnary with preferences, required
):
"""Initialise our page template."""
from app.scodoc.sco_logos import (
find_logo,
) # defered import (solve circular dependency ->sco_logo ->scodoc, ->sco_pdf
self.preferences = preferences
self.pagesbookmarks = pagesbookmarks
self.pdfmeta_author = author

View File

@ -2017,10 +2017,10 @@ class BasePreferences(object):
H = [
html_sco_header.sco_header(page_title="Préférences"),
"<h2>Préférences globales pour %s</h2>" % scu.ScoURL(),
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>"""
if current_user.is_administrator()
else "",
# 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>"""
# if current_user.is_administrator()
# 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="msg">Attention: cliquez sur "Enregistrer les modifications" en bas de page pour appliquer vos changements !</p>
""",

View File

@ -867,6 +867,9 @@ div.sco_help {
span.wtf-field ul.errors li {
color: red;
}
.configuration_logo div.img {
}
.configuration_logo div.img-container {
width: 256px;
@ -874,6 +877,20 @@ span.wtf-field ul.errors li {
.configuration_logo div.img-container img {
max-width: 100%;
}
.configuration_logo div.img-data {
vertical-align: top;
}
.configuration_logo logo-edit titre {
background-color:lightblue;
}
.configuration_logo logo-edit nom {
float: left;
vertical-align: baseline;
}
.configuration_logo logo-edit description {
float:right;
vertical-align:baseline;
}
p.indent {
padding-left: 2em;

View File

@ -0,0 +1,6 @@
function submit_form() {
$("#configuration_form").submit();
}
$(function () {
})

View 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 %}#}

View File

@ -1,10 +1,12 @@
{% extends 'base.html' %}
{% import 'bootstrap/wtf.html' as wtf %}
{% macro render_field(field) %}
{% macro render_field(field, with_label=True) %}
<div>
<span class="wtf-field">{{ field.label }} :</span>
<span class="wtf-field">{{ field()|safe }}
{% if with_label %}
<span class="wtf-field">{{ field.label }} :</span>
{% endif %}
<span class="wtf-field">{{ field(**kwargs)|safe }}
{% if field.errors %}
<ul class=errors>
{% for error in field.errors %}
@ -16,66 +18,102 @@
</div>
{% endmacro %}
{% macro render_logo(logo_form, titre=None) %}
{% if titre %}
<tr>
<td colspan="2">
<h3>{{ titre }}</h3>
</td>
</tr>
{% endif %}
<tr>
<td style="padding-right: 20px">
<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>
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/>
Usage: {{ logo_form.logo.get_usage() }}
<span class="wtf-field">{{ logo_form.action()|safe }}</span>
<span class="wtf-field">{{ render_field(logo_form.upload) }}</span>
{% if logo_form.can_delete %}
{{ render_field(logo_form.do_delete) }}
{% endif %}
</td>
</tr>
{% 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 %} &nbsp; / &nbsp; {{ 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 %}
{% if scodoc_dept %}
<h1>Logos du département {{ scodoc_dept }}</h1>
{% else %}
<h1>Configuration générale</h1>
{% endif %}
<script src="/ScoDoc/static/jQuery/jquery.js"></script>
<script src="/ScoDoc/static/js/configuration.js"></script>
<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() }}
{% 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>
{{ form.footer.form.breakpoint(form) }}
{{ 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="configuration_logo">
<h1>Configuration générale</h1>
<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, onChange="submit_form()")}}
<div class="sco-submit">{{ form.submit() }}</div>
<h1>Bibliothèque de logos</h1>
{% for dept_entry in form.depts.entries %}
{% set dept_form = dept_entry.form %}
{{ dept_entry.form.hidden_tag() }}
{% if dept_entry.form.is_local() %}
<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>
</form>
{% endblock %}

View File

@ -46,7 +46,7 @@ from flask_login.utils import login_required, current_user
from flask_wtf import FlaskForm
from flask_wtf.file import FileField, FileAllowed
from werkzeug.exceptions import BadRequest, NotFound
from wtforms import SelectField, SubmitField, FormField, validators, Form
from wtforms import SelectField, SubmitField, FormField, validators, Form, FieldList
from wtforms.fields import IntegerField
from wtforms.fields.simple import BooleanField, StringField, TextAreaField, HiddenField
from wtforms.validators import ValidationError, DataRequired, Email, EqualTo
@ -56,7 +56,7 @@ from app.models import Departement, Identite
from app.models import FormSemestre, FormsemestreInscription
from app.models import ScoDocSiteConfig
import sco_version
from app.scodoc import sco_logos
from app.scodoc import sco_logos, sco_config_form
from app.scodoc import sco_find_etud
from app.scodoc import sco_utils as scu
from app.decorators import (
@ -64,7 +64,9 @@ from app.decorators import (
scodoc7func,
scodoc,
permission_required_compat_scodoc7,
permission_required,
)
from app.scodoc.sco_config_form import configuration
from app.scodoc.sco_exceptions import AccessDenied
from app.scodoc.sco_logos import find_logo
from app.scodoc.sco_permissions import Permission
@ -180,64 +182,6 @@ def about(scodoc_dept=None):
# ---- CONFIGURATION
class LogoForm(FlaskForm):
action = HiddenField("action")
upload = FileField(
label="Modifier l'image:",
validators=[
FileAllowed(
scu.LOGOS_IMAGES_ALLOWED_TYPES,
f"n'accepte que les fichiers image {','.join([e for e in scu.LOGOS_IMAGES_ALLOWED_TYPES])}",
)
],
)
do_delete = SubmitField("Supprimer")
def set_infos(self, logo, description=None, can_delete=None):
self.logo = logo
self.description = description
self.can_delete = can_delete
def breakpoint(self, form):
breakpoint()
def __init__(self, *args, **kwargs):
super(LogoForm, self).__init__(*args, **kwargs)
self.logo = None
self.description = None
self.can_delete = None
class ScoDocConfigurationForm(FlaskForm):
"Panneau de configuration général"
bonus_sport_func_name = SelectField(
label="Fonction de calcul des bonus sport&culture",
choices=[
(x, x if x else "Aucune")
for x in ScoDocSiteConfig.get_bonus_sport_func_names()
],
)
header = FormField(LogoForm)
footer = FormField(LogoForm)
submit = SubmitField("Enregistrer")
def __init__(self, *args, **kwargs):
super(ScoDocConfigurationForm, self).__init__(*args, **kwargs)
breakpoint()
self.header.form.set_infos(
logo=find_logo("header", dept_id=None).select(),
description="image placée en haut de certains documents documents PDF.",
can_delete=False,
)
self.footer.form.set_infos(
logo=find_logo("footer", dept_id=None).select(),
description="image placée en pied de page de certains documents documents PDF.",
can_delete=False,
)
# Notes pour variables config: (valeurs par défaut des paramètres de département)
# Chaines simples
# SCOLAR_FONT = "Helvetica"
@ -259,29 +203,13 @@ class ScoDocConfigurationForm(FlaskForm):
@bp.route("/ScoDoc/configuration", methods=["GET", "POST"])
@admin_required
def configuration():
"Panneau de configuration général"
form = ScoDocConfigurationForm(
bonus_sport_func_name=ScoDocSiteConfig.get_bonus_sport_func_name(),
)
if form.validate_on_submit():
ScoDocSiteConfig.set_bonus_sport_func(form.bonus_sport_func_name.data)
if form.header.data:
sco_logos.write_logo(stream=form.header.data, name="header")
if form.footer.data:
sco_logos.write_logo(stream=form.footer.data, name="footer")
app.clear_scodoc_cache()
flash(f"Configuration enregistrée")
return redirect(url_for("scodoc.index"))
return render_template(
"configuration.html",
title="Configuration ScoDoc",
form=form,
scodoc_dept=None,
)
auth_name = str(current_user)
if not current_user.is_administrator():
raise AccessDenied("invalid user (%s) must be SuperAdmin" % auth_name)
return sco_config_form.configuration()
SMALL_SIZE = (300, 300)
SMALL_SIZE = (200, 200)
def _return_logo(name="header", dept_id="", small=False, strict: bool = True):

View File

@ -174,7 +174,7 @@ class DeptLogosConfigurationForm(FlaskForm):
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>",
f"n'accepte que les fichiers image <tt>{','.join(scu.LOGOS_IMAGES_ALLOWED_TYPES)}</tt>",
)
],
)
@ -185,7 +185,7 @@ class DeptLogosConfigurationForm(FlaskForm):
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>",
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")
@bp.route("/config_logos", methods=["GET", "POST"])
@permission_required(Permission.ScoChangePreferences)
def config_logos(scodoc_dept):
"Panneau de configuration général"
form = DeptLogosConfigurationForm()
if form.validate_on_submit():
if form.logo_header.data:
sco_logos.store_image(
form.logo_header.data,
os.path.join(
scu.SCODOC_LOGOS_DIR, "logos_" + scodoc_dept, "logo_header"
),
)
if form.logo_footer.data:
sco_logos.store_image(
form.logo_footer.data,
os.path.join(
scu.SCODOC_LOGOS_DIR, "logos_" + scodoc_dept, "logo_footer"
),
)
app.clear_scodoc_cache()
flash(f"Logos enregistrés")
return flask.redirect(url_for("scolar.index_html", scodoc_dept=scodoc_dept))
# @bp.route("/config_logos", methods=["GET", "POST"])
# @permission_required(Permission.ScoChangePreferences)
# def config_logos(scodoc_dept):
# "Panneau de configuration général"
# form = DeptLogosConfigurationForm()
# if form.validate_on_submit():
# if form.logo_header.data:
# sco_logos.store_image(
# form.logo_header.data,
# os.path.join(
# scu.SCODOC_LOGOS_DIR, "logos_" + scodoc_dept, "logo_header"
# ),
# )
# if form.logo_footer.data:
# sco_logos.store_image(
# form.logo_footer.data,
# os.path.join(
# scu.SCODOC_LOGOS_DIR, "logos_" + scodoc_dept, "logo_footer"
# ),
# )
# app.clear_scodoc_cache()
# flash(f"Logos enregistrés")
# return flask.redirect(url_for("scolar.index_html", scodoc_dept=scodoc_dept))
#
# return render_template(
# "configuration.html",
# title="Configuration Logos du département",
# form=form,
# scodoc_dept=scodoc_dept,
# )
#
#
# class DeptLogosConfigurationForm(FlaskForm):
# "Panneau de configuration logos dept"
#
# logo_header = FileField(
# label="Modifier l'image:",
# description="logo placé en haut des documents PDF",
# validators=[
# FileAllowed(
# scu.LOGOS_IMAGES_ALLOWED_TYPES,
# f"n'accepte que les fichiers image <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",
title="Configuration Logos du département",
form=form,
scodoc_dept=scodoc_dept,
)
# @bp.route("/config_logos", methods=["GET", "POST"])
# @permission_required(Permission.ScoChangePreferences)
# def config_logos(scodoc_dept):
# "Panneau de configuration général"
# form = DeptLogosConfigurationForm()
# if form.validate_on_submit():
# if form.logo_header.data:
# sco_logos.store_image(
# form.logo_header.data,
# os.path.join(
# scu.SCODOC_LOGOS_DIR, "logos_" + scodoc_dept, "logo_header"
# ),
# )
# if form.logo_footer.data:
# sco_logos.store_image(
# form.logo_footer.data,
# os.path.join(
# scu.SCODOC_LOGOS_DIR, "logos_" + scodoc_dept, "logo_footer"
# ),
# )
# app.clear_scodoc_cache()
# flash(f"Logos enregistrés")
# return flask.redirect(url_for("scolar.index_html", scodoc_dept=scodoc_dept))
#
# return render_template(
# "configuration.html",
# title="Configuration Logos du département",
# form=form,
# scodoc_dept=scodoc_dept,
# )
# --------------------------------------------------------------------

Binary file not shown.

Before

(image error) Size: 38 KiB

After

(image error) Size: 3.7 KiB

Binary file not shown.

After

(image error) Size: 3.9 KiB

Binary file not shown.

Before

(image error) Size: 2.4 KiB

After

(image error) Size: 3.0 KiB

Binary file not shown.

Before

(image error) Size: 2.9 KiB

After

(image error) Size: 2.9 KiB

Binary file not shown.

Before

(image error) Size: 2.8 KiB

After

(image error) Size: 2.9 KiB

Binary file not shown.

Before

(image error) Size: 2.7 KiB

After

(image error) Size: 3.1 KiB

Binary file not shown.

Before

(image error) Size: 2.8 KiB

After

(image error) Size: 3.0 KiB

Binary file not shown.

Before

(image error) Size: 2.6 KiB

After

(image error) Size: 3.2 KiB

Binary file not shown.

After

(image error) Size: 3.4 KiB

View File

@ -7,7 +7,6 @@ Utiliser comme:
pytest tests/unit/test_logos.py
"""
from io import BytesIO
from pathlib import Path
from shutil import copytree, copy, rmtree
@ -18,7 +17,14 @@ import app
from app import db
from app.models import Departement
import app.scodoc.sco_utils as scu
from app.scodoc.sco_logos import find_logo, Logo, list_logos
from app.scodoc.sco_logos import (
find_logo,
Logo,
list_logos,
GLOBAL,
write_logo,
delete_logo,
)
RESOURCES_DIR = "/opt/scodoc/tests/ressources/test_logos"
@ -30,12 +36,15 @@ def create_dept(test_client):
"""
dept1 = Departement(acronym="RT")
dept2 = Departement(acronym="INFO")
dept3 = Departement(acronym="GEA")
db.session.add(dept1)
db.session.add(dept2)
db.session.add(dept3)
db.session.commit()
yield dept1, dept2
yield dept1, dept2, dept3
db.session.delete(dept1)
db.session.delete(dept2)
db.session.delete(dept3)
db.session.commit()
@ -52,9 +61,10 @@ def create_logos(create_dept):
+-- logos_{d2} --+-- logo_A.jpg
"""
dept1, dept2 = create_dept
dept1, dept2, dept3 = create_dept
d1 = dept1.id
d2 = dept2.id
d3 = dept3.id
FILE_LIST = ["logo_A.jpg", "logo_C.jpg", "logo_D.png", "logo_E.jpg", "logo_F.jpeg"]
for fn in FILE_LIST:
from_path = Path(RESOURCES_DIR).joinpath(fn)
@ -83,33 +93,33 @@ def test_select_global_only(create_logos):
def test_select_local_only(create_dept, create_logos):
dept1, dept2 = create_dept
dept1, dept2, dept3 = create_dept
B_logo = app.scodoc.sco_logos.find_logo(logoname="B", dept_id=dept1.id)
assert B_logo.filepath == f"{scu.SCODOC_LOGOS_DIR}/logos_{dept1.id}/logo_B.jpg"
def test_select_local_override_global(create_dept, create_logos):
dept1, dept2 = create_dept
dept1, dept2, dept3 = create_dept
A1_logo = app.scodoc.sco_logos.find_logo(logoname="A", dept_id=dept1.id)
assert A1_logo.filepath == f"{scu.SCODOC_LOGOS_DIR}/logos_{dept1.id}/logo_A.jpg"
def test_select_global_with_strict(create_dept, create_logos):
dept1, dept2 = create_dept
dept1, dept2, dept3 = create_dept
A_logo = app.scodoc.sco_logos.find_logo(logoname="A", dept_id=dept1.id, strict=True)
assert A_logo.filepath == f"{scu.SCODOC_LOGOS_DIR}/logos_{dept1.id}/logo_A.jpg"
def test_looks_for_non_existant_should_give_none(create_dept, create_logos):
# search for a local non-existant logo returns None
dept1, dept2 = create_dept
dept1, dept2, dept3 = create_dept
no_logo = app.scodoc.sco_logos.find_logo(logoname="Z", dept_id=dept1.id)
assert no_logo is None
def test_looks_localy_for_a_global_should_give_none(create_dept, create_logos):
# search for a local non-existant logo returns None
dept1, dept2 = create_dept
dept1, dept2, dept3 = create_dept
no_logo = app.scodoc.sco_logos.find_logo(
logoname="C", dept_id=dept1.id, strict=True
)
@ -123,8 +133,8 @@ def test_get_jpg_data(create_dept, create_logos):
assert logo.logoname == "A"
assert logo.suffix == "jpg"
assert logo.filename == "A.jpg"
assert logo.size == (1200, 600)
assert logo.mm == approx((40, 30), 0.1)
assert logo.size == (140, 121)
assert logo.mm == approx((5.74, 4.96), 0.1)
def test_get_png_without_data(create_dept, create_logos):
@ -139,10 +149,59 @@ def test_get_png_without_data(create_dept, create_logos):
assert logo.mm is None
def test_create_globale_jpg_logo(create_dept, create_logos):
def test_delete_unique_global_jpg_logo(create_dept, create_logos):
from_path = Path(RESOURCES_DIR).joinpath("logo_A.jpg")
to_path = Path(scu.SCODOC_LOGOS_DIR).joinpath("logo_W.jpg")
copy(from_path.absolute(), to_path.absolute())
assert to_path.exists()
delete_logo(name="W")
assert not to_path.exists()
def test_delete_unique_local_jpg_logo(create_dept, create_logos):
dept1, dept2, dept3 = create_dept
from_path = Path(RESOURCES_DIR).joinpath("logo_A.jpg")
to_path = Path(scu.SCODOC_LOGOS_DIR).joinpath(f"logos_{dept1.id}", "logo_W.jpg")
copy(from_path.absolute(), to_path.absolute())
assert to_path.exists()
delete_logo(name="W", dept_id=dept1.id)
assert not to_path.exists()
def test_delete_multiple_local_jpg_logo(create_dept, create_logos):
dept1, dept2, dept3 = create_dept
from_path_A = Path(RESOURCES_DIR).joinpath("logo_A.jpg")
to_path_A = Path(scu.SCODOC_LOGOS_DIR).joinpath(f"logos_{dept1.id}", "logo_V.jpg")
from_path_B = Path(RESOURCES_DIR).joinpath("logo_D.png")
to_path_B = Path(scu.SCODOC_LOGOS_DIR).joinpath(f"logos_{dept1.id}", "logo_V.png")
copy(from_path_A.absolute(), to_path_A.absolute())
copy(from_path_B.absolute(), to_path_B.absolute())
assert to_path_A.exists()
assert to_path_B.exists()
delete_logo(name="V", dept_id=dept1.id)
assert not to_path_A.exists()
assert not to_path_B.exists()
def test_create_global_jpg_logo(create_dept, create_logos):
path = Path(f"{RESOURCES_DIR}/logo_C.jpg")
logo = Logo("X") # create global logo
stream = path.open("rb")
logo_path = Path(scu.SCODOC_LOGOS_DIR).joinpath("logo_X.jpg")
assert not logo_path.exists()
write_logo(stream, name="X") # create global logo
assert logo_path.exists()
logo_path.unlink(missing_ok=True)
def test_create_locale_jpg_logo(create_dept, create_logos):
dept1, dept2, dept3 = create_dept
path = Path(f"{RESOURCES_DIR}/logo_C.jpg")
stream = path.open("rb")
logo_path = Path(scu.SCODOC_LOGOS_DIR).joinpath(f"logos_{dept1.id}", "logo_Y.jpg")
assert not logo_path.exists()
write_logo(stream, name="Y", dept_id=dept1.id) # create global logo
assert logo_path.exists()
logo_path.unlink(missing_ok=True)
def test_create_jpg_instead_of_png_logo(create_dept, create_logos):
@ -169,15 +228,17 @@ def test_create_jpg_instead_of_png_logo(create_dept, create_logos):
def test_list_logo(create_dept, create_logos):
# test only existence of copied logos. We assumes that they are OK
dept1, dept2 = create_dept
dept1, dept2, dept3 = create_dept
logos = list_logos()
assert logos.keys() == {"_GLOBAL", "RT", "INFO"}
assert set(logos.keys()) == {dept1.id, dept2.id, None}
assert {"A", "C", "D", "E", "F", "header", "footer"}.issubset(
set(logos["_GLOBAL"].keys())
set(logos[None].keys())
)
rt = logos.get("RT", None)
rt = logos.get(dept1.id, None)
assert rt is not None
assert {"A", "B"}.issubset(set(rt.keys()))
info = logos.get("INFO", None)
info = logos.get(dept2.id, None)
assert info is not None
assert {"A"}.issubset(set(rt.keys()))
gea = logos.get(dept3.id, None)
assert gea is None