347 lines
14 KiB
Python
347 lines
14 KiB
Python
"""
|
|
Services pour la préparation du conseil de classe.
|
|
Comprend CouncilPreparationService, StudentEvaluationService, AppreciationService.
|
|
"""
|
|
from dataclasses import dataclass
|
|
from typing import Dict, List, Optional, Tuple
|
|
from datetime import datetime
|
|
from repositories.appreciation_repository import AppreciationRepository
|
|
from repositories.grade_repository import GradeRepository
|
|
from repositories.assessment_repository import AssessmentRepository
|
|
from repositories.student_repository import StudentRepository
|
|
from models import Student, Assessment, CouncilAppreciation, GradingCalculator
|
|
|
|
|
|
@dataclass
|
|
class StudentTrimesterSummary:
|
|
"""Résumé d'un élève pour un trimestre."""
|
|
student: Student
|
|
overall_average: Optional[float]
|
|
assessment_count: int
|
|
grades_by_assessment: Dict[int, Dict] # assessment_id -> {'score': float, 'max': float, 'title': str}
|
|
appreciation: Optional[CouncilAppreciation]
|
|
performance_status: str # 'excellent', 'good', 'average', 'struggling'
|
|
|
|
@property
|
|
def has_appreciation(self) -> bool:
|
|
"""Vérifie si l'élève a une appréciation avec contenu."""
|
|
return self.appreciation and self.appreciation.has_content
|
|
|
|
|
|
@dataclass
|
|
class CouncilPreparationData:
|
|
"""Données complètes pour la préparation du conseil de classe."""
|
|
class_group_id: int
|
|
trimester: int
|
|
student_summaries: List[StudentTrimesterSummary]
|
|
class_statistics: Dict
|
|
appreciation_stats: Dict
|
|
total_students: int
|
|
completed_appreciations: int
|
|
|
|
|
|
class StudentEvaluationService:
|
|
"""Service spécialisé dans l'évaluation des performances étudiantes."""
|
|
|
|
def __init__(self, grade_repo: GradeRepository, assessment_repo: AssessmentRepository):
|
|
self.grade_repo = grade_repo
|
|
self.assessment_repo = assessment_repo
|
|
|
|
def calculate_student_trimester_average(self, student_id: int, trimester: int) -> Optional[float]:
|
|
"""Calcule la moyenne d'un élève pour un trimestre donné."""
|
|
assessments = self.assessment_repo.find_completed_by_class_trimester(
|
|
# On récupère d'abord la classe de l'élève
|
|
Student.query.get(student_id).class_group_id,
|
|
trimester
|
|
)
|
|
|
|
if not assessments:
|
|
return None
|
|
|
|
weighted_sum = 0.0
|
|
total_coefficient = 0.0
|
|
|
|
for assessment in assessments:
|
|
student_score = self._calculate_assessment_score_for_student(assessment, student_id)
|
|
if student_score is not None:
|
|
weighted_sum += student_score * assessment.coefficient
|
|
total_coefficient += assessment.coefficient
|
|
|
|
return round(weighted_sum / total_coefficient, 2) if total_coefficient > 0 else None
|
|
|
|
def get_student_trimester_summary(self, student_id: int, trimester: int) -> StudentTrimesterSummary:
|
|
"""Génère le résumé d'un élève pour un trimestre."""
|
|
student = Student.query.get(student_id)
|
|
|
|
# Récupérer les évaluations du trimestre
|
|
assessments = self.assessment_repo.find_by_class_trimester_with_details(
|
|
student.class_group_id, trimester
|
|
)
|
|
|
|
# Calculer les scores par évaluation
|
|
grades_by_assessment = {}
|
|
for assessment in assessments:
|
|
score_data = self._get_student_assessment_data(student_id, assessment)
|
|
if score_data:
|
|
grades_by_assessment[assessment.id] = score_data
|
|
|
|
# Calculer la moyenne générale
|
|
overall_average = self.calculate_student_trimester_average(student_id, trimester)
|
|
|
|
# Déterminer le statut de performance
|
|
performance_status = self._determine_performance_status(overall_average, grades_by_assessment)
|
|
|
|
# Récupérer l'appréciation existante
|
|
appreciation_repo = AppreciationRepository()
|
|
appreciation = appreciation_repo.find_by_student_trimester(
|
|
student_id, student.class_group_id, trimester
|
|
)
|
|
|
|
return StudentTrimesterSummary(
|
|
student=student,
|
|
overall_average=overall_average,
|
|
assessment_count=len([a for a in assessments if self._has_grades(student_id, a)]),
|
|
grades_by_assessment=grades_by_assessment,
|
|
appreciation=appreciation,
|
|
performance_status=performance_status
|
|
)
|
|
|
|
def get_students_summaries(self, class_group_id: int, trimester: int) -> List[StudentTrimesterSummary]:
|
|
"""Génère les résumés de tous les élèves d'une classe pour un trimestre."""
|
|
student_repo = StudentRepository()
|
|
students = student_repo.find_by_class_group(class_group_id)
|
|
|
|
summaries = []
|
|
for student in students:
|
|
summary = self.get_student_trimester_summary(student.id, trimester)
|
|
summaries.append(summary)
|
|
|
|
# Trier par nom de famille puis prénom
|
|
summaries.sort(key=lambda s: (s.student.last_name, s.student.first_name))
|
|
|
|
return summaries
|
|
|
|
def _calculate_assessment_score_for_student(self, assessment: Assessment, student_id: int) -> Optional[float]:
|
|
"""Calcule le score d'un élève pour une évaluation."""
|
|
grades = self.grade_repo.get_student_grades_by_assessment(student_id, assessment.id)
|
|
|
|
if not grades:
|
|
return None
|
|
|
|
total_score = 0.0
|
|
total_max_points = 0.0
|
|
|
|
for grade in grades:
|
|
element = grade.grading_element
|
|
if grade.value:
|
|
score = GradingCalculator.calculate_score(
|
|
grade.value, element.grading_type, element.max_points
|
|
)
|
|
if score is not None and GradingCalculator.is_counted_in_total(grade.value, element.grading_type):
|
|
total_score += score
|
|
total_max_points += element.max_points
|
|
|
|
return round(total_score / total_max_points * 20, 2) if total_max_points > 0 else None
|
|
|
|
def _get_student_assessment_data(self, student_id: int, assessment: Assessment) -> Optional[Dict]:
|
|
"""Récupère les données d'évaluation d'un élève pour une évaluation."""
|
|
score = self._calculate_assessment_score_for_student(assessment, student_id)
|
|
if score is None:
|
|
return None
|
|
|
|
return {
|
|
'score': score,
|
|
'max': 20.0, # Score ramené sur 20
|
|
'title': assessment.title,
|
|
'date': assessment.date,
|
|
'coefficient': assessment.coefficient
|
|
}
|
|
|
|
def _has_grades(self, student_id: int, assessment: Assessment) -> bool:
|
|
"""Vérifie si un élève a des notes pour une évaluation."""
|
|
grades = self.grade_repo.get_student_grades_by_assessment(student_id, assessment.id)
|
|
return any(grade.value for grade in grades)
|
|
|
|
def _determine_performance_status(self, average: Optional[float], grades_by_assessment: Dict) -> str:
|
|
"""Détermine le statut de performance d'un élève."""
|
|
if not average:
|
|
return 'no_data'
|
|
|
|
if average >= 16:
|
|
return 'excellent'
|
|
elif average >= 14:
|
|
return 'good'
|
|
elif average >= 10:
|
|
return 'average'
|
|
else:
|
|
return 'struggling'
|
|
|
|
|
|
class AppreciationService:
|
|
"""Service pour la gestion des appréciations du conseil de classe."""
|
|
|
|
def __init__(self, appreciation_repo: AppreciationRepository):
|
|
self.appreciation_repo = appreciation_repo
|
|
|
|
def save_appreciation(self, data: Dict) -> CouncilAppreciation:
|
|
"""Sauvegarde ou met à jour une appréciation."""
|
|
return self.appreciation_repo.create_or_update(
|
|
student_id=data['student_id'],
|
|
class_group_id=data['class_group_id'],
|
|
trimester=data['trimester'],
|
|
data={
|
|
'general_appreciation': data.get('general_appreciation'),
|
|
'strengths': data.get('strengths'),
|
|
'areas_for_improvement': data.get('areas_for_improvement'),
|
|
'status': data.get('status', 'draft')
|
|
}
|
|
)
|
|
|
|
def auto_save_appreciation(self, data: Dict) -> CouncilAppreciation:
|
|
"""Sauvegarde automatique en mode brouillon."""
|
|
data['status'] = 'draft'
|
|
return self.save_appreciation(data)
|
|
|
|
def finalize_appreciation(self, student_id: int, class_group_id: int, trimester: int) -> CouncilAppreciation:
|
|
"""Finalise une appréciation (change le statut à 'finalized')."""
|
|
appreciation = self.appreciation_repo.find_by_student_trimester(
|
|
student_id, class_group_id, trimester
|
|
)
|
|
|
|
if not appreciation:
|
|
raise ValueError("Aucune appréciation trouvée pour finalisation")
|
|
|
|
appreciation.status = 'finalized'
|
|
return self.appreciation_repo.update(appreciation)
|
|
|
|
def get_class_appreciations(self, class_group_id: int, trimester: int) -> List[CouncilAppreciation]:
|
|
"""Récupère toutes les appréciations d'une classe pour un trimestre."""
|
|
return self.appreciation_repo.find_by_class_trimester(class_group_id, trimester)
|
|
|
|
def get_completion_stats(self, class_group_id: int, trimester: int) -> Dict:
|
|
"""Statistiques de completion des appréciations."""
|
|
return self.appreciation_repo.get_completion_stats(class_group_id, trimester)
|
|
|
|
|
|
class CouncilPreparationService:
|
|
"""Service principal pour la préparation du conseil de classe."""
|
|
|
|
def __init__(self,
|
|
student_evaluation_service: StudentEvaluationService,
|
|
appreciation_service: AppreciationService,
|
|
assessment_repo: AssessmentRepository):
|
|
self.student_evaluation = student_evaluation_service
|
|
self.appreciation = appreciation_service
|
|
self.assessment_repo = assessment_repo
|
|
|
|
def prepare_council_data(self, class_group_id: int, trimester: int) -> CouncilPreparationData:
|
|
"""Prépare toutes les données nécessaires au conseil de classe."""
|
|
|
|
# 1. Résumés par élève
|
|
student_summaries = self.student_evaluation.get_students_summaries(class_group_id, trimester)
|
|
|
|
# 2. Statistiques générales de la classe
|
|
class_statistics = self._calculate_class_statistics(student_summaries)
|
|
|
|
# 3. Statistiques des appréciations
|
|
appreciation_stats = self.appreciation.get_completion_stats(class_group_id, trimester)
|
|
|
|
return CouncilPreparationData(
|
|
class_group_id=class_group_id,
|
|
trimester=trimester,
|
|
student_summaries=student_summaries,
|
|
class_statistics=class_statistics,
|
|
appreciation_stats=appreciation_stats,
|
|
total_students=len(student_summaries),
|
|
completed_appreciations=appreciation_stats['completed_appreciations']
|
|
)
|
|
|
|
def _calculate_class_statistics(self, student_summaries: List[StudentTrimesterSummary]) -> Dict:
|
|
"""Calcule les statistiques de la classe."""
|
|
averages = [s.overall_average for s in student_summaries if s.overall_average is not None]
|
|
|
|
if not averages:
|
|
return {
|
|
'mean': None,
|
|
'median': None,
|
|
'min': None,
|
|
'max': None,
|
|
'std_dev': None,
|
|
'performance_distribution': {
|
|
'excellent': 0,
|
|
'good': 0,
|
|
'average': 0,
|
|
'struggling': 0,
|
|
'no_data': len(student_summaries)
|
|
}
|
|
}
|
|
|
|
# Calculs statistiques
|
|
mean = round(sum(averages) / len(averages), 2)
|
|
sorted_averages = sorted(averages)
|
|
n = len(sorted_averages)
|
|
|
|
if n % 2 == 0:
|
|
median = (sorted_averages[n//2 - 1] + sorted_averages[n//2]) / 2
|
|
else:
|
|
median = sorted_averages[n//2]
|
|
median = round(median, 2)
|
|
|
|
min_avg = min(averages)
|
|
max_avg = max(averages)
|
|
|
|
# Écart-type
|
|
variance = sum((x - mean) ** 2 for x in averages) / len(averages)
|
|
std_dev = round(variance ** 0.5, 2)
|
|
|
|
# Distribution des performances
|
|
performance_distribution = {
|
|
'excellent': 0,
|
|
'good': 0,
|
|
'average': 0,
|
|
'struggling': 0,
|
|
'no_data': 0
|
|
}
|
|
|
|
for summary in student_summaries:
|
|
performance_distribution[summary.performance_status] += 1
|
|
|
|
return {
|
|
'mean': mean,
|
|
'median': median,
|
|
'min': min_avg,
|
|
'max': max_avg,
|
|
'std_dev': std_dev,
|
|
'performance_distribution': performance_distribution,
|
|
'student_count_with_data': len(averages),
|
|
'total_students': len(student_summaries)
|
|
}
|
|
|
|
|
|
# Factory pour créer les services avec injection de dépendances
|
|
class CouncilServiceFactory:
|
|
"""Factory pour créer les services du conseil de classe."""
|
|
|
|
@staticmethod
|
|
def create_council_preparation_service() -> CouncilPreparationService:
|
|
"""Crée le service principal avec toutes ses dépendances."""
|
|
# Repositories
|
|
grade_repo = GradeRepository()
|
|
assessment_repo = AssessmentRepository()
|
|
appreciation_repo = AppreciationRepository()
|
|
|
|
# Services
|
|
student_evaluation_service = StudentEvaluationService(grade_repo, assessment_repo)
|
|
appreciation_service = AppreciationService(appreciation_repo)
|
|
|
|
return CouncilPreparationService(
|
|
student_evaluation_service,
|
|
appreciation_service,
|
|
assessment_repo
|
|
)
|
|
|
|
@staticmethod
|
|
def create_appreciation_service() -> AppreciationService:
|
|
"""Crée le service d'appréciations."""
|
|
appreciation_repo = AppreciationRepository()
|
|
return AppreciationService(appreciation_repo) |