Compare commits

...

6 Commits

Author SHA1 Message Date
17964e4b7c wip 2021-11-04 15:55:48 +01:00
b1fd389504 wip: miniatures 2021-11-04 15:19:19 +01:00
fab550d4d8 wip 2021-11-04 12:05:49 +01:00
bbf7401d09 wip 2021-11-04 08:29:02 +01:00
8730df1a8d wip 2021-11-04 08:29:02 +01:00
ce4bfd9f21 wip 2021-11-04 08:29:02 +01:00
5 changed files with 386 additions and 162 deletions

View File

@ -0,0 +1,144 @@
# -*- 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
#
##############################################################################
from flask import flash, redirect, url_for, render_template
from werkzeug.utils import secure_filename
import app.scodoc.sco_utils as scu
import app
from app import Departement, ScoValueError
from app.models import ScoDocSiteConfig
from app.scodoc import sco_logos
import flask
from flask import jsonify, url_for, flash, redirect, render_template, make_response
from wtforms import SelectField, FileField, FormField, HiddenField
from flask import current_app, g, request
from flask_login import current_user
from flask_wtf import FlaskForm
from wtforms import SubmitField
from flask_wtf.file import FileField, FileAllowed
# ---- CONFIGURATION
from app.scodoc.sco_logos import list_logos
class SimpleForm(FlaskForm):
key = HiddenField("form_id")
logo = FileField(
label="Modifier l'image:",
description="logo",
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("submit")
# def __init__(self, **kwargs):
# super().__init__(**kwargs)
class LogoForm(FlaskForm):
header = FormField(SimpleForm, label="header")
footer = FormField(SimpleForm, label="footer")
logo = FileField(
label="Modifier l'image:",
description="logo",
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>",
)
],
)
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")
def configuration():
"""Panneau de configuration général"""
inventory = list_logos()
form = LogoForm()
# 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.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,
inventory=inventory,
)

View File

@ -34,30 +34,170 @@ SCODOC_LOGOS_DIR /opt/scodoc-data/config/logos
"""
import imghdr
import os
import re
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
import PIL
from PIL import Image as PILImage
GLOBAL = "_SERVER" # category for server level logos
LOGOS_DIR_PREFIX = "logos_"
LOGO_FILE_PREFIX = "logo_"
ALLOWED_EXT = "|".join(scu.LOGOS_IMAGES_ALLOWED_TYPES)
FILENAME_PARSER = re.compile(r"logo_([^.]*).(%s)" % ALLOWED_EXT)
def get_logo_filename(logo_type: str, scodoc_dept: str) -> str:
"""return full filename for this logo, or "" if not found
an existing file with extension.
logo_type: "header" or "footer"
scodoc-dept: acronym
def get_logo(logoname, dept_id=None):
"""Recherche le logo 'name' d'abord dans le département puis si non trouvé au niveau global"""
logo = Logo(logoname, dept_id)
try:
logo.read()
except ScoValueError: # logo non trouvé au niveau du département recherche au niveau global
logo = Logo(logoname=logoname, dept_id=None).read()
return logo
def get_logo_filename(name, dept_id=None):
breakpoint()
return get_logo(name, dept_id).read().filename
def get_logo_url(name, dept_id):
return get_logo(name, dept_id).read().get_url()
def write_logo(stream, name, dept_id=None):
Logo(logoname=name, dept_id=dept_id).create(stream)
def list_logos():
inventory = {GLOBAL: _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)
return inventory
def _list_dept_logos(dept_id=None):
logos = {}
path_dir = Path(scu.SCODOC_LOGOS_DIR)
if dept_id:
path_dir = Path(
os.path.sep.join([scu.SCODOC_LOGOS_DIR, 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).read()
return logos if len(logos.keys()) > 0 else None
class Logo:
"""Responsable des opérations (read, create)
du calcul des chemins
et de la récupération des informations sur un logp
"""
# Search logos in dept specific dir (/opt/scodoc-data/config/logos/logos_<dept>),
# then in config dir /opt/scodoc-data/config/logos/
for image_dir in (
scu.SCODOC_LOGOS_DIR + "/logos_" + scodoc_dept,
scu.SCODOC_LOGOS_DIR, # global logos
):
for suffix in scu.LOGOS_IMAGES_ALLOWED_TYPES:
filename = os.path.join(image_dir, f"logo_{logo_type}.{suffix}")
if os.path.isfile(filename) and os.access(filename, os.R_OK):
return filename
return ""
def __init__(self, logoname, dept_id=None):
"""Initialisation des noms et département des logos.
Le format est renseigné au moment de la lecture ou de la création du logo
"""
self.logoname = logoname
self.scodoc_dept = dept_id
self.suffix = None
self.dimensions = None
if self.scodoc_dept:
self.dirname = os.path.sep.join(
[scu.SCODOC_LOGOS_DIR, LOGOS_DIR_PREFIX + secure_filename(str(dept_id))]
)
else:
self.dirname = scu.SCODOC_LOGOS_DIR
self.basename = os.path.sep.join(
[self.dirname, LOGO_FILE_PREFIX + secure_filename(self.logoname)]
)
self.filename = None
def set_format(self, fmt):
self.suffix = fmt
self.filename = self.basename + "." + fmt
def _ensure_directory_exists(self):
"create enclosing directory if necessary"
if not Path(self.dirname).exists():
current_app.logger.info(f"sco_logos creating directory %s", self.dirname)
os.mkdir(self.dirname)
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.basename + "." + 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.basename + "." + suffix)
except IOError:
pass
def read(self):
"""
Récupération des données pour un logo existant (sinon -> Exception)
il doit exister un et un seul fichier image parmi les types autorisés
(sinon on considère le premier trouvé)
permet d'affiner le format d'un logo de format inconnu
"""
for suffix in scu.LOGOS_IMAGES_ALLOWED_TYPES:
path = Path(self.basename + "." + suffix)
if path.exists():
self.set_format(suffix)
with open(self.filename, "rb") as f:
img = PILImage.open(f)
self.dimensions = img.size
return self
# if no file found, raise exception
raise ScoValueError(
"Logo %s not found for dept %s" % (self.logoname, self.scodoc_dept)
)
def get_url(self):
if self.scodoc_dept
return url_for(
"scodoc.logo_custom", scodoc_dept=self.scodoc_dept, name=self.logoname
)
# def get_logo_filename(logo_type: str, scodoc_dept: str) -> str:
# """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>),
# # then in config dir /opt/scodoc-data/config/logos/
# for image_dir in (
# scu.SCODOC_LOGOS_DIR + "/logos_" + scodoc_dept,
# scu.SCODOC_LOGOS_DIR, # global logos
# ):
# for suffix in scu.LOGOS_IMAGES_ALLOWED_TYPES:
# filename = os.path.join(image_dir, f"logo_{logo_type}.{suffix}")
# if os.path.isfile(filename) and os.access(filename, os.R_OK):
# return filename
#
# return ""
def guess_image_type(stream) -> str:
@ -68,28 +208,3 @@ def guess_image_type(stream) -> str:
if not fmt:
return None
return fmt if fmt != "jpeg" else "jpg"
def _ensure_directory_exists(filename):
"create enclosing directory if necessary"
directory = os.path.split(filename)[0]
if not os.path.exists(directory):
current_app.logger.info(f"sco_logos creating directory %s", directory)
os.mkdir(directory)
def store_image(stream, basename):
img_type = guess_image_type(stream)
if img_type not in scu.LOGOS_IMAGES_ALLOWED_TYPES:
abort(400, "type d'image invalide")
filename = basename + "." + img_type
_ensure_directory_exists(filename)
with open(filename, "wb") as f:
f.write(stream.read())
current_app.logger.info(f"sco_logos.store_image %s", filename)
# erase other formats if they exists
for extension in set(scu.LOGOS_IMAGES_ALLOWED_TYPES) - set([img_type]):
try:
os.unlink(basename + "." + extension)
except IOError:
pass

View File

@ -18,36 +18,48 @@
{% block app_content %}
{% if scodoc_dept %}
<h1>Logos du département {{ scodoc_dept }}</h1>
{% else %}
<h1>Configuration générale {{ scodoc_dept }}</h1>
{% endif %}
{#{% if scodoc_dept %}#}
{#<h1>Logos du département {{ scodoc_dept }}</h1>#}
{#{% else %}#}
{#<h1>Configuration générale {{ scodoc_dept }}</h1>#}
{#{% endif %}#}
<form class="sco-form" action="" method="post" enctype="multipart/form-data" novalidate>
{{ form.hidden_tag() }}
{{ form.header() }}
{{ form.footer() }}
{% for logos_dept in inventory.keys() %}
<div><h2> Département: {{ logos_dept }} </h2>
{% for logo in inventory[logos_dept].values() %}
<div class="img-container"><img src="{{ logo }}"
alt="pas de logo chargé" /></div>
{# {{ render_field(form.logo_header) }}#}
<h3>Logo {{ logo.logoname }}</h3>
{% if not scodoc_dept %}
<div class="sco_help">Les paramètres donnés ici s'appliquent à tout ScoDoc (tous les départements):</div>
{% endfor %}
</div>
{% endfor %}
{# {% 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 %}
{# {{ render_field(form.bonus_sport_func_name)}}#}
{# {% endif %}#}
<div class="configuration_logo">
<h3>Logo en-tête</h3>
<p class="help">image placée en haut de certains documents documents PDF. Image actuelle:</p>
<div class="img-container"><img src="{{ url_for('scodoc.logo_header', scodoc_dept=scodoc_dept) }}"
alt="pas de logo chargé" /></div>
{{ render_field(form.logo_header) }}
<h3>Logo pied de page</h3>
<p class="help">image placée en pied de page de certains documents documents PDF. Image actuelle:</p>
<div class="img-container"><img src="{{ url_for('scodoc.logo_footer', scodoc_dept=g.scodoc_dept) }}"
alt="pas de logo chargé" /></div>
{{ render_field(form.logo_footer) }}
</div>
{# <div class="configuration_logo">#}
{# <h3>Logo en-tête</h3>#}
{# <p class="help">image placée en haut de certains documents documents PDF. Image actuelle:</p>#}
{# <div class="img-container"><img src="{{ url_for('scodoc.logo_header', scodoc_dept=scodoc_dept) }}"#}
{# alt="pas de logo chargé" /></div>#}
{# {{ render_field(form.logo_header) }}#}
{# <h3>Logo pied de page</h3>#}
{# <p class="help">image placée en pied de page de certains documents documents PDF. Image actuelle:</p>#}
{# <div class="img-container"><img src="{{ url_for('scodoc.logo_footer', scodoc_dept=g.scodoc_dept) }}"#}
{# alt="pas de logo chargé" /></div>#}
{# {{ render_field(form.logo_footer) }}#}
{# </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>
{# <div class="sco-submit">{{ form.submit() }}</div>#}
</form>
{% endblock %}

View File

@ -38,7 +38,7 @@ 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
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
@ -52,7 +52,7 @@ from app.models import Departement, Identite
from app.models import FormSemestre, NotesFormsemestreInscription
from app.models import ScoDocSiteConfig
import sco_version
from app.scodoc import sco_logos
from app.scodoc import sco_logos, sco_configuration_form
from app.scodoc import sco_find_etud
from app.scodoc import sco_utils as scu
from app.decorators import (
@ -61,7 +61,9 @@ from app.decorators import (
scodoc,
permission_required_compat_scodoc7,
)
from app.scodoc.imageresize import ImageScale
from app.scodoc.sco_exceptions import AccessDenied
from app.scodoc.sco_configuration_form import configuration
from app.scodoc.sco_permissions import Permission
from app.views import scodoc_bp as bp
@ -173,45 +175,6 @@ def about(scodoc_dept=None):
)
# ---- 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)
# Chaines simples
# SCOLAR_FONT = "Helvetica"
@ -233,45 +196,64 @@ 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.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 sco_configuration_form.configuration()
return render_template(
"configuration.html",
title="Configuration ScoDoc",
form=form,
scodoc_dept=None,
)
THUMB_WIDTH = 300
THUMB_HEIGHT = 100
def _return_logo(logo_type="header", scodoc_dept=""):
def _return_logo(logo_type="header", scodoc_dept="", small=False):
# stockée dans /opt/scodoc-data/config/logos donc servie manuellement ici
filename = sco_logos.get_logo_filename(logo_type, scodoc_dept)
if filename:
extension = os.path.splitext(filename)[1]
return send_file(filename, mimetype=f"image/{extension}")
if small:
breakpoint()
return send_file(ImageScale(filename, THUMB_WIDTH, THUMB_HEIGHT), mimetype=f"image/{extension[1:]}")
else:
return send_file(filename, mimetype=f"image/{extension}")
else:
return ""
@bp.route("/ScoDoc/logos/<name>")
def logo_server(scodoc_dept="", name=None):
"Image logo niveau général"
u = current_user
if not u.is_administrator():
raise AccessDenied("Vous n'avez pas la permission de voir cette page")
return _return_logo(logo_type=name)
@bp.route("/ScoDoc/<scodoc_dept>/logos/<name>")
def logo_dept(scodoc_dept="", name=None):
"Image logo personnalisé"
u = current_user
if not u.is_administrator():
raise AccessDenied("Vous n'avez pas la permission de voir cette page")
return _return_logo(logo_type=name, scodoc_dept=scodoc_dept)
@bp.route("/ScoDoc/logos/<name>/small")
def logo_server_small(scodoc_dept="", name=None):
"Image logo niveau général"
u = current_user
if not u.is_administrator():
raise AccessDenied("Vous n'avez pas la permission de voir cette page")
return _return_logo(logo_type=name, small=True)
@bp.route("/ScoDoc/<scodoc_dept>/logos/<name>/small")
def logo_dept_small(scodoc_dept="", name=None):
"Image logo personnalisé"
u = current_user
if not u.is_administrator():
raise AccessDenied("Vous n'avez pas la permission de voir cette page")
return _return_logo(logo_type=name, scodoc_dept=scodoc_dept, small=True)
@bp.route("/ScoDoc/logo_header")
@bp.route("/ScoDoc/<scodoc_dept>/logo_header")
def logo_header(scodoc_dept=""):
def logo_header(scodoc_dept=None):
"Image logo header"
# "/opt/scodoc-data/config/logos/logo_header")
return _return_logo(logo_type="header", scodoc_dept=scodoc_dept)
@ -279,7 +261,7 @@ def logo_header(scodoc_dept=""):
@bp.route("/ScoDoc/logo_footer")
@bp.route("/ScoDoc/<scodoc_dept>/logo_footer")
def logo_footer(scodoc_dept=""):
def logo_footer(scodoc_dept=None):
"Image logo footer"
return _return_logo(logo_type="footer", scodoc_dept=scodoc_dept)

View File

@ -38,7 +38,6 @@ from flask import jsonify, url_for, flash, render_template, make_response
from flask import current_app, g, request
from flask_login import current_user
from flask_wtf import FlaskForm
from flask_wtf.file import FileField, FileAllowed
from wtforms import SubmitField
from app.decorators import (
@ -165,34 +164,6 @@ def doc_preferences():
return response
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")
@bp.route("/config_logos", methods=["GET", "POST"])
@permission_required(Permission.ScoChangePreferences)
def config_logos(scodoc_dept):