403 lines
16 KiB
Python
403 lines
16 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()
|
|
return render_template('config/scale.html', competence_scale=competence_scale)
|
|
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
|
|
|
|
# Validation des données
|
|
import re
|
|
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
|
|
|
|
if success_count == len(scale_data):
|
|
flash('Échelle de réussite mise à jour avec succès', 'success')
|
|
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('/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('/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')) |