from typing import Dict, Any, Optional, List 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' }, 'grading_system': { 'types': { 'notes': { 'label': 'Notes numériques', 'description': 'Valeurs décimales (ex: 15.5/20)', 'input_type': 'number', 'validation': 'decimal' }, 'score': { 'label': 'Échelle de compétences (0-3)', 'description': 'Échelle fixe : 0=Non acquis, 1=En cours, 2=Acquis, 3=Expert', 'max_value': 3, 'input_type': 'select' } }, 'special_values': { '.': { 'label': 'Pas de réponse', 'description': 'Aucune réponse fournie', 'color': '#6b7280', 'counts': True, 'value': 0 }, 'd': { 'label': 'Dispensé', 'description': 'Élève dispensé de cet exercice', 'color': '#c0bfbc', 'counts': False, 'value': None }, 'a': { 'label': 'Absent', 'description': 'Élève absent lors de l\'évaluation', 'color': '#f87171', 'counts': True, 'value': 0 } }, 'score_meanings': { 0: {'label': 'Non acquis', 'color': '#ef4444', 'description': 'Compétence non maîtrisée'}, 1: {'label': 'En cours d\'acquisition', 'color': '#f6d32d', 'description': 'Compétence en cours d\'apprentissage'}, 2: {'label': 'Acquis', 'color': '#22c55e', 'description': 'Compétence maîtrisée'}, 3: {'label': 'Expert', 'color': '#059669', 'description': 'Compétence parfaitement maîtrisée'} } }, 'evaluations': { 'default_grading_system': 'competences', 'competence_scale': { 'values': { '0': { 'label': 'Non acquis', 'color': '#ef4444', 'included_in_total': True }, '1': { 'label': 'En cours d\'acquisition', 'color': '#f6d32d', 'included_in_total': True }, '2': { 'label': 'Acquis', 'color': '#22c55e', 'included_in_total': True }, '3': { 'label': 'Expert', 'color': '#059669', 'included_in_total': True }, '.': { 'label': 'Pas de réponse', 'color': '#6b7280', 'included_in_total': True }, 'd': { 'label': 'Dispensé', 'color': '#c0bfbc', 'included_in_total': False }, 'a': { 'label': 'Absent', 'color': '#f87171', 'included_in_total': True } } }, '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' } ] }, 'domains': { 'default_domains': [ { 'name': 'Algèbre', 'color': '#3b82f6', 'description': 'Calculs algébriques, équations, expressions' }, { 'name': 'Géométrie', 'color': '#10b981', 'description': 'Figures, mesures, constructions géométriques' }, { 'name': 'Statistiques', 'color': '#f59e0b', 'description': 'Données, moyennes, graphiques statistiques' }, { 'name': 'Fonctions', 'color': '#8b5cf6', 'description': 'Fonctions, graphiques, tableaux de valeurs' }, { 'name': 'Problèmes', 'color': '#ef4444', 'description': 'Résolution de problèmes concrets' }, { 'name': 'Calcul mental', 'color': '#06b6d4', 'description': 'Calculs rapides, estimations' } ] } } 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) # Domaines par défaut from models import Domain if Domain.query.count() == 0: default_domains = self.default_config['domains']['default_domains'] for domain_data in default_domains: domain = Domain( name=domain_data['name'], color=domain_data['color'], description=domain_data.get('description', '') ) db.session.add(domain) 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() from flask import current_app current_app.logger.error(f"Erreur lors de la sauvegarde de la configuration: {e}", exc_info=True) 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() # Si aucune valeur n'existe, initialiser avec les valeurs par défaut if not scale_values: self.initialize_default_config() scale_values = CompetenceScaleValue.query.all() result = {} for scale_value in scale_values: # Garder toutes les clés comme des strings pour la cohérence result[scale_value.value] = { '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_domains_list(self) -> List[Dict[str, Any]]: """Récupère la liste des domaines configurés.""" from models import Domain domains = Domain.query.order_by(Domain.name).all() return [ { 'id': domain.id, 'name': domain.name, 'color': domain.color, 'description': domain.description or '' } for domain in domains ] def add_domain(self, name: str, color: str = '#6B7280', description: str = '') -> bool: """Ajoute un nouveau domaine.""" try: from models import Domain domain = Domain(name=name, color=color, description=description) db.session.add(domain) db.session.commit() return True except Exception as e: db.session.rollback() from flask import current_app current_app.logger.error(f"Erreur lors de l'ajout du domaine: {e}") return False def get_or_create_domain(self, name: str, color: str = '#6B7280') -> 'Domain': """Récupère un domaine existant ou le crée s'il n'existe pas.""" from models import Domain domain = Domain.query.filter_by(name=name).first() if not domain: domain = Domain(name=name, color=color) db.session.add(domain) db.session.commit() return domain 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') # === Nouveau système de notation unifié === def get_grading_types(self) -> Dict[str, Dict[str, Any]]: """Récupère les types de notation disponibles (notes, score).""" return self.default_config['grading_system']['types'] def get_special_values(self) -> Dict[str, Dict[str, Any]]: """Récupère les valeurs spéciales configurables (., d, a) depuis la base de données.""" # Récupérer les valeurs spéciales depuis la base de données special_values_keys = ['.', 'd', 'a'] scale_values = CompetenceScaleValue.query.filter( CompetenceScaleValue.value.in_(special_values_keys) ).all() # Si aucune valeur n'existe, initialiser avec les valeurs par défaut if not scale_values: self.initialize_default_config() scale_values = CompetenceScaleValue.query.filter( CompetenceScaleValue.value.in_(special_values_keys) ).all() # Construire le dict avec mapping included_in_total -> counts result = {} for scale_value in scale_values: # Récupérer les valeurs par défaut pour compléter les informations manquantes default_config = self.default_config['grading_system']['special_values'].get(scale_value.value, {}) result[scale_value.value] = { 'label': scale_value.label, 'color': scale_value.color, 'counts': scale_value.included_in_total, # Mapping: included_in_total -> counts 'description': default_config.get('description', scale_value.label), 'value': default_config.get('value', 0 if scale_value.included_in_total else None) } # Ajouter les valeurs manquantes depuis la config par défaut si elles n'existent pas en base default_special = self.default_config['grading_system']['special_values'] for key, default_value in default_special.items(): if key not in result: result[key] = default_value return result def get_score_meanings(self) -> Dict[int, Dict[str, str]]: """Récupère les significations des scores (0-3) depuis la base de données.""" # Récupérer les valeurs depuis la base de données scale_values = CompetenceScaleValue.query.filter( CompetenceScaleValue.value.in_(['0', '1', '2', '3']) ).all() # Convertir en dict avec clés entières meanings = {} for scale_value in scale_values: try: score_int = int(scale_value.value) meanings[score_int] = { 'label': scale_value.label, 'color': scale_value.color } except ValueError: continue # Ignorer les valeurs non numériques # Fallback vers la config par défaut si pas de données en base if not meanings: default_meanings = self.default_config['grading_system']['score_meanings'] for key, value in default_meanings.items(): meanings[int(key)] = value return meanings def validate_grade_value(self, value: str, grading_type: str, max_points: float = None) -> bool: """ Valide une valeur de note selon le type de notation. Args: value: Valeur à valider grading_type: Type de notation ('notes' ou 'score') max_points: Points maximum pour le type 'notes' (optionnel) Returns: True si la valeur est valide, False sinon """ # Valeurs vides sont acceptées if not value or value.strip() == '': return True value = value.strip() # Valeurs spéciales toujours valides if self.is_special_value(value): return True # Validation selon le type if grading_type == 'notes': try: # Normaliser virgule en point pour les décimaux français normalized_value = value.replace(',', '.') # Vérifier le format strict avec regex import re if not re.match(r'^[0-9]+([.,][0-9]+)?$', value): return False float_value = float(normalized_value) # Vérifier la plage if float_value < 0: return False # Si max_points est spécifié, vérifier la limite supérieure if max_points is not None and float_value > max_points: return False return True except (ValueError, TypeError): return False elif grading_type == 'score': try: int_value = int(value) return 0 <= int_value <= 3 except (ValueError, TypeError): return False return False def is_special_value(self, value: str) -> bool: """Vérifie si une valeur est une valeur spéciale.""" return value in self.get_special_values() def get_numeric_value(self, value: str, grading_type: str) -> Optional[float]: """ Convertit une valeur de note en valeur numérique. Args: value: Valeur à convertir grading_type: Type de notation Returns: Valeur numérique ou None pour les valeurs dispensées """ # Valeurs spéciales if self.is_special_value(value): special_config = self.get_special_values()[value] return special_config['value'] # Conversion selon le type try: if grading_type == 'notes': return float(value) elif grading_type == 'score': return float(value) except (ValueError, TypeError): pass return None def get_display_info(self, value: str, grading_type: str) -> Dict[str, Any]: """ Récupère les informations d'affichage pour une valeur. Returns: Dict avec 'color', 'label', 'description' """ # Récupérer les valeurs d'échelle de la base de données scale_values = self.get_competence_scale_values() # Si la valeur existe dans l'échelle, utiliser ses informations if value in scale_values: scale_config = scale_values[value] return { 'color': scale_config['color'], 'label': scale_config['label'], 'description': scale_config.get('description', scale_config['label']) } # Valeur par défaut pour notes numériques ou valeurs non configurées return { 'color': '#374151', # Gris neutre 'label': str(value), 'description': f'Note : {value}' } # 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() from flask import current_app current_app.logger.error(f"Erreur lors de l'ajout de la compétence: {e}", exc_info=True) 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() from flask import current_app current_app.logger.error(f"Erreur lors de la mise à jour de la compétence: {e}", exc_info=True) 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() from flask import current_app current_app.logger.error(f"Erreur lors de la suppression de la compétence: {e}", exc_info=True) 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() from flask import current_app current_app.logger.error(f"Erreur lors de l'ajout de la valeur d'échelle: {e}", exc_info=True) 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() from flask import current_app current_app.logger.error(f"Erreur lors de la mise à jour de la valeur d'échelle: {e}", exc_info=True) 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() from flask import current_app current_app.logger.error(f"Erreur lors de la suppression de la valeur d'échelle: {e}", exc_info=True) return False # Instance globale de configuration config_manager = ConfigManager()