✨ 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!
279 lines
8.7 KiB
Python
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
|
|
}
|