feat: add concil page
This commit is contained in:
		
							
								
								
									
										347
									
								
								services/council_services.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										347
									
								
								services/council_services.py
									
									
									
									
									
										Normal 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) | ||||
		Reference in New Issue
	
	Block a user