✨ 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!
114 lines
3.1 KiB
Python
114 lines
3.1 KiB
Python
"""
|
|
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
|
|
)
|