658 lines
30 KiB
Python
658 lines
30 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, Any
|
|
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'
|
|
competence_domain_breakdown: Optional[Dict] = None # Données des compétences et domaines
|
|
special_values_summary: Optional[Dict] = None # Résumé des valeurs spéciales
|
|
|
|
@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
|
|
)
|
|
|
|
# Calculer les données de compétences et domaines
|
|
competence_domain_breakdown = self.get_student_competence_domain_breakdown(student_id, trimester)
|
|
|
|
# Calculer le résumé des valeurs spéciales
|
|
special_values_summary = self.get_student_special_values_summary(student_id, trimester)
|
|
|
|
# Ajouter tous les commentaires organisés par évaluations
|
|
all_comments_data = self.grade_repo.get_all_comments_by_student_trimester(student_id, trimester)
|
|
special_values_summary['comments_by_assessments'] = all_comments_data
|
|
|
|
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,
|
|
competence_domain_breakdown=competence_domain_breakdown,
|
|
special_values_summary=special_values_summary
|
|
)
|
|
|
|
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 get_student_competence_domain_breakdown(self, student_id: int, trimester: int) -> Dict[str, List[Dict]]:
|
|
"""
|
|
Calcule la répartition des points par compétence et domaine pour un élève.
|
|
|
|
Returns:
|
|
Dict avec 'competences' et 'domains', chacun contenant les points accumulés par évaluation
|
|
"""
|
|
from models import Student, db, GradingCalculator
|
|
|
|
student = Student.query.get(student_id)
|
|
if not student:
|
|
return {'competences': [], 'domains': []}
|
|
|
|
# Récupérer toutes les évaluations du trimestre
|
|
assessments = self.assessment_repo.find_by_class_trimester_with_details(
|
|
student.class_group_id, trimester
|
|
)
|
|
|
|
# Structures pour accumuler les données
|
|
competences_data = {}
|
|
domains_data = {}
|
|
|
|
# Parcourir chaque évaluation
|
|
for assessment in assessments:
|
|
grades = self.grade_repo.get_student_grades_by_assessment(student_id, assessment.id)
|
|
|
|
for grade in grades:
|
|
element = grade.grading_element
|
|
|
|
# Calculer le score pour cet élément
|
|
if grade.value and GradingCalculator.is_counted_in_total(grade.value, element.grading_type):
|
|
score = GradingCalculator.calculate_score(
|
|
grade.value, element.grading_type, element.max_points
|
|
)
|
|
|
|
if score is not None:
|
|
# Traiter les compétences
|
|
if element.skill:
|
|
if element.skill not in competences_data:
|
|
competences_data[element.skill] = {
|
|
'name': element.skill,
|
|
'earned_points': 0.0,
|
|
'total_points': 0.0,
|
|
'assessments': {}
|
|
}
|
|
|
|
# Accumuler les points
|
|
competences_data[element.skill]['earned_points'] += score
|
|
competences_data[element.skill]['total_points'] += element.max_points
|
|
|
|
# Grouper par évaluation
|
|
if assessment.id not in competences_data[element.skill]['assessments']:
|
|
competences_data[element.skill]['assessments'][assessment.id] = {
|
|
'id': assessment.id,
|
|
'title': assessment.title,
|
|
'earned': 0.0,
|
|
'max': 0.0,
|
|
'date': assessment.date
|
|
}
|
|
|
|
competences_data[element.skill]['assessments'][assessment.id]['earned'] += score
|
|
competences_data[element.skill]['assessments'][assessment.id]['max'] += element.max_points
|
|
|
|
# Traiter les domaines
|
|
if element.domain_id:
|
|
domain = element.domain
|
|
domain_name = domain.name if domain else f"Domaine {element.domain_id}"
|
|
|
|
if domain_name not in domains_data:
|
|
domains_data[domain_name] = {
|
|
'id': element.domain_id,
|
|
'name': domain_name,
|
|
'color': domain.color if domain else '#6B7280',
|
|
'earned_points': 0.0,
|
|
'total_points': 0.0,
|
|
'assessments': {}
|
|
}
|
|
|
|
# Accumuler les points
|
|
domains_data[domain_name]['earned_points'] += score
|
|
domains_data[domain_name]['total_points'] += element.max_points
|
|
|
|
# Grouper par évaluation
|
|
if assessment.id not in domains_data[domain_name]['assessments']:
|
|
domains_data[domain_name]['assessments'][assessment.id] = {
|
|
'id': assessment.id,
|
|
'title': assessment.title,
|
|
'earned': 0.0,
|
|
'max': 0.0,
|
|
'date': assessment.date
|
|
}
|
|
|
|
domains_data[domain_name]['assessments'][assessment.id]['earned'] += score
|
|
domains_data[domain_name]['assessments'][assessment.id]['max'] += element.max_points
|
|
|
|
# Récupérer la configuration des compétences pour les couleurs
|
|
from app_config import config_manager
|
|
competences_config = {comp['name']: comp for comp in config_manager.get_competences_list()}
|
|
|
|
# Finaliser les données des compétences
|
|
competences = []
|
|
for comp_name, comp_data in competences_data.items():
|
|
config = competences_config.get(comp_name, {})
|
|
|
|
# Calculer le pourcentage
|
|
percentage = (comp_data['earned_points'] / comp_data['total_points'] * 100) if comp_data['total_points'] > 0 else 0
|
|
|
|
# Convertir les assessments en liste avec calculs d'accumulation
|
|
assessments_list = []
|
|
# Palette de couleurs moderne et contrastée
|
|
colors_palette = ['#3B82F6', '#EF4444', '#10B981', '#F59E0B', '#8B5CF6', '#EC4899', '#06B6D4', '#84CC16']
|
|
|
|
# D'abord, trier par date pour avoir l'ordre chronologique
|
|
sorted_assessments = sorted(comp_data['assessments'].values(),
|
|
key=lambda x: x['date'] if x['date'] else datetime.min)
|
|
|
|
# Calculer l'accumulation progressive
|
|
cumulative_earned = 0
|
|
cumulative_max = 0
|
|
|
|
for i, assessment_data in enumerate(sorted_assessments):
|
|
# Ajouter les points de cette évaluation à l'accumulation
|
|
cumulative_earned += assessment_data['earned']
|
|
cumulative_max += assessment_data['max']
|
|
|
|
# Calculer les pourcentages
|
|
cumulative_percentage = (cumulative_earned / comp_data['total_points'] * 100) if comp_data['total_points'] > 0 else 0
|
|
assessment_performance = (assessment_data['earned'] / assessment_data['max'] * 100) if assessment_data['max'] > 0 else 0
|
|
# Calculer le pourcentage de contribution de cette évaluation par rapport au total final
|
|
contribution_percentage = (assessment_data['earned'] / comp_data['total_points'] * 100) if comp_data['total_points'] > 0 else 0
|
|
|
|
# Couleur cohérente basée sur l'ID de l'évaluation pour garantir la cohérence entre compétences/domaines
|
|
assessment_color = colors_palette[assessment_data['id'] % len(colors_palette)]
|
|
|
|
assessments_list.append({
|
|
'id': assessment_data['id'],
|
|
'title': assessment_data['title'],
|
|
'earned_this': round(assessment_data['earned'], 2), # Points de cette évaluation
|
|
'max_this': round(assessment_data['max'], 2), # Max de cette évaluation
|
|
'earned_cumulative': round(cumulative_earned, 2), # Total cumulé jusqu'à cette éval
|
|
'max_cumulative': round(cumulative_max, 2), # Max cumulé jusqu'à cette éval
|
|
'date': assessment_data['date'],
|
|
'percentage_cumulative': round(cumulative_percentage, 1), # % cumulé par rapport au total
|
|
'percentage_contribution': round(contribution_percentage, 1), # % de contribution individuelle
|
|
'performance': round(assessment_performance, 1), # % de réussite de cette évaluation seule
|
|
'color': assessment_color # Couleur cohérente basée sur l'ID
|
|
})
|
|
|
|
competences.append({
|
|
'name': comp_name,
|
|
'color': config.get('color', '#6B7280'),
|
|
'earned_points': round(comp_data['earned_points'], 2),
|
|
'total_points': round(comp_data['total_points'], 2),
|
|
'percentage': round(percentage, 1),
|
|
'assessments': assessments_list
|
|
})
|
|
|
|
# Finaliser les données des domaines
|
|
domains = []
|
|
for domain_data in domains_data.values():
|
|
# Calculer le pourcentage
|
|
percentage = (domain_data['earned_points'] / domain_data['total_points'] * 100) if domain_data['total_points'] > 0 else 0
|
|
|
|
# Convertir les assessments en liste avec calculs d'accumulation
|
|
assessments_list = []
|
|
# Utiliser la même palette que les compétences pour la cohérence
|
|
colors_palette = ['#3B82F6', '#EF4444', '#10B981', '#F59E0B', '#8B5CF6', '#EC4899', '#06B6D4', '#84CC16']
|
|
|
|
# D'abord, trier par date pour avoir l'ordre chronologique
|
|
sorted_assessments = sorted(domain_data['assessments'].values(),
|
|
key=lambda x: x['date'] if x['date'] else datetime.min)
|
|
|
|
# Calculer l'accumulation progressive
|
|
cumulative_earned = 0
|
|
cumulative_max = 0
|
|
|
|
for i, assessment_data in enumerate(sorted_assessments):
|
|
# Ajouter les points de cette évaluation à l'accumulation
|
|
cumulative_earned += assessment_data['earned']
|
|
cumulative_max += assessment_data['max']
|
|
|
|
# Calculer les pourcentages
|
|
cumulative_percentage = (cumulative_earned / domain_data['total_points'] * 100) if domain_data['total_points'] > 0 else 0
|
|
assessment_performance = (assessment_data['earned'] / assessment_data['max'] * 100) if assessment_data['max'] > 0 else 0
|
|
# Calculer le pourcentage de contribution de cette évaluation par rapport au total final
|
|
contribution_percentage = (assessment_data['earned'] / domain_data['total_points'] * 100) if domain_data['total_points'] > 0 else 0
|
|
|
|
# Couleur cohérente basée sur l'ID de l'évaluation (même logique que les compétences)
|
|
assessment_color = colors_palette[assessment_data['id'] % len(colors_palette)]
|
|
|
|
assessments_list.append({
|
|
'id': assessment_data['id'],
|
|
'title': assessment_data['title'],
|
|
'earned_this': round(assessment_data['earned'], 2), # Points de cette évaluation
|
|
'max_this': round(assessment_data['max'], 2), # Max de cette évaluation
|
|
'earned_cumulative': round(cumulative_earned, 2), # Total cumulé jusqu'à cette éval
|
|
'max_cumulative': round(cumulative_max, 2), # Max cumulé jusqu'à cette éval
|
|
'date': assessment_data['date'],
|
|
'percentage_cumulative': round(cumulative_percentage, 1), # % cumulé par rapport au total
|
|
'percentage_contribution': round(contribution_percentage, 1), # % de contribution individuelle
|
|
'performance': round(assessment_performance, 1), # % de réussite de cette évaluation seule
|
|
'color': assessment_color # Couleur cohérente basée sur l'ID
|
|
})
|
|
|
|
domains.append({
|
|
'id': domain_data['id'],
|
|
'name': domain_data['name'],
|
|
'color': domain_data['color'],
|
|
'earned_points': round(domain_data['earned_points'], 2),
|
|
'total_points': round(domain_data['total_points'], 2),
|
|
'percentage': round(percentage, 1),
|
|
'assessments': assessments_list
|
|
})
|
|
|
|
# Trier par nom
|
|
competences.sort(key=lambda x: x['name'].lower())
|
|
domains.sort(key=lambda x: x['name'].lower())
|
|
|
|
return {
|
|
'competences': competences,
|
|
'domains': domains
|
|
}
|
|
|
|
def get_student_special_values_summary(self, student_id: int, trimester: int) -> Dict[str, Any]:
|
|
"""
|
|
Calcule le résumé des valeurs spéciales pour un élève sur un trimestre.
|
|
|
|
Returns:
|
|
Dict avec 'global' (total par trimestre) et 'by_assessment' (détail par évaluation)
|
|
"""
|
|
from app_config import config_manager
|
|
|
|
# Récupérer les valeurs spéciales configurées
|
|
special_values_config = config_manager.get_special_values()
|
|
|
|
# 1. Comptes globaux par trimestre
|
|
global_counts = self.grade_repo.get_special_values_counts_by_student_trimester(student_id, trimester)
|
|
global_details = self.grade_repo.get_special_values_details_by_student_trimester(student_id, trimester)
|
|
|
|
# 2. Comptes par évaluation
|
|
assessments = self.assessment_repo.find_by_class_trimester_with_details(
|
|
Student.query.get(student_id).class_group_id, trimester
|
|
)
|
|
|
|
by_assessment = {}
|
|
for assessment in assessments:
|
|
assessment_counts = self.grade_repo.get_special_values_counts_by_student_assessment(
|
|
student_id, assessment.id
|
|
)
|
|
assessment_details = self.grade_repo.get_special_values_details_by_student_assessment(
|
|
student_id, assessment.id
|
|
)
|
|
|
|
# Ajouter seulement si l'élève a des valeurs spéciales dans cette évaluation
|
|
if any(count > 0 for count in assessment_counts.values()):
|
|
by_assessment[assessment.id] = {
|
|
'title': assessment.title,
|
|
'date': assessment.date,
|
|
'counts': assessment_counts,
|
|
'details': assessment_details
|
|
}
|
|
|
|
# 3. Enrichir avec les métadonnées de configuration
|
|
enriched_global = {}
|
|
enriched_by_assessment = {}
|
|
|
|
for special_value, config in special_values_config.items():
|
|
enriched_global[special_value] = {
|
|
'count': global_counts.get(special_value, 0),
|
|
'label': config['label'],
|
|
'color': config['color'],
|
|
'counts_in_total': config['counts'],
|
|
'details': global_details.get(special_value, [])
|
|
}
|
|
|
|
for assessment_id, assessment_data in by_assessment.items():
|
|
enriched_by_assessment[assessment_id] = {
|
|
'title': assessment_data['title'],
|
|
'date': assessment_data['date'],
|
|
'special_values': {}
|
|
}
|
|
|
|
for special_value, config in special_values_config.items():
|
|
enriched_by_assessment[assessment_id]['special_values'][special_value] = {
|
|
'count': assessment_data['counts'].get(special_value, 0),
|
|
'label': config['label'],
|
|
'color': config['color'],
|
|
'details': assessment_data['details'].get(special_value, [])
|
|
}
|
|
|
|
return {
|
|
'global': enriched_global,
|
|
'by_assessment': enriched_by_assessment,
|
|
'total_special_values': sum(global_counts.values()),
|
|
'has_special_values': sum(global_counts.values()) > 0
|
|
}
|
|
|
|
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) |