✨ 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!
275 lines
8.7 KiB
Python
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
|
|
)
|