From b08cc2aba46850ce7e2227457cb43827e5526e08 Mon Sep 17 00:00:00 2001 From: Bertrand Benjamin Date: Tue, 5 Aug 2025 05:09:32 +0200 Subject: [PATCH] feat: add config page --- app.py | 11 + app_config.py | 326 +++++++++++++++++++++ models.py | 46 ++- pyproject.toml | 4 +- routes/config.py | 404 ++++++++++++++++++++++++++ templates/base.html | 6 + templates/config/competences.html | 297 +++++++++++++++++++ templates/config/general.html | 169 +++++++++++ templates/config/index.html | 157 ++++++++++ templates/config/scale.html | 464 ++++++++++++++++++++++++++++++ utils.py | 8 +- 11 files changed, 1888 insertions(+), 4 deletions(-) create mode 100644 app_config.py create mode 100644 routes/config.py create mode 100644 templates/config/competences.html create mode 100644 templates/config/general.html create mode 100644 templates/config/index.html create mode 100644 templates/config/scale.html diff --git a/app.py b/app.py index 9f55594..e7be89f 100644 --- a/app.py +++ b/app.py @@ -4,11 +4,13 @@ from flask import Flask, render_template from models import db, Assessment, Student, ClassGroup from commands import init_db from config import config +from app_config import config_manager # Import blueprints from routes.assessments import bp as assessments_bp from routes.exercises import bp as exercises_bp from routes.grading import bp as grading_bp +from routes.config import bp as config_bp def create_app(config_name=None): if config_name is None: @@ -17,6 +19,9 @@ def create_app(config_name=None): app = Flask(__name__) app.config.from_object(config[config_name]) + # Initialiser la configuration de l'application + app.app_config = config_manager + # Configuration du logging if not app.debug and not app.testing: if not os.path.exists('logs'): @@ -33,11 +38,17 @@ def create_app(config_name=None): # Initialize extensions db.init_app(app) + + # Initialiser la configuration par défaut après l'initialisation de la DB + with app.app_context(): + db.create_all() + config_manager.initialize_default_config() # Register blueprints app.register_blueprint(assessments_bp) app.register_blueprint(exercises_bp) app.register_blueprint(grading_bp) + app.register_blueprint(config_bp) # Register CLI commands app.cli.add_command(init_db) diff --git a/app_config.py b/app_config.py new file mode 100644 index 0000000..b5a686d --- /dev/null +++ b/app_config.py @@ -0,0 +1,326 @@ +from typing import Dict, Any, Optional, List +from flask import current_app +from models import db, AppConfig, CompetenceScaleValue, Competence + +class ConfigManager: + """Gestionnaire de configuration de l'application Notytex utilisant SQLAlchemy.""" + + @property + def default_config(self) -> Dict[str, Any]: + """Configuration par défaut de l'application.""" + return { + 'context': { + 'school_year': '2025-2026' + }, + 'evaluations': { + 'default_grading_system': 'competences', + 'competence_scale': { + 'values': { + '0': { + 'label': 'A revoir', + 'color': '#ef4444', + 'included_in_total': True + }, + '1': { + 'label': 'Des choses justes', + 'color': '#f6d32d', + 'included_in_total': True + }, + '2': { + 'label': 'Globalement ok', + 'color': '#8ff0a4', + 'included_in_total': True + }, + '3': { + 'label': 'Parfait', + 'color': '#008000', + 'included_in_total': True + }, + '.': { + 'label': 'Pas de réponse', + 'color': '#6b7280', + 'included_in_total': True + }, + 'd': { + 'label': 'Dispensé', + 'color': '#c0bfbc', + 'included_in_total': False + } + } + }, + 'competences': [ + { + 'name': 'Calculer', + 'color': '#3b82f6', + 'icon': 'calculator' + }, + { + 'name': 'Raisonner', + 'color': '#8b5cf6', + 'icon': 'lightbulb' + }, + { + 'name': 'Communiquer', + 'color': '#06b6d4', + 'icon': 'chat-bubble-left-right' + }, + { + 'name': 'Modéliser', + 'color': '#10b981', + 'icon': 'cube' + }, + { + 'name': 'Représenter', + 'color': '#f59e0b', + 'icon': 'chart-bar' + }, + { + 'name': 'Chercher', + 'color': '#ef4444', + 'icon': 'magnifying-glass' + } + ] + } + } + + def initialize_default_config(self) -> None: + """Initialise la configuration par défaut en base de données si elle n'existe pas.""" + # Configuration simple + for section, values in self.default_config.items(): + if section == 'context': + for key, value in values.items(): + config_key = f"{section}.{key}" + if not AppConfig.query.filter_by(key=config_key).first(): + config = AppConfig(key=config_key, value=str(value)) + db.session.add(config) + elif section == 'evaluations': + # Système de notation par défaut + if 'default_grading_system' in values: + config_key = f"{section}.default_grading_system" + if not AppConfig.query.filter_by(key=config_key).first(): + config = AppConfig(key=config_key, value=values['default_grading_system']) + db.session.add(config) + + # Échelle des compétences + if CompetenceScaleValue.query.count() == 0: + scale_values = self.default_config['evaluations']['competence_scale']['values'] + for value, config in scale_values.items(): + scale_value = CompetenceScaleValue( + value=value, + label=config['label'], + color=config['color'], + included_in_total=config['included_in_total'] + ) + db.session.add(scale_value) + + # Compétences + if Competence.query.count() == 0: + competences = self.default_config['evaluations']['competences'] + for i, comp in enumerate(competences): + competence = Competence( + name=comp['name'], + color=comp['color'], + icon=comp['icon'], + order_index=i + ) + db.session.add(competence) + + db.session.commit() + + def get(self, key_path: str, default: Any = None) -> Any: + """ + Récupère une valeur de configuration. + + Args: + key_path: Chemin vers la valeur (ex: 'context.school_year') + default: Valeur par défaut si la clé n'existe pas + + Returns: + La valeur de configuration ou la valeur par défaut + """ + # Configuration simple stockée dans AppConfig + config = AppConfig.query.filter_by(key=key_path).first() + if config: + return config.value + + # Valeurs par défaut pour les cas non trouvés + return self._get_default_value(key_path, default) + + def _get_default_value(self, key_path: str, fallback: Any) -> Any: + """Récupère une valeur par défaut depuis la configuration par défaut.""" + if fallback is not None: + return fallback + + keys = key_path.split('.') + value = self.default_config + + try: + for key in keys: + value = value[key] + return value + except (KeyError, TypeError): + return None + + def set(self, key_path: str, value: Any) -> None: + """ + Définit une valeur de configuration. + + Args: + key_path: Chemin vers la valeur (ex: 'context.school_year') + value: Nouvelle valeur + """ + config = AppConfig.query.filter_by(key=key_path).first() + if config: + config.value = str(value) + else: + config = AppConfig(key=key_path, value=str(value)) + db.session.add(config) + + def save(self) -> bool: + """Sauvegarde la configuration en base de données.""" + try: + db.session.commit() + return True + except Exception as e: + db.session.rollback() + print(f"Erreur lors de la sauvegarde de la configuration: {e}") + return False + + def get_competence_scale_values(self) -> Dict[str, Dict[str, Any]]: + """Récupère les valeurs de l'échelle des compétences.""" + scale_values = CompetenceScaleValue.query.all() + result = {} + for scale_value in scale_values: + # Convertir en int si c'est un nombre, sinon garder string + key = int(scale_value.value) if scale_value.value.isdigit() else scale_value.value + result[key] = { + 'label': scale_value.label, + 'color': scale_value.color, + 'included_in_total': scale_value.included_in_total + } + return result + + def get_competences_list(self) -> List[Dict[str, Any]]: + """Récupère la liste des compétences configurées.""" + competences = Competence.query.order_by(Competence.order_index).all() + return [ + { + 'name': comp.name, + 'color': comp.color, + 'icon': comp.icon + } + for comp in competences + ] + + def get_school_year(self) -> str: + """Récupère l'année scolaire courante.""" + return self.get('context.school_year', '2025-2026') + + def get_default_grading_system(self) -> str: + """Récupère le système de notation par défaut.""" + return self.get('evaluations.default_grading_system', 'competences') + + # Méthodes spécifiques pour la gestion des compétences + + def add_competence(self, name: str, color: str, icon: str) -> bool: + """Ajoute une nouvelle compétence.""" + try: + # Obtenir le prochain index d'ordre + max_order = db.session.query(db.func.max(Competence.order_index)).scalar() or 0 + + competence = Competence( + name=name, + color=color, + icon=icon, + order_index=max_order + 1 + ) + db.session.add(competence) + db.session.commit() + return True + except Exception as e: + db.session.rollback() + print(f"Erreur lors de l'ajout de la compétence: {e}") + return False + + def update_competence(self, competence_id: int, name: str, color: str, icon: str) -> bool: + """Met à jour une compétence.""" + try: + competence = Competence.query.get(competence_id) + if competence: + competence.name = name + competence.color = color + competence.icon = icon + db.session.commit() + return True + return False + except Exception as e: + db.session.rollback() + print(f"Erreur lors de la mise à jour de la compétence: {e}") + return False + + def delete_competence(self, competence_id: int) -> bool: + """Supprime une compétence.""" + try: + competence = Competence.query.get(competence_id) + if competence: + db.session.delete(competence) + db.session.commit() + return True + return False + except Exception as e: + db.session.rollback() + print(f"Erreur lors de la suppression de la compétence: {e}") + return False + + # Méthodes spécifiques pour la gestion de l'échelle + + def add_scale_value(self, value: str, label: str, color: str, included_in_total: bool = True) -> bool: + """Ajoute une nouvelle valeur à l'échelle.""" + try: + scale_value = CompetenceScaleValue( + value=value, + label=label, + color=color, + included_in_total=included_in_total + ) + db.session.add(scale_value) + db.session.commit() + return True + except Exception as e: + db.session.rollback() + print(f"Erreur lors de l'ajout de la valeur d'échelle: {e}") + return False + + def update_scale_value(self, value: str, label: str, color: str, included_in_total: bool) -> bool: + """Met à jour une valeur de l'échelle.""" + try: + scale_value = CompetenceScaleValue.query.get(value) + if scale_value: + scale_value.label = label + scale_value.color = color + scale_value.included_in_total = included_in_total + db.session.commit() + return True + return False + except Exception as e: + db.session.rollback() + print(f"Erreur lors de la mise à jour de la valeur d'échelle: {e}") + return False + + def delete_scale_value(self, value: str) -> bool: + """Supprime une valeur de l'échelle.""" + try: + scale_value = CompetenceScaleValue.query.get(value) + if scale_value: + db.session.delete(scale_value) + db.session.commit() + return True + return False + except Exception as e: + db.session.rollback() + print(f"Erreur lors de la suppression de la valeur d'échelle: {e}") + return False + + +# Instance globale de configuration +config_manager = ConfigManager() \ No newline at end of file diff --git a/models.py b/models.py index f2e70bc..12a5fec 100644 --- a/models.py +++ b/models.py @@ -246,4 +246,48 @@ class Grade(db.Model): def __repr__(self): - return f'' \ No newline at end of file + return f'' + +# Configuration tables + +class AppConfig(db.Model): + """Configuration simple de l'application (clé-valeur).""" + __tablename__ = 'app_config' + + key = db.Column(db.String(100), primary_key=True) + value = db.Column(db.Text, nullable=False) + description = db.Column(db.Text) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + def __repr__(self): + return f'' + +class CompetenceScaleValue(db.Model): + """Valeurs de l'échelle des compétences (0, 1, 2, 3, ., d, etc.).""" + __tablename__ = 'competence_scale_values' + + value = db.Column(db.String(10), primary_key=True) # '0', '1', '2', '3', '.', 'd', etc. + label = db.Column(db.String(100), nullable=False) + color = db.Column(db.String(7), nullable=False) # Format #RRGGBB + included_in_total = db.Column(db.Boolean, default=True, nullable=False) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + def __repr__(self): + return f'' + +class Competence(db.Model): + """Liste des compétences (Calculer, Raisonner, etc.).""" + __tablename__ = 'competences' + + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(100), unique=True, nullable=False) + color = db.Column(db.String(7), nullable=False) # Format #RRGGBB + icon = db.Column(db.String(50), nullable=False) + order_index = db.Column(db.Integer, default=0) # Pour l'ordre d'affichage + created_at = db.Column(db.DateTime, default=datetime.utcnow) + updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + def __repr__(self): + return f'' \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 80ef529..8d82f06 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ description = "Application web de gestion scolaire" requires-python = ">=3.9" dependencies = [ "Flask>=2.3.3", - "Flask-SQLAlchemy>=3.0.5", + "Flask-SQLAlchemy>=3.0.5", "Flask-WTF>=1.1.1", "WTForms>=3.0.1", ] @@ -22,4 +22,4 @@ dev-dependencies = [ "pytest>=7.4.0", "pytest-flask>=1.2.0", "pytest-cov>=4.1.0", -] \ No newline at end of file +] diff --git a/routes/config.py b/routes/config.py new file mode 100644 index 0000000..985d1ed --- /dev/null +++ b/routes/config.py @@ -0,0 +1,404 @@ +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 +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']) +def add_competence(): + """Ajouter une nouvelle compétence.""" + try: + 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') + + except Exception as e: + logging.error(f"Erreur ajout compétence: {e}") + flash('Erreur lors de l\'ajout de la compétence', 'error') + + return redirect(url_for('config.competences')) + +@bp.route('/competences/update', methods=['POST']) +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']) +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 des compétences.""" + 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 des compétences.""" + 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 des compétences 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/', 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']) +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')) \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index 387faf4..ff55181 100644 --- a/templates/base.html +++ b/templates/base.html @@ -36,6 +36,12 @@ Classes Élèves Évaluations + + + Configuration + diff --git a/templates/config/competences.html b/templates/config/competences.html new file mode 100644 index 0000000..f5ffc80 --- /dev/null +++ b/templates/config/competences.html @@ -0,0 +1,297 @@ +{% extends "base.html" %} + +{% block title %}Configuration des compétences - Notytex{% endblock %} + +{% block content %} +
+ + + + +
+
+
+

⭐ Gestion des compétences

+

Configurez les compétences avec leurs couleurs et icônes

+
+ +
+
+ +
+ +
+
+
+

📋 Compétences configurées

+

{{ competences|length }} compétence(s) définie(s)

+
+ +
+ {% if competences %} +
+ {% for competence in competences %} +
+
+ +
+ + +
+ {% if competence.icon == 'calculator' %} + + + + {% elif competence.icon == 'lightbulb' %} + + + + {% elif competence.icon == 'chat-bubble-left-right' %} + + + + {% elif competence.icon == 'cube' %} + + + + {% elif competence.icon == 'chart-bar' %} + + + + {% elif competence.icon == 'magnifying-glass' %} + + + + {% else %} + + + + {% endif %} +
+ + +
+

{{ competence.name }}

+

Icône: {{ competence.icon }}

+
+
+ + +
+ + +
+ +
+
+
+ {% endfor %} +
+ {% else %} +
+ + + +

Aucune compétence définie

+

Commencez par ajouter votre première compétence

+
+ {% endif %} +
+
+
+ + +
+
+
+

➕ Ajouter une compétence

+
+ +
+ + +
+ + +
+ +
+ +
+ + +
+
+ +
+ + +
+ +
+ + + +
+
+
+
+
+ + +
+ + + + + Retour à la configuration + + +
+ {{ competences|length }} compétence(s) configurée(s) +
+
+
+ + +{% endblock %} \ No newline at end of file diff --git a/templates/config/general.html b/templates/config/general.html new file mode 100644 index 0000000..f7c6090 --- /dev/null +++ b/templates/config/general.html @@ -0,0 +1,169 @@ +{% extends "base.html" %} + +{% block title %}Configuration générale - Notytex{% endblock %} + +{% block content %} +
+ + + + +
+
+
+

🏫 Configuration générale

+

Paramètres système et contexte de l'application

+
+ +
+
+ + +
+
+ + +
+

📅 Contexte scolaire

+ +
+
+ + +

Format attendu : YYYY-YYYY

+
+
+
+ + + +
+

🎯 Configuration des évaluations

+ +
+
+ + +

+ Système utilisé par défaut lors de la création d'exercices +

+
+
+
+ + +
+ + + + + Retour + + +
+ + + +
+
+
+
+ + +
+

ℹ️ Informations système

+
+
+ Version : + Notytex 1.0 +
+
+ Type de base : + SQLite +
+
+ Configuration : + Base de données SQLite +
+
+
+
+ + +{% endblock %} \ No newline at end of file diff --git a/templates/config/index.html b/templates/config/index.html new file mode 100644 index 0000000..78c887a --- /dev/null +++ b/templates/config/index.html @@ -0,0 +1,157 @@ +{% extends "base.html" %} + +{% block title %}Configuration - Notytex{% endblock %} + +{% block content %} +
+ +
+
+
+

⚙️ Configuration

+

Personnalisez votre application Notytex

+
+ +
+
+ + +
+ + +
+
+
+ + + +
+

Configuration générale

+
+

Base de données, année scolaire et paramètres système

+
+
+ Année scolaire : + {{ app_config.get_school_year() }} +
+
+ Base de données : + SQLite +
+
+ + Modifier + +
+ + +
+
+
+ + + +
+

Compétences

+
+

Gérez la liste des compétences et leurs couleurs

+
+
+
+ Calculer +
+
+
+ Raisonner +
+
+4 autres compétences
+
+ + Gérer les compétences + +
+ + +
+
+
+ + + +
+

Échelle des compétences

+
+

Personnalisez les valeurs et libellés de l'échelle

+
+
+ 0 - Non acquis +
+
+
+ 3 - Expert +
+
+
5 valeurs configurées
+
+ + Configurer l'échelle + +
+
+ + +
+

Actions système

+
+ + + +
+
+
+ + + +{% endblock %} \ No newline at end of file diff --git a/templates/config/scale.html b/templates/config/scale.html new file mode 100644 index 0000000..beed54b --- /dev/null +++ b/templates/config/scale.html @@ -0,0 +1,464 @@ +{% extends "base.html" %} + +{% block title %}Configuration de l'échelle - Notytex{% endblock %} + +{% block content %} +
+ + + + +
+
+
+

📏 Échelle des compétences

+

Configurez l'échelle numérique et les valeurs spéciales

+
+ +
+
+ + +
+
+ + + +
+

Organisation de l'échelle

+
    +
  • Échelle numérique : Valeurs de 0 à N avec progression continue
  • +
  • Valeurs spéciales : "." pour non évalué et autres valeurs personnalisées
  • +
  • Réglage de l'étendue : Vous pouvez étendre l'échelle jusqu'à 10
  • +
+
+
+
+ + +
+
+

🎚️ Configuration de l'échelle

+

{{ competence_scale|length }} valeur(s) configurée(s)

+
+ +
+ + +
+
+

🔢 Échelle numérique

+ + +
+ + +
+
+ +
+

+ Configurez chaque niveau de l'échelle numérique. L'étendue actuelle va de 0 à {{ current_max }}. +

+
+ +
+ {% for value, config in competence_scale.items() %} + {% if value in [0, 1, 2, 3, 4, 5] %} +
+
+
+
+ {{ value }} +
+
+ {% if value == '0' %} + Niveau {{ value }} (minimum) + {% else %} + Niveau {{ value }} + {% endif %} +
+
+ + + {% if config.included_in_total %}✅ Inclus{% else %}❌ Exclu{% endif %} + +
+ +
+
+ + +
+ +
+ +
+ + +
+
+ +
+ + +
+
+
+ {% endif %} + {% endfor %} +
+
+ + +
+
+

🔤 Valeurs spéciales

+ +
+ +
+ {% for value, config in competence_scale.items() %} + {% if value not in [0, 1, 2, 3, 4, 5] %} +
+
+
+
+ {% if value == '.' %} + • + {% else %} + {{ value }} + {% endif %} +
+
+ {% if value == '.' %} + Valeur "." (Non évalué) + {% else %} + Valeur "{{ value }}" + {% endif %} +
+
+ +
+ + {% if config.included_in_total %}✅ Inclus{% else %}❌ Exclu{% endif %} + + + {% if value != '.' %} + + {% endif %} +
+
+ +
+
+ + +
+ +
+ +
+ + +
+
+ +
+ + + {% if value == '.' %} +

+ Généralement inclus car compte dans le total possible +

+ {% endif %} +
+
+
+ {% endif %} + {% endfor %} +
+
+ + +
+ + + + + Retour + + +
+ + + +
+
+
+
+
+ + +{% endblock %} \ No newline at end of file diff --git a/utils.py b/utils.py index 41c9c77..0eb3595 100644 --- a/utils.py +++ b/utils.py @@ -91,4 +91,10 @@ def log_user_action(action, details=None): class ValidationError(Exception): """Exception personnalisée pour les erreurs de validation""" - pass \ No newline at end of file + pass + +def handle_error(exception, default_message="Une erreur s'est produite"): + """Gestionnaire d'erreur générique""" + current_app.logger.error(f"Erreur: {exception}") + flash(default_message, 'error') + return render_template('error.html', error=default_message), 500 \ No newline at end of file