Files
notytex/backend/domain/services/score_calculator.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

279 lines
8.7 KiB
Python

"""
Service de calcul des scores des élèves.
Calcule les scores pour chaque élève d'une évaluation,
par exercice et au total.
"""
from typing import Dict, List, Optional, Any
from dataclasses import dataclass
from domain.value_objects import StudentScore, ExerciseScore
from domain.services.grading_calculator import GradingCalculator
@dataclass
class GradeData:
"""Données d'une note pour le calcul."""
student_id: int
grading_element_id: int
value: Optional[str]
grading_type: str
max_points: float
exercise_id: int
exercise_title: str
@dataclass
class StudentData:
"""Données d'un élève pour le calcul."""
id: int
first_name: str
last_name: str
@property
def full_name(self) -> str:
"""Nom complet au format 'Nom Prénom'."""
return f"{self.last_name} {self.first_name}"
class StudentScoreCalculator:
"""
Calculateur de scores par élève.
Utilise le GradingCalculator pour calculer les scores individuels
et agrège les résultats par élève et par exercice.
"""
def __init__(self, grading_calculator: Optional[GradingCalculator] = None):
"""
Initialise le calculateur.
Args:
grading_calculator: Calculateur de notes (créé si non fourni)
"""
self.grading_calculator = grading_calculator or GradingCalculator()
def calculate_all_scores(
self,
students: List[StudentData],
grades: List[GradeData],
exercises: List[Dict[str, Any]]
) -> Dict[int, StudentScore]:
"""
Calcule les scores de tous les élèves pour une évaluation.
Args:
students: Liste des élèves
grades: Liste des notes
exercises: Liste des exercices avec leurs éléments
Returns:
Dictionnaire {student_id: StudentScore}
"""
# Indexer les notes par (student_id, element_id)
grades_index: Dict[tuple, GradeData] = {}
for grade in grades:
key = (grade.student_id, grade.grading_element_id)
grades_index[key] = grade
# Calculer pour chaque élève
result = {}
for student in students:
student_score = self._calculate_student_score(
student, grades_index, exercises
)
result[student.id] = student_score
return result
def _calculate_student_score(
self,
student: StudentData,
grades_index: Dict[tuple, GradeData],
exercises: List[Dict[str, Any]]
) -> StudentScore:
"""
Calcule le score d'un seul élève.
Args:
student: Données de l'élève
grades_index: Index des notes (student_id, element_id) -> GradeData
exercises: Liste des exercices
Returns:
StudentScore avec les scores par exercice
"""
total_score = 0.0
total_max_points = 0.0
exercise_scores = {}
for exercise in exercises:
exercise_id = exercise["id"]
exercise_title = exercise["title"]
elements = exercise.get("elements", [])
ex_score = 0.0
ex_max = 0.0
for element in elements:
element_id = element["id"]
grading_type = element["grading_type"]
max_points = element["max_points"]
# Récupérer la note
grade_data = grades_index.get((student.id, element_id))
if grade_data and grade_data.value:
value = grade_data.value.strip()
# Calculer le score
calculated = self.grading_calculator.calculate_score(
value, grading_type, max_points
)
# Vérifier si compte dans le total
if self.grading_calculator.is_counted_in_total(value):
if calculated is not None: # Pas dispensé
ex_score += calculated
ex_max += max_points
# Stocker le score de l'exercice
exercise_scores[exercise_id] = ExerciseScore(
exercise_id=exercise_id,
title=exercise_title,
score=ex_score,
max_points=ex_max
)
total_score += ex_score
total_max_points += ex_max
return StudentScore(
student_id=student.id,
student_name=student.full_name,
total_score=round(total_score, 2),
total_max_points=total_max_points,
exercise_scores=exercise_scores
)
def calculate_from_raw_data(
self,
assessment_data: Dict[str, Any],
students_data: List[Dict[str, Any]],
grades_data: List[Dict[str, Any]]
) -> Dict[int, StudentScore]:
"""
Calcule les scores à partir de données brutes (dicts).
Méthode utilitaire pour simplifier l'utilisation avec des données
provenant directement de la base de données.
Args:
assessment_data: Données de l'évaluation avec exercises et elements
students_data: Liste des élèves [{id, first_name, last_name}]
grades_data: Liste des notes [{student_id, element_id, value, ...}]
Returns:
Dictionnaire des scores par élève
"""
# Convertir les données
students = [
StudentData(
id=s["id"],
first_name=s["first_name"],
last_name=s["last_name"]
)
for s in students_data
]
# Préparer les exercices avec leurs éléments
exercises = []
element_info = {} # Cache des infos d'éléments
for ex in assessment_data.get("exercises", []):
elements = []
for elem in ex.get("grading_elements", []):
element_info[elem["id"]] = {
"grading_type": elem["grading_type"],
"max_points": elem["max_points"],
"exercise_id": ex["id"],
"exercise_title": ex["title"]
}
elements.append({
"id": elem["id"],
"grading_type": elem["grading_type"],
"max_points": elem["max_points"]
})
exercises.append({
"id": ex["id"],
"title": ex["title"],
"elements": elements
})
# Convertir les notes
grades = []
for g in grades_data:
elem_id = g["grading_element_id"]
info = element_info.get(elem_id, {})
grades.append(GradeData(
student_id=g["student_id"],
grading_element_id=elem_id,
value=g.get("value"),
grading_type=info.get("grading_type", "notes"),
max_points=info.get("max_points", 0),
exercise_id=info.get("exercise_id", 0),
exercise_title=info.get("exercise_title", "")
))
return self.calculate_all_scores(students, grades, exercises)
class ProgressCalculator:
"""
Calculateur de progression de correction.
Calcule le pourcentage de notes saisies pour une évaluation.
"""
@staticmethod
def calculate_progress(
grades_count: int,
total_elements: int,
students_count: int
) -> Dict[str, Any]:
"""
Calcule la progression de correction.
Args:
grades_count: Nombre de notes saisies
total_elements: Nombre d'éléments de notation
students_count: Nombre d'élèves
Returns:
Dict avec percentage, completed, total, status, students_count
"""
total = total_elements * students_count
completed = grades_count
if total == 0:
percentage = 0
status = "not_started"
else:
percentage = round((completed / total) * 100)
if percentage == 0:
status = "not_started"
elif percentage == 100:
status = "completed"
else:
status = "in_progress"
return {
"percentage": percentage,
"completed": completed,
"total": total,
"status": status,
"students_count": students_count
}