✨ 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!
305 lines
9.0 KiB
Python
305 lines
9.0 KiB
Python
"""
|
|
Service de calcul des scores de notation.
|
|
|
|
Implémente le Strategy Pattern pour gérer les différents types de notation
|
|
(notes numériques vs compétences 0-3).
|
|
"""
|
|
from abc import ABC, abstractmethod
|
|
from typing import Optional, Dict, Any
|
|
|
|
from domain.value_objects import GradeValue
|
|
|
|
|
|
# =================== STRATEGY PATTERN ===================
|
|
|
|
class GradingStrategy(ABC):
|
|
"""Interface Strategy pour les différents types de notation."""
|
|
|
|
@abstractmethod
|
|
def calculate_score(self, grade_value: str, max_points: float) -> Optional[float]:
|
|
"""
|
|
Calcule le score selon le type de notation.
|
|
|
|
Args:
|
|
grade_value: Valeur de la note (string)
|
|
max_points: Points maximum pour cet élément
|
|
|
|
Returns:
|
|
Score calculé ou None si invalide
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def get_grading_type(self) -> str:
|
|
"""Retourne l'identifiant du type de notation."""
|
|
pass
|
|
|
|
|
|
class NotesStrategy(GradingStrategy):
|
|
"""
|
|
Strategy pour la notation en points (notes numériques).
|
|
|
|
Les valeurs sont utilisées directement comme points.
|
|
Ex: "15.5" sur 20 → 15.5 points
|
|
"""
|
|
|
|
def calculate_score(self, grade_value: str, max_points: float) -> Optional[float]:
|
|
try:
|
|
# Normaliser la virgule en point pour les décimaux français
|
|
normalized = grade_value.replace(',', '.')
|
|
return float(normalized)
|
|
except (ValueError, TypeError):
|
|
return 0.0
|
|
|
|
def get_grading_type(self) -> str:
|
|
return "notes"
|
|
|
|
|
|
class ScoreStrategy(GradingStrategy):
|
|
"""
|
|
Strategy pour la notation par compétences (score 0-3).
|
|
|
|
Les valeurs sont converties en points proportionnellement.
|
|
Ex: score 2/3 avec max_points=6 → (2/3) * 6 = 4 points
|
|
"""
|
|
|
|
def calculate_score(self, grade_value: str, max_points: float) -> Optional[float]:
|
|
try:
|
|
score_int = int(grade_value)
|
|
if 0 <= score_int <= 3:
|
|
# Formule: (score / 3) * max_points
|
|
return (score_int / 3) * max_points
|
|
return 0.0
|
|
except (ValueError, TypeError):
|
|
return 0.0
|
|
|
|
def get_grading_type(self) -> str:
|
|
return "score"
|
|
|
|
|
|
class GradingStrategyFactory:
|
|
"""Factory pour créer les strategies de notation."""
|
|
|
|
_strategies: Dict[str, type] = {
|
|
"notes": NotesStrategy,
|
|
"score": ScoreStrategy
|
|
}
|
|
|
|
@classmethod
|
|
def create(cls, grading_type: str) -> GradingStrategy:
|
|
"""
|
|
Crée une strategy selon le type.
|
|
|
|
Args:
|
|
grading_type: Type de notation ("notes" ou "score")
|
|
|
|
Returns:
|
|
Instance de la strategy correspondante
|
|
|
|
Raises:
|
|
ValueError: Si le type n'est pas supporté
|
|
"""
|
|
strategy_class = cls._strategies.get(grading_type)
|
|
if not strategy_class:
|
|
raise ValueError(f"Type de notation non supporté: {grading_type}")
|
|
return strategy_class()
|
|
|
|
@classmethod
|
|
def register_strategy(cls, grading_type: str, strategy_class: type) -> None:
|
|
"""
|
|
Enregistre un nouveau type de notation.
|
|
|
|
Permet d'étendre le système avec de nouveaux types de notation
|
|
sans modifier le code existant (Open/Closed Principle).
|
|
"""
|
|
cls._strategies[grading_type] = strategy_class
|
|
|
|
|
|
# =================== SERVICE PRINCIPAL ===================
|
|
|
|
class GradingCalculator:
|
|
"""
|
|
Calculateur de scores unifié.
|
|
|
|
Utilise le Strategy Pattern pour gérer les différents types de notation
|
|
et gère les valeurs spéciales de manière configurable.
|
|
"""
|
|
|
|
# Configuration par défaut des valeurs spéciales
|
|
DEFAULT_SPECIAL_VALUES: Dict[str, Dict[str, Any]] = {
|
|
".": {
|
|
"label": "Pas de réponse",
|
|
"value": 0,
|
|
"counts": True
|
|
},
|
|
"d": {
|
|
"label": "Dispensé",
|
|
"value": None,
|
|
"counts": False
|
|
},
|
|
"a": {
|
|
"label": "Absent",
|
|
"value": 0,
|
|
"counts": True
|
|
}
|
|
}
|
|
|
|
def __init__(self, special_values: Optional[Dict[str, Dict[str, Any]]] = None):
|
|
"""
|
|
Initialise le calculateur.
|
|
|
|
Args:
|
|
special_values: Configuration des valeurs spéciales (optionnel)
|
|
Si None, utilise la configuration par défaut
|
|
"""
|
|
self.special_values = special_values or self.DEFAULT_SPECIAL_VALUES
|
|
|
|
def calculate_score(
|
|
self,
|
|
grade_value: str,
|
|
grading_type: str,
|
|
max_points: float
|
|
) -> Optional[float]:
|
|
"""
|
|
Point d'entrée unifié pour tous les calculs de score.
|
|
|
|
Args:
|
|
grade_value: Valeur de la note (string)
|
|
grading_type: Type de notation ("notes" ou "score")
|
|
max_points: Points maximum pour cet élément
|
|
|
|
Returns:
|
|
Score calculé, 0 pour valeurs spéciales comptées, None pour dispensé
|
|
"""
|
|
# Normaliser la valeur
|
|
value = grade_value.strip() if grade_value else ""
|
|
|
|
# Valeurs vides
|
|
if not value:
|
|
return None
|
|
|
|
# Valeurs spéciales en premier
|
|
if self.is_special_value(value):
|
|
special_config = self.special_values[value]
|
|
special_result = special_config.get("value")
|
|
if special_result is None: # Dispensé
|
|
return None
|
|
return float(special_result) # 0 pour '.', 'a'
|
|
|
|
# Utilisation du Strategy Pattern pour le calcul normal
|
|
strategy = GradingStrategyFactory.create(grading_type)
|
|
return strategy.calculate_score(value, max_points)
|
|
|
|
def is_special_value(self, value: str) -> bool:
|
|
"""
|
|
Vérifie si une valeur est une valeur spéciale.
|
|
|
|
Args:
|
|
value: Valeur à vérifier
|
|
|
|
Returns:
|
|
True si la valeur est spéciale (., d, a)
|
|
"""
|
|
return value in self.special_values
|
|
|
|
def is_counted_in_total(self, grade_value: str) -> bool:
|
|
"""
|
|
Détermine si une note doit être comptée dans le total.
|
|
|
|
Args:
|
|
grade_value: Valeur de la note
|
|
|
|
Returns:
|
|
True si la note compte dans le total
|
|
"""
|
|
value = grade_value.strip() if grade_value else ""
|
|
|
|
if not value:
|
|
return False
|
|
|
|
if self.is_special_value(value):
|
|
return self.special_values[value].get("counts", True)
|
|
|
|
return True
|
|
|
|
def parse_grade(
|
|
self,
|
|
grade_value: str,
|
|
grading_type: str,
|
|
max_points: float
|
|
) -> GradeValue:
|
|
"""
|
|
Parse une valeur de note en GradeValue avec toutes ses métadonnées.
|
|
|
|
Args:
|
|
grade_value: Valeur brute de la note
|
|
grading_type: Type de notation
|
|
max_points: Points maximum
|
|
|
|
Returns:
|
|
GradeValue avec toutes les informations
|
|
"""
|
|
value = grade_value.strip() if grade_value else ""
|
|
|
|
if not value:
|
|
return GradeValue.empty()
|
|
|
|
is_special = self.is_special_value(value)
|
|
counts = self.is_counted_in_total(value)
|
|
numeric = self.calculate_score(value, grading_type, max_points)
|
|
|
|
return GradeValue(
|
|
raw_value=value,
|
|
numeric_value=numeric,
|
|
is_special=is_special,
|
|
counts_in_total=counts
|
|
)
|
|
|
|
def validate_grade_value(
|
|
self,
|
|
value: str,
|
|
grading_type: str,
|
|
max_points: Optional[float] = None
|
|
) -> bool:
|
|
"""
|
|
Valide une valeur de note selon le type de notation.
|
|
|
|
Args:
|
|
value: Valeur à valider
|
|
grading_type: Type de notation
|
|
max_points: Points maximum (optionnel, pour vérifier la limite)
|
|
|
|
Returns:
|
|
True si la valeur est valide
|
|
"""
|
|
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:
|
|
normalized = value.replace(",", ".")
|
|
float_value = float(normalized)
|
|
if float_value < 0:
|
|
return False
|
|
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
|