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!
This commit is contained in:
278
backend/domain/services/score_calculator.py
Normal file
278
backend/domain/services/score_calculator.py
Normal file
@@ -0,0 +1,278 @@
|
||||
"""
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user