feat: add concil page

This commit is contained in:
2025-08-11 06:01:23 +02:00
parent 13f0e69bb0
commit c132419213
17 changed files with 5072 additions and 12 deletions

View File

@@ -0,0 +1,347 @@
"""
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)