""" Value Objects pour les scores et notes. """ from dataclasses import dataclass, field from typing import Dict, Optional, Any @dataclass(frozen=True) class ExerciseScore: """ Score d'un élève pour un exercice. Attributes: exercise_id: ID de l'exercice title: Titre de l'exercice score: Score obtenu max_points: Points maximum possibles """ exercise_id: int title: str score: float max_points: float @property def percentage(self) -> float: """Calcule le pourcentage de réussite.""" if self.max_points == 0: return 0.0 return round((self.score / self.max_points) * 100, 1) def to_dict(self) -> dict: """Convertit en dictionnaire pour la sérialisation.""" return { "score": round(self.score, 2), "max": self.max_points } @dataclass class StudentScore: """ Score complet d'un élève pour une évaluation. Attributes: student_id: ID de l'élève student_name: Nom complet de l'élève total_score: Score total obtenu total_max_points: Points maximum totaux exercise_scores: Scores par exercice """ student_id: int student_name: str total_score: float total_max_points: float exercise_scores: Dict[int, ExerciseScore] = field(default_factory=dict) @property def percentage(self) -> float: """Calcule le pourcentage global de réussite.""" if self.total_max_points == 0: return 0.0 return round((self.total_score / self.total_max_points) * 100, 1) def to_dict(self) -> dict: """Convertit en dictionnaire pour la sérialisation.""" return { "student_id": self.student_id, "student_name": self.student_name, "total_score": round(self.total_score, 2), "total_max_points": self.total_max_points, "percentage": self.percentage, "exercise_scores": { ex_id: ex_score.to_dict() for ex_id, ex_score in self.exercise_scores.items() } } @dataclass(frozen=True) class GradeValue: """ Représentation d'une valeur de note avec ses métadonnées. Attributes: raw_value: Valeur brute saisie numeric_value: Valeur numérique calculée (None si dispensé) is_special: True si valeur spéciale (., d, a) counts_in_total: True si compte dans le total """ raw_value: str numeric_value: Optional[float] is_special: bool counts_in_total: bool @classmethod def empty(cls) -> "GradeValue": """Crée une valeur vide (pas de note saisie).""" return cls( raw_value="", numeric_value=None, is_special=False, counts_in_total=False ) @classmethod def dispensed(cls, raw_value: str = "d") -> "GradeValue": """Crée une valeur pour un élève dispensé.""" return cls( raw_value=raw_value, numeric_value=None, is_special=True, counts_in_total=False )