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/', 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/', 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'))