567 lines
23 KiB
Python
567 lines
23 KiB
Python
from flask import Blueprint, render_template, request, flash, redirect, url_for, jsonify
|
|
from app_config import config_manager
|
|
from models import db, AppConfig, CompetenceScaleValue, Competence
|
|
from utils import handle_error, handle_db_errors
|
|
import logging
|
|
|
|
bp = Blueprint('config', __name__, url_prefix='/config')
|
|
|
|
@bp.route('/')
|
|
def index():
|
|
"""Page principale de configuration."""
|
|
try:
|
|
return render_template('config/index.html', app_config=config_manager)
|
|
except Exception as e:
|
|
return handle_error(e, "Erreur lors du chargement de la configuration")
|
|
|
|
@bp.route('/api/get')
|
|
def api_get_config():
|
|
"""API pour récupérer la configuration actuelle."""
|
|
try:
|
|
config_data = {
|
|
'context': {
|
|
'school_year': config_manager.get('context.school_year')
|
|
},
|
|
'evaluations': {
|
|
'default_grading_system': config_manager.get('evaluations.default_grading_system'),
|
|
'competence_scale': config_manager.get_competence_scale_values(),
|
|
'competences': config_manager.get_competences_list()
|
|
}
|
|
}
|
|
return jsonify(config_data)
|
|
except Exception as e:
|
|
logging.error(f"Erreur API configuration: {e}")
|
|
return jsonify({'error': 'Erreur lors de la récupération de la configuration'}), 500
|
|
|
|
@bp.route('/api/update', methods=['POST'])
|
|
def api_update_config():
|
|
"""API pour mettre à jour la configuration."""
|
|
try:
|
|
data = request.get_json()
|
|
|
|
if not data:
|
|
return jsonify({'error': 'Données manquantes'}), 400
|
|
|
|
# Mettre à jour la configuration
|
|
for section, values in data.items():
|
|
if isinstance(values, dict):
|
|
for key, value in values.items():
|
|
config_manager.set(f"{section}.{key}", value)
|
|
else:
|
|
config_manager.set(section, values)
|
|
|
|
# Sauvegarder
|
|
if config_manager.save():
|
|
flash('Configuration mise à jour avec succès', 'success')
|
|
return jsonify({'success': True, 'message': 'Configuration sauvegardée'})
|
|
else:
|
|
return jsonify({'error': 'Erreur lors de la sauvegarde'}), 500
|
|
|
|
except Exception as e:
|
|
logging.error(f"Erreur mise à jour configuration: {e}")
|
|
return jsonify({'error': 'Erreur lors de la mise à jour'}), 500
|
|
|
|
@bp.route('/competences')
|
|
def competences():
|
|
"""Page de configuration des compétences."""
|
|
try:
|
|
competences_list = config_manager.get_competences_list()
|
|
competence_scale = config_manager.get_competence_scale_values()
|
|
return render_template('config/competences.html',
|
|
competences=competences_list,
|
|
competence_scale=competence_scale)
|
|
except Exception as e:
|
|
return handle_error(e, "Erreur lors du chargement des compétences")
|
|
|
|
@bp.route('/competences/add', methods=['POST'])
|
|
@handle_db_errors
|
|
def add_competence():
|
|
"""Ajouter une nouvelle compétence."""
|
|
name = request.form.get('name')
|
|
color = request.form.get('color', '#3b82f6')
|
|
icon = request.form.get('icon', 'star')
|
|
|
|
if not name:
|
|
flash('Le nom de la compétence est requis', 'error')
|
|
return redirect(url_for('config.competences'))
|
|
|
|
# Validation de la couleur hexadécimale
|
|
import re
|
|
if not re.match(r'^#[0-9a-fA-F]{6}$', color):
|
|
flash('Format de couleur invalide', 'error')
|
|
return redirect(url_for('config.competences'))
|
|
|
|
# Vérifier si le nom existe déjà
|
|
if Competence.query.filter_by(name=name).first():
|
|
flash(f'Une compétence "{name}" existe déjà', 'error')
|
|
return redirect(url_for('config.competences'))
|
|
|
|
if config_manager.add_competence(name, color, icon):
|
|
flash(f'Compétence "{name}" ajoutée avec succès', 'success')
|
|
else:
|
|
flash('Erreur lors de la sauvegarde', 'error')
|
|
|
|
return redirect(url_for('config.competences'))
|
|
|
|
@bp.route('/competences/update', methods=['POST'])
|
|
@handle_db_errors
|
|
def update_competence():
|
|
"""Modifier une compétence existante."""
|
|
try:
|
|
edit_index = request.form.get('edit_index')
|
|
name = request.form.get('name')
|
|
color = request.form.get('color', '#3b82f6')
|
|
icon = request.form.get('icon', 'star')
|
|
|
|
if not edit_index or not name:
|
|
flash('Données manquantes pour la modification', 'error')
|
|
return redirect(url_for('config.competences'))
|
|
|
|
index = int(edit_index)
|
|
competences = Competence.query.order_by(Competence.order_index).all()
|
|
|
|
if 0 <= index < len(competences):
|
|
# Validation de la couleur hexadécimale
|
|
import re
|
|
if not re.match(r'^#[0-9a-fA-F]{6}$', color):
|
|
flash('Format de couleur invalide', 'error')
|
|
return redirect(url_for('config.competences'))
|
|
|
|
competence = competences[index]
|
|
|
|
# Vérifier si le nom existe déjà (sauf pour l'élément en cours d'édition)
|
|
existing = Competence.query.filter(Competence.name == name, Competence.id != competence.id).first()
|
|
if existing:
|
|
flash(f'Une compétence "{name}" existe déjà', 'error')
|
|
return redirect(url_for('config.competences'))
|
|
|
|
old_name = competence.name
|
|
|
|
if config_manager.update_competence(competence.id, name, color, icon):
|
|
flash(f'Compétence "{old_name}" modifiée en "{name}"', 'success')
|
|
else:
|
|
flash('Erreur lors de la sauvegarde', 'error')
|
|
else:
|
|
flash('Compétence introuvable', 'error')
|
|
|
|
except Exception as e:
|
|
logging.error(f"Erreur modification compétence: {e}")
|
|
flash('Erreur lors de la modification', 'error')
|
|
|
|
return redirect(url_for('config.competences'))
|
|
|
|
@bp.route('/competences/delete/<int:index>', methods=['POST'])
|
|
@handle_db_errors
|
|
def delete_competence(index):
|
|
"""Supprimer une compétence."""
|
|
try:
|
|
competences = Competence.query.order_by(Competence.order_index).all()
|
|
|
|
if 0 <= index < len(competences):
|
|
competence = competences[index]
|
|
deleted_name = competence.name
|
|
|
|
if config_manager.delete_competence(competence.id):
|
|
flash(f'Compétence "{deleted_name}" supprimée', 'success')
|
|
else:
|
|
flash('Erreur lors de la sauvegarde', 'error')
|
|
else:
|
|
flash('Compétence introuvable', 'error')
|
|
|
|
except Exception as e:
|
|
logging.error(f"Erreur suppression compétence: {e}")
|
|
flash('Erreur lors de la suppression', 'error')
|
|
|
|
return redirect(url_for('config.competences'))
|
|
|
|
@bp.route('/scale')
|
|
def scale():
|
|
"""Page de configuration de l'échelle de réussite."""
|
|
try:
|
|
competence_scale = config_manager.get_competence_scale_values()
|
|
|
|
# Récupérer la configuration du dégradé des notes
|
|
notes_gradient = {
|
|
'min_color': config_manager.get('grading.notes_gradient.min_color', '#dc2626'),
|
|
'max_color': config_manager.get('grading.notes_gradient.max_color', '#059669'),
|
|
'enabled': config_manager.get('grading.notes_gradient.enabled', False)
|
|
}
|
|
|
|
return render_template('config/scale.html',
|
|
competence_scale=competence_scale,
|
|
notes_gradient=notes_gradient)
|
|
except Exception as e:
|
|
return handle_error(e, "Erreur lors du chargement de l'échelle")
|
|
|
|
@bp.route('/scale/update', methods=['POST'])
|
|
def update_scale():
|
|
"""Mettre à jour l'échelle de réussite."""
|
|
try:
|
|
scale_data = {}
|
|
|
|
# Récupérer tous les champs du formulaire
|
|
for key in request.form.keys():
|
|
if key.startswith('scale_'):
|
|
parts = key.split('_', 2) # scale_VALUE_FIELD
|
|
if len(parts) == 3:
|
|
value, field = parts[1], parts[2]
|
|
|
|
if value not in scale_data:
|
|
scale_data[value] = {}
|
|
|
|
form_value = request.form[key]
|
|
if field == 'included_in_total':
|
|
form_value = form_value.lower() == 'true'
|
|
|
|
scale_data[value][field] = form_value
|
|
|
|
# Récupérer aussi les paramètres du dégradé de couleurs des notes
|
|
notes_gradient_min = request.form.get('notes_gradient_color_min')
|
|
notes_gradient_max = request.form.get('notes_gradient_color_max')
|
|
|
|
# Validation des données
|
|
import re
|
|
|
|
# Validation des couleurs du dégradé si présentes
|
|
if notes_gradient_min and notes_gradient_max:
|
|
if not re.match(r'^#[0-9a-fA-F]{6}$', notes_gradient_min) or not re.match(r'^#[0-9a-fA-F]{6}$', notes_gradient_max):
|
|
flash('Format de couleur invalide pour le dégradé', 'error')
|
|
return redirect(url_for('config.scale'))
|
|
|
|
for value, config in scale_data.items():
|
|
# Vérifier que tous les champs requis sont présents
|
|
if not all(field in config for field in ['label', 'color', 'included_in_total']):
|
|
flash(f'Données manquantes pour la valeur {value}', 'error')
|
|
return redirect(url_for('config.scale'))
|
|
|
|
# Valider le format de couleur
|
|
if not re.match(r'^#[0-9a-fA-F]{6}$', config['color']):
|
|
flash(f'Format de couleur invalide pour la valeur {value}', 'error')
|
|
return redirect(url_for('config.scale'))
|
|
|
|
# Valider que le libellé n'est pas vide
|
|
if not config['label'].strip():
|
|
flash(f'Le libellé ne peut pas être vide pour la valeur {value}', 'error')
|
|
return redirect(url_for('config.scale'))
|
|
|
|
# Mettre à jour chaque valeur
|
|
success_count = 0
|
|
for value, config in scale_data.items():
|
|
if config_manager.update_scale_value(
|
|
value,
|
|
config['label'],
|
|
config['color'],
|
|
config['included_in_total']
|
|
):
|
|
success_count += 1
|
|
|
|
# Sauvegarder le dégradé des notes si présent
|
|
gradient_saved = True
|
|
if notes_gradient_min and notes_gradient_max:
|
|
config_manager.set('grading.notes_gradient.min_color', notes_gradient_min)
|
|
config_manager.set('grading.notes_gradient.max_color', notes_gradient_max)
|
|
config_manager.set('grading.notes_gradient.enabled', True)
|
|
gradient_saved = config_manager.save()
|
|
|
|
# Messages de succès
|
|
if success_count == len(scale_data) and gradient_saved:
|
|
if notes_gradient_min and notes_gradient_max:
|
|
flash('Paramètres mis à jour avec succès (échelle et dégradé)', 'success')
|
|
else:
|
|
flash('Échelle de réussite mise à jour avec succès', 'success')
|
|
elif success_count == len(scale_data) and not gradient_saved:
|
|
flash('Échelle mise à jour mais erreur sauvegarde dégradé', 'warning')
|
|
else:
|
|
flash(f'Mise à jour partielle : {success_count}/{len(scale_data)} valeurs mises à jour', 'warning')
|
|
|
|
except Exception as e:
|
|
logging.error(f"Erreur mise à jour échelle: {e}")
|
|
flash('Erreur lors de la mise à jour', 'error')
|
|
|
|
return redirect(url_for('config.scale'))
|
|
|
|
@bp.route('/scale/add', methods=['POST'])
|
|
def add_scale_value():
|
|
"""Ajouter une nouvelle valeur à l'échelle."""
|
|
try:
|
|
value = request.form.get('value')
|
|
label = request.form.get('label')
|
|
color = request.form.get('color', '#6b7280')
|
|
included_in_total = request.form.get('included_in_total', 'true').lower() == 'true'
|
|
|
|
if not value or not label:
|
|
flash('La valeur et le libellé sont requis', 'error')
|
|
return redirect(url_for('config.scale'))
|
|
|
|
# Validation de la couleur hexadécimale
|
|
import re
|
|
if not re.match(r'^#[0-9a-fA-F]{6}$', color):
|
|
flash('Format de couleur invalide', 'error')
|
|
return redirect(url_for('config.scale'))
|
|
|
|
# Vérifier si la valeur existe déjà
|
|
if CompetenceScaleValue.query.filter_by(value=value).first():
|
|
flash(f'La valeur "{value}" existe déjà dans l\'échelle', 'error')
|
|
return redirect(url_for('config.scale'))
|
|
|
|
if config_manager.add_scale_value(value, label.strip(), color, included_in_total):
|
|
flash(f'Valeur "{value}" ajoutée à l\'échelle avec succès', 'success')
|
|
else:
|
|
flash('Erreur lors de la sauvegarde', 'error')
|
|
|
|
except Exception as e:
|
|
logging.error(f"Erreur ajout valeur échelle: {e}")
|
|
flash('Erreur lors de l\'ajout de la valeur', 'error')
|
|
|
|
return redirect(url_for('config.scale'))
|
|
|
|
@bp.route('/scale/delete/<path:value>', methods=['POST'])
|
|
def delete_scale_value(value):
|
|
"""Supprimer une valeur de l'échelle."""
|
|
try:
|
|
# Vérifier si la valeur existe
|
|
scale_value = CompetenceScaleValue.query.filter_by(value=value).first()
|
|
if not scale_value:
|
|
flash(f'La valeur "{value}" n\'existe pas dans l\'échelle', 'error')
|
|
return redirect(url_for('config.scale'))
|
|
|
|
# Empêcher la suppression des valeurs de base (0, 1, 2, 3, .)
|
|
base_values = ['0', '1', '2', '3', '.']
|
|
if value in base_values:
|
|
flash(f'La valeur "{value}" est une valeur de base et ne peut pas être supprimée', 'error')
|
|
return redirect(url_for('config.scale'))
|
|
|
|
label = scale_value.label
|
|
|
|
if config_manager.delete_scale_value(value):
|
|
flash(f'Valeur "{value}" ({label}) supprimée de l\'échelle', 'success')
|
|
else:
|
|
flash('Erreur lors de la sauvegarde', 'error')
|
|
|
|
except Exception as e:
|
|
logging.error(f"Erreur suppression valeur échelle: {e}")
|
|
flash('Erreur lors de la suppression', 'error')
|
|
|
|
return redirect(url_for('config.scale'))
|
|
|
|
@bp.route('/scale/reset', methods=['POST'])
|
|
def reset_scale():
|
|
"""Réinitialiser l'échelle aux valeurs par défaut."""
|
|
try:
|
|
# Supprimer toutes les valeurs existantes
|
|
CompetenceScaleValue.query.delete()
|
|
|
|
# Réajouter les valeurs par défaut
|
|
default_scale = config_manager.default_config['evaluations']['competence_scale']['values']
|
|
for value, config in default_scale.items():
|
|
scale_value = CompetenceScaleValue(
|
|
value=value,
|
|
label=config['label'],
|
|
color=config['color'],
|
|
included_in_total=config['included_in_total']
|
|
)
|
|
db.session.add(scale_value)
|
|
|
|
db.session.commit()
|
|
flash('Échelle réinitialisée aux valeurs par défaut', 'success')
|
|
|
|
except Exception as e:
|
|
db.session.rollback()
|
|
logging.error(f"Erreur réinitialisation échelle: {e}")
|
|
flash('Erreur lors de la réinitialisation', 'error')
|
|
|
|
return redirect(url_for('config.scale'))
|
|
|
|
|
|
@bp.route('/scale/notes-gradient', methods=['POST'])
|
|
def save_notes_gradient():
|
|
"""Sauvegarder la configuration du dégradé de couleurs pour les notes."""
|
|
try:
|
|
min_color = request.form.get('notes_gradient_min_color', '#dc2626')
|
|
max_color = request.form.get('notes_gradient_max_color', '#059669')
|
|
|
|
# Validation des couleurs hexadécimales
|
|
import re
|
|
if not re.match(r'^#[0-9a-fA-F]{6}$', min_color) or not re.match(r'^#[0-9a-fA-F]{6}$', max_color):
|
|
flash('Format de couleur invalide', 'error')
|
|
return redirect(url_for('config.scale'))
|
|
|
|
# Sauvegarder dans la configuration
|
|
config_manager.set('grading.notes_gradient.min_color', min_color)
|
|
config_manager.set('grading.notes_gradient.max_color', max_color)
|
|
config_manager.set('grading.notes_gradient.enabled', True)
|
|
|
|
if config_manager.save():
|
|
flash('Configuration du dégradé de couleurs des notes sauvegardée avec succès', 'success')
|
|
else:
|
|
flash('Erreur lors de la sauvegarde', 'error')
|
|
|
|
except Exception as e:
|
|
logging.error(f"Erreur sauvegarde dégradé notes: {e}")
|
|
flash('Erreur lors de la sauvegarde du dégradé', 'error')
|
|
|
|
return redirect(url_for('config.scale'))
|
|
|
|
@bp.route('/general')
|
|
def general():
|
|
"""Page de configuration générale."""
|
|
try:
|
|
return render_template('config/general.html',
|
|
app_config=config_manager,
|
|
school_year=config_manager.get_school_year(),
|
|
default_grading_system=config_manager.get_default_grading_system())
|
|
except Exception as e:
|
|
return handle_error(e, "Erreur lors du chargement de la configuration générale")
|
|
|
|
@bp.route('/general/update', methods=['POST'])
|
|
@handle_db_errors
|
|
def update_general():
|
|
"""Mettre à jour la configuration générale."""
|
|
try:
|
|
school_year = request.form.get('school_year')
|
|
default_grading_system = request.form.get('default_grading_system')
|
|
|
|
if not school_year:
|
|
flash('L\'année scolaire est requise', 'error')
|
|
return redirect(url_for('config.general'))
|
|
|
|
# Validation de l'année scolaire (format YYYY-YYYY)
|
|
import re
|
|
if not re.match(r'^\d{4}-\d{4}$', school_year):
|
|
flash('Format d\'année scolaire invalide (attendu: YYYY-YYYY)', 'error')
|
|
return redirect(url_for('config.general'))
|
|
|
|
# Mettre à jour la configuration
|
|
config_manager.set('context.school_year', school_year)
|
|
config_manager.set('evaluations.default_grading_system', default_grading_system)
|
|
|
|
if config_manager.save():
|
|
flash('Configuration générale mise à jour avec succès', 'success')
|
|
else:
|
|
flash('Erreur lors de la sauvegarde', 'error')
|
|
|
|
except Exception as e:
|
|
logging.error(f"Erreur mise à jour config générale: {e}")
|
|
flash('Erreur lors de la mise à jour', 'error')
|
|
|
|
return redirect(url_for('config.general'))
|
|
|
|
@bp.route('/email')
|
|
def email():
|
|
"""Page de configuration email."""
|
|
try:
|
|
# Récupérer la configuration email actuelle
|
|
email_config = {
|
|
'smtp_host': config_manager.get('email.smtp_host', ''),
|
|
'smtp_port': config_manager.get('email.smtp_port', '587'),
|
|
'username': config_manager.get('email.username', ''),
|
|
'password': config_manager.get('email.password', ''),
|
|
'use_tls': config_manager.get('email.use_tls', 'true') == 'true',
|
|
'from_name': config_manager.get('email.from_name', 'Notytex'),
|
|
'from_address': config_manager.get('email.from_address', ''),
|
|
}
|
|
|
|
return render_template('config/email.html', email_config=email_config)
|
|
except Exception as e:
|
|
return handle_error(e, "Erreur lors du chargement de la configuration email")
|
|
|
|
@bp.route('/email/update', methods=['POST'])
|
|
@handle_db_errors
|
|
def update_email():
|
|
"""Mettre à jour la configuration email."""
|
|
try:
|
|
# Récupérer les données du formulaire
|
|
smtp_host = request.form.get('smtp_host', '').strip()
|
|
smtp_port = request.form.get('smtp_port', '587').strip()
|
|
username = request.form.get('username', '').strip()
|
|
password = request.form.get('password', '').strip()
|
|
use_tls = request.form.get('use_tls') == 'on'
|
|
from_name = request.form.get('from_name', 'Notytex').strip()
|
|
from_address = request.form.get('from_address', '').strip()
|
|
|
|
# Validation des données
|
|
if smtp_host and not smtp_port.isdigit():
|
|
flash('Le port SMTP doit être un nombre', 'error')
|
|
return redirect(url_for('config.email'))
|
|
|
|
if from_address:
|
|
import re
|
|
email_regex = re.compile(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$')
|
|
if not email_regex.match(from_address):
|
|
flash('Format d\'adresse email invalide', 'error')
|
|
return redirect(url_for('config.email'))
|
|
|
|
# Sauvegarder la configuration
|
|
config_manager.set('email.smtp_host', smtp_host)
|
|
config_manager.set('email.smtp_port', smtp_port)
|
|
config_manager.set('email.username', username)
|
|
config_manager.set('email.password', password)
|
|
config_manager.set('email.use_tls', 'true' if use_tls else 'false')
|
|
config_manager.set('email.from_name', from_name)
|
|
config_manager.set('email.from_address', from_address)
|
|
|
|
if config_manager.save():
|
|
flash('Configuration email mise à jour avec succès', 'success')
|
|
else:
|
|
flash('Erreur lors de la sauvegarde', 'error')
|
|
|
|
except Exception as e:
|
|
logging.error(f"Erreur mise à jour config email: {e}")
|
|
flash('Erreur lors de la mise à jour', 'error')
|
|
|
|
return redirect(url_for('config.email'))
|
|
|
|
@bp.route('/email/test', methods=['POST'])
|
|
@handle_db_errors
|
|
def test_email():
|
|
"""Tester la configuration email."""
|
|
try:
|
|
test_email_address = request.form.get('test_email', '').strip()
|
|
|
|
if not test_email_address:
|
|
flash('Adresse email de test requise', 'error')
|
|
return redirect(url_for('config.email'))
|
|
|
|
# Validation de l'adresse email
|
|
import re
|
|
email_regex = re.compile(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$')
|
|
if not email_regex.match(test_email_address):
|
|
flash('Format d\'adresse email invalide', 'error')
|
|
return redirect(url_for('config.email'))
|
|
|
|
# Tenter l'envoi d'un email de test
|
|
from services.email_service import EmailService
|
|
email_service = EmailService(config_manager)
|
|
result = email_service.send_test_email(test_email_address)
|
|
|
|
if result['success']:
|
|
flash(f'Email de test envoyé avec succès à {test_email_address}', 'success')
|
|
else:
|
|
flash(f'Erreur lors de l\'envoi du test: {result["error"]}', 'error')
|
|
|
|
except Exception as e:
|
|
logging.error(f"Erreur test email: {e}")
|
|
flash('Erreur lors du test d\'envoi', 'error')
|
|
|
|
return redirect(url_for('config.email'))
|
|
|
|
@bp.route('/reset', methods=['POST'])
|
|
def reset_config():
|
|
"""Réinitialise la configuration aux valeurs par défaut."""
|
|
try:
|
|
# Supprimer toute la configuration existante
|
|
AppConfig.query.delete()
|
|
CompetenceScaleValue.query.delete()
|
|
Competence.query.delete()
|
|
|
|
# Réinitialiser avec les valeurs par défaut
|
|
config_manager.initialize_default_config()
|
|
|
|
flash('Configuration réinitialisée aux valeurs par défaut', 'success')
|
|
|
|
except Exception as e:
|
|
db.session.rollback()
|
|
logging.error(f"Erreur réinitialisation: {e}")
|
|
flash('Erreur lors de la réinitialisation', 'error')
|
|
|
|
return redirect(url_for('config.index')) |