This commit is contained in:
Jean-Marie Place 2021-11-26 17:32:06 +01:00 committed by Jean-Marie Place
parent a9986e9522
commit f40c0d8a6e
6 changed files with 314 additions and 154 deletions

View File

@ -30,49 +30,19 @@ Module main: page d'accueil, avec liste des départements
Emmanuel Viennet, 2021
"""
import abc
import io
from collections import OrderedDict
import re
import wtforms.validators
from app.auth.models import User
import os
import flask
from flask import abort, flash, url_for, redirect, render_template, send_file
from flask import request
from flask.app import Flask
import flask_login
from flask_login.utils import login_required, current_user
from flask import flash, url_for, redirect, render_template
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, FieldList
from wtforms.fields import IntegerField
from wtforms.fields.simple import BooleanField, StringField, TextAreaField, HiddenField
from wtforms.validators import ValidationError, DataRequired, Email, EqualTo
from wtforms import SelectField, SubmitField, FormField, validators, FieldList
from wtforms.fields.simple import BooleanField, StringField, HiddenField
import app
from app.models import Departement, Identite
from app.models import FormSemestre, NotesFormsemestreInscription
from app.models import Departement
from app.models import ScoDocSiteConfig
import sco_version
from app.scodoc import sco_logos, html_sco_header
from app.scodoc import sco_find_etud
from app.scodoc import sco_utils as scu
from app.decorators import (
admin_required,
scodoc7func,
scodoc,
permission_required_compat_scodoc7,
)
from app.scodoc.sco_exceptions import AccessDenied
from app.scodoc.sco_logos import find_logo
from app.scodoc.sco_permissions import Permission
from app.views import scodoc_bp as bp
from PIL import Image as PILImage
JAVASCRIPTS = html_sco_header.BOOTSTRAP_MULTISELECT_JS + []
@ -81,6 +51,35 @@ CSSSTYLES = html_sco_header.BOOTSTRAP_MULTISELECT_CSS
# ---- CONFIGURATION
class AddLogoForm(FlaskForm):
"""Formulaire permettant l'ajout d'un logo (dans un département)"""
dept_id = HiddenField()
name = StringField(
label="Nom",
validators=[
validators.regexp(
"[A-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"
),
],
)
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)}",
)
],
)
# class ItemForm(FlaskForm):
# """Unused Generic class to document common behavior for classes
# * ScoConfigurationForm
@ -95,78 +94,114 @@ CSSSTYLES = html_sco_header.BOOTSTRAP_MULTISELECT_CSS
# * have some information added to be displayed
# - information are collected from a model object
# Common methods:
# build_index(model) (not for LogoForm who has no child)
# 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
# * recursively calls build_index with submodel
# fill_in([key,] model)
# * fill_in additional information into the form
# key is needed for DeptForm to tell global from dept levels
# * recursively calls fill_in for each chid
# some spécific information may be added after standard processing (typically header/footer description)
# * 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)
#
# Someday we'll have it as abstract classes but Abstract FieldList seems a bit complicated
# 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 = "_"
class LogoForm(FlaskForm):
"""Embed both presentation of a logo (cf. template file configuration.html)
and all its data and UI action (change, delete)"""
dept_id = HiddenField()
logo_id = HiddenField()
upload = FileField(
label="Modifier l'image",
label="Remplacer 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])}",
f"n'accepte que les fichiers image {','.join(scu.LOGOS_IMAGES_ALLOWED_TYPES)}",
)
],
)
do_delete = BooleanField("Supprimer l'image")
do_delete = BooleanField("Supprimer l'image", default=None)
def __init__(self, *args, **kwargs):
super(LogoForm, self).__init__(*args, **kwargs)
self.index = None
super().__init__(*args, **kwargs)
self.logo = None
self.description = None
self.can_delete = True
def fill_in(self, dept_form, modele):
def build(self, modele):
self.logo = modele
self.do_delete.data = False
class DeptForm(FlaskForm):
dept_key = HiddenField()
add_logo = FormField(AddLogoForm)
logos = FieldList(FormField(LogoForm))
name = None
def __init__(self, *args, **kwargs):
super(DeptForm, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
self.index = None
self.dept_name = None
def build_index(self, modele):
def _set_local_logos_infos(self):
local_header = self.get_form("header")
if local_header:
local_header.description = "Remplace le header défini au niveau global"
local_footer = self.get_form("footer")
if local_footer:
local_footer.description = "Remplace le footer défini au niveau global"
def is_local(self):
if self.dept_name == GLOBAL:
return None
return True
@staticmethod
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 build(self, dept_name, modele):
dept_key = self.dept_key.data
self.dept_name = dept_name
self.index = {}
for logoname in modele or []:
entry = self.logos.append_entry()
self.index[logoname] = entry.form
entry.form.fill_in(self, modele[logoname])
for logoname in self._ordered_logos(modele):
self._build_logo(dept_key, logoname, modele)
if self.is_local():
self._set_local_logos_infos()
def fill_in(self, dept_key, dept_name, modele):
for logoname, logoform in self.index.items():
logoform.fill_in(self, modele[logoname])
self.name = dept_name
if dept_key is not None:
local_header = self.get_form("header")
if local_header:
local_header.description = "Remplace le header défini au niveau global"
local_footer = self.get_form("footer")
if local_footer:
local_footer.description = "Remplace le footer défini au niveau global"
def _build_logo(self, dept_key, logoname, modele):
entry = self.logos.append_entry(
{
"dept_id": dept_key,
"logo_id": logoname,
}
)
self.index[logoname] = entry.form
entry.form.build(modele[logoname])
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
else:
return self.index.get(logoname, None)
return self.index.get(logoname, None)
class ScoDocConfigurationForm(FlaskForm):
@ -179,57 +214,66 @@ class ScoDocConfigurationForm(FlaskForm):
for x in ScoDocSiteConfig.get_bonus_sport_func_names()
],
)
# header = FormField(LogoForm)
# footer = FormField(LogoForm)
# logos = FieldList(FormField(LogoForm), min_entries=2)
depts = FieldList(FormField(DeptForm))
submit = SubmitField("Enregistrer")
def __init__(self, *args, **kwargs):
super(ScoDocConfigurationForm, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
self.index = None
# 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 (Global d'abord, puis par ordre alpha de nom de département)
# build dept_keys [ (None, None), (dept_id, dept_name)... ]
self.dept_keys = [(None, None)]
self.dept_id_name = (
None # list a one tuple (dept_id, dept_name) for all departements
)
def _make_dept_id_name(self):
"""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)... ]"""
self.dept_id_name = [(None, GLOBAL)]
for dept in (
Departement.query.filter_by(visible=True)
.order_by(Departement.acronym)
.all()
):
self.dept_keys.append((dept.id, dept.acronym))
self.dept_id_name.append((dept.id, dept.acronym))
def build_index(self, modele):
self.index = {}
# create entries
for dept_key, dept_name in self.dept_keys:
entry = self.depts.append_entry()
entry.form.build_index(modele.get(dept_key, {}))
self.index[dept_key] = entry.form
def fill_in(self, modele):
for dept_key, dept_name in self.dept_keys:
self.index[dept_key].fill_in(dept_key, dept_name, modele.get(dept_key, {}))
# specific processing for globals items
global_header = self.get_form(None, "header")
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(None, "footer")
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 get_form(self, dept_id=None, logoname=None):
"""Retourne le formulaire associé à un département (get_form(dept_id))
ou à un logo (get_form(dept_id, logname))
retourne None si l'élément cherché ne peut être trouvé
def _build_dept(self, dept_id, dept_name, modele):
dept_key = dept_id or GLOBAL
data = {"dept_id": dept_key}
entry = self.depts.append_entry(data)
entry.form.build(dept_name, modele.get(dept_id, {}))
self.index[dept_key] = entry.form
def build(self, modele):
"Build the Form hierachy (DeptForm, LogoForm) and add extra data (from modele)"
self.index = {}
self._make_dept_id_name()
# 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_id, None)
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)
@ -241,9 +285,7 @@ def configuration():
bonus_sport_func_name=ScoDocSiteConfig.get_bonus_sport_func_name(),
)
modele = sco_logos.list_logos()
form.build_index(modele)
form.fill_in(modele)
form.build(modele)
if form.validate_on_submit():
ScoDocSiteConfig.set_bonus_sport_func(form.bonus_sport_func_name.data)
# if form.header.data:
@ -251,7 +293,7 @@ def configuration():
# if form.footer.data:
# sco_logos.write_logo(stream=form.footer.data, name="footer")
app.clear_scodoc_cache()
flash(f"Configuration enregistrée")
flash("Configuration enregistrée")
return redirect(url_for("scodoc.index"))
return render_template(

View File

@ -269,7 +269,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

@ -848,6 +848,9 @@ div.sco_help {
span.wtf-field ul.errors li {
color: red;
}
.configuration_logo div.img {
}
.configuration_logo div.img-container {
width: 256px;
@ -855,6 +858,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,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

@ -16,60 +16,60 @@
</div>
{% endmacro %}
{% macro render_logo(logo_form) %}
{% if logo_form.titre %}
<tr style="border= 1px, solid, black; background-color:lightblue;">
<td colspan="2" style="padding-left: 20px;">
<h3>{{ logo_form.titre }}</h3>
</td>
</tr>
{% else %}
<tr style="border= 1px, solid, black; background-color:lightblue;">
<td colspan="2" style="padding-left: 20px;">
<span style="float:left;">{{ logo_form.logo.logoname }}</span>
<span style="float:right;">{{ logo_form.description or "Pas de description" }}</span>
</td>
</tr>
{% endif %}
<tr>
<td style="padding-right: 20px; vertical-align: top;">
<div class="img-container"><img src="{{ logo_form.logo.get_url_small() }}"
alt="pas de logo chargé" /></div>
<p class="help">
<br/> Image actuelle</p>
</td>
<td style="vertical-align: top;">
{{ logo_form.dept_id() }}
{{ logo_form.logo_id() }}
Format: {{ logo_form.logo.suffix }}<br/>
Taille: {{ logo_form.logo.size }} px /
{% if logo_form.logo.mm %} {{ logo_form.logo.mm }} {% endif %}<br/>
Aspect ratio: {{ logo_form.logo.aspect_ratio }}<br/>
Usage: <span style="font-family: system-ui">{{ logo_form.logo.get_usage() }}</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>
{{ render_field(add_logo_form.name) }}
{{ render_field(add_logo_form.upload) }}
<div>
{% endmacro %}
{% macro render_dept(dept_form) %}
{% if dept_form.name %}
<h2>Logos du département {{ dept_form.name }}</h2>
<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 mais définis globalement:</div>
{% else %}
<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)}}
{% endif %}
{% macro render_logo(logo_form) %}
<div class="logo-edit">
{% if logo_form.titre %}
<tr class="logo-edit">
<td colspan="2" 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="2" class=""titre">
<span class=""nom"{{ logo_form.logo.logoname }}</span>
<span class="description">{{ logo_form.description or "" }}</span>
</td>
</tr>
{% endif %}
<tr>
<td style="padding-right: 20px; vertical-align: top;">
<div class="img-container">
<img src="{{ logo_form.logo.get_url_small() }}" alt="pas de logo chargé" /></div>
<p class="help">
<br/> Image actuelle</p>
</td>
<td class="img-data">
{{ logo_form.dept_id() }}
{{ logo_form.logo_id() }}
Format: {{ logo_form.logo.suffix }}<br/>
Taille: {{ logo_form.logo.size }} px
{% if logo_form.logo.mm %} &nbsp; / &nbsp; {{ logo_form.logo.mm }} {% endif %}<br/>
Aspect ratio: {{ logo_form.logo.aspect_ratio }}<br/>
Usage: <span style="font-family: system-ui">{{ logo_form.logo.get_usage() }}</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>
</div>
{% endmacro %}
{% macro render_logos(dept_form) %}
<table>
{% for logoform in dept_form.index.values() %}
<span class="wtf-field">{{ render_logo(logoform) }}</span>
{# <h4>{{ logoform.logo.logoname }}</h4>#}
{{ render_logo(logoform) }}
{% endfor %}
</table>
{% endmacro %}
{% endmacro %}
{% block app_content %}
@ -77,10 +77,31 @@
{{ form.hidden_tag() }}
<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)}}
{% for dept in form.index %}
{{ render_dept(form.get_form(dept)) }}
{% endfor %}
<h1>Bibliothèque de logos</h1>
{% for dept, dept_form in form.index.items() %}
{% if dept_form.is_local() %}
<div class=""departement">
<h2>Logos du département {{ dept_form.dept_name }}</h2>
<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>
{{ render_add_logo(dept_form.add_logo.form) }}
{{ render_logos(dept_form) }}
</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>
{{ render_add_logo(dept_form.add_logo.form) }}
{{ render_logos(dept_form) }}
</div>
{% endif %}
{% endfor %}
</div>
<div class="sco-submit">{{ form.submit() }}</div>

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>",
)
],
)