Files
notytex/backend/domain/services/config_service.py
Bertrand Benjamin 2b08eb534a Migration v1 (Flask) -> v2 (FastAPI + Vue.js) complétée
 Changements majeurs:
- Suppression complète du code Flask legacy
- Migration backend FastAPI vers racine /backend
- Migration frontend Vue.js vers racine /frontend
- Suppression de notytex-v2/ (code monté à la racine)

 Validations:
- Backend démarre correctement (port 8000)
- API /api/v2/health répond healthy
- 99/99 tests unitaires passent
- Frontend configuré avec proxy Vite

📝 Documentation:
- README.md réécrit pour v2
- Instructions de démarrage mises à jour
- .gitignore adapté pour backend/frontend/

🎯 Architecture finale:
notytex/
├── backend/     # FastAPI + SQLAlchemy + Pydantic
├── frontend/    # Vue 3 + Vite + TailwindCSS
├── docs/        # Documentation
└── school_management.db  # Base de données (inchangée)

Jalon 6 complété: Application v2 prête pour utilisation!
2025-11-25 21:09:47 +01:00

275 lines
8.7 KiB
Python

"""
Service de configuration pour le domaine métier.
Fournit l'accès à la configuration de l'application
(valeurs spéciales, signification des scores, etc.)
sans dépendance directe à la base de données.
"""
from typing import Dict, Any, Optional, List
from dataclasses import dataclass
@dataclass
class SpecialValueConfig:
"""Configuration d'une valeur spéciale."""
label: str
description: str
color: str
counts: bool # Compte dans le total
value: Optional[float] # Valeur numérique (None = dispensé)
def to_dict(self) -> dict:
return {
"label": self.label,
"description": self.description,
"color": self.color,
"counts": self.counts,
"value": self.value
}
@dataclass
class ScoreMeaning:
"""Signification d'un score de compétence (0-3)."""
label: str
color: str
description: str = ""
def to_dict(self) -> dict:
return {
"label": self.label,
"color": self.color,
"description": self.description
}
@dataclass
class GradingTypeConfig:
"""Configuration d'un type de notation."""
label: str
description: str
input_type: str
max_value: Optional[int] = None
def to_dict(self) -> Dict[str, Any]:
result: Dict[str, Any] = {
"label": self.label,
"description": self.description,
"input_type": self.input_type
}
if self.max_value is not None:
result["max_value"] = self.max_value
return result
class ConfigService:
"""
Service de configuration pour le domaine métier.
Fournit des valeurs par défaut et peut être étendu pour
charger la configuration depuis une source externe (DB, fichier, etc.)
"""
# Configuration par défaut des valeurs spéciales
DEFAULT_SPECIAL_VALUES: Dict[str, SpecialValueConfig] = {
".": SpecialValueConfig(
label="Pas de réponse",
description="Aucune réponse fournie",
color="#6b7280",
counts=True,
value=0.0
),
"d": SpecialValueConfig(
label="Dispensé",
description="Élève dispensé de cet exercice",
color="#c0bfbc",
counts=False,
value=None
),
"a": SpecialValueConfig(
label="Absent",
description="Élève absent lors de l'évaluation",
color="#f87171",
counts=True,
value=0.0
)
}
@staticmethod
def _get_default_score_meanings() -> Dict[int, ScoreMeaning]:
"""Retourne les significations par défaut des scores."""
return {
0: ScoreMeaning(
label="Non acquis",
color="#ef4444",
description="Compétence non maîtrisée"
),
1: ScoreMeaning(
label="En cours d'acquisition",
color="#f6d32d",
description="Compétence en cours d'apprentissage"
),
2: ScoreMeaning(
label="Acquis",
color="#22c55e",
description="Compétence maîtrisée"
),
3: ScoreMeaning(
label="Expert",
color="#059669",
description="Compétence parfaitement maîtrisée"
)
}
# Types de notation
DEFAULT_GRADING_TYPES: Dict[str, GradingTypeConfig] = {
"notes": GradingTypeConfig(
label="Notes numériques",
description="Valeurs décimales (ex: 15.5/20)",
input_type="number"
),
"score": GradingTypeConfig(
label="Échelle de compétences (0-3)",
description="Échelle fixe : 0=Non acquis, 1=En cours, 2=Acquis, 3=Expert",
input_type="select",
max_value=3
)
}
def __init__(
self,
special_values: Optional[Dict[str, SpecialValueConfig]] = None,
score_meanings: Optional[Dict[int, ScoreMeaning]] = None,
grading_types: Optional[Dict[str, GradingTypeConfig]] = None
):
"""
Initialise le service avec une configuration optionnelle.
Args:
special_values: Valeurs spéciales personnalisées
score_meanings: Significations des scores personnalisées
grading_types: Types de notation personnalisés
"""
self._special_values = special_values or self.DEFAULT_SPECIAL_VALUES.copy()
self._score_meanings = score_meanings or self._get_default_score_meanings()
self._grading_types = grading_types or self.DEFAULT_GRADING_TYPES.copy()
def get_special_values(self) -> Dict[str, SpecialValueConfig]:
"""Retourne les valeurs spéciales configurées."""
return self._special_values
def get_special_values_dict(self) -> Dict[str, Dict[str, Any]]:
"""Retourne les valeurs spéciales en format dict."""
return {
key: config.to_dict()
for key, config in self._special_values.items()
}
def get_score_meanings(self) -> Dict[int, ScoreMeaning]:
"""Retourne les significations des scores."""
return self._score_meanings
def get_score_meanings_dict(self) -> Dict[str, Dict[str, str]]:
"""Retourne les significations en format dict (clés string pour JSON)."""
return {
str(score): meaning.to_dict()
for score, meaning in self._score_meanings.items()
}
def get_grading_types(self) -> Dict[str, GradingTypeConfig]:
"""Retourne les types de notation."""
return self._grading_types
def get_grading_types_dict(self) -> Dict[str, Dict[str, Any]]:
"""Retourne les types en format dict."""
return {
key: config.to_dict()
for key, config in self._grading_types.items()
}
def is_special_value(self, value: str) -> bool:
"""Vérifie si une valeur est spéciale."""
return value in self._special_values
def get_special_value_numeric(self, value: str) -> Optional[float]:
"""
Retourne la valeur numérique d'une valeur spéciale.
Args:
value: Valeur spéciale (., d, a)
Returns:
Valeur numérique ou None si dispensé ou non trouvée
"""
config = self._special_values.get(value)
if config:
return config.value
return None
def get_score_color(self, score: int) -> str:
"""
Retourne la couleur associée à un score de compétence.
Args:
score: Score (0-3)
Returns:
Code couleur hexadécimal
"""
meaning = self._score_meanings.get(score)
return meaning.color if meaning else "#374151"
def get_score_label(self, score: int) -> str:
"""
Retourne le label d'un score de compétence.
Args:
score: Score (0-3)
Returns:
Label du score
"""
meaning = self._score_meanings.get(score)
return meaning.label if meaning else str(score)
@classmethod
def from_database_config(cls, db_config: Dict[str, Any]) -> "ConfigService":
"""
Crée une instance à partir d'une configuration de base de données.
Args:
db_config: Configuration extraite de la DB
Returns:
Instance de ConfigService
"""
special_values = {}
score_meanings = {}
# Parser les valeurs spéciales
for key, config in db_config.get("special_values", {}).items():
special_values[key] = SpecialValueConfig(
label=config.get("label", key),
description=config.get("description", ""),
color=config.get("color", "#6b7280"),
counts=config.get("counts", True),
value=config.get("value")
)
# Parser les significations des scores
for score_str, meaning in db_config.get("score_meanings", {}).items():
try:
score = int(score_str)
score_meanings[score] = ScoreMeaning(
label=meaning.get("label", str(score)),
color=meaning.get("color", "#374151"),
description=meaning.get("description", "")
)
except (ValueError, TypeError):
continue
return cls(
special_values=special_values if special_values else None,
score_meanings=score_meanings if score_meanings else None
)