""" Service de génération de bilans d'évaluation pour les élèves. Génère les rapports HTML individualisés à envoyer par email. """ from typing import Dict, List, Any, Optional from models import Assessment, Student, GradingCalculator class StudentReportService: """Service de génération des bilans d'évaluation individuels.""" def __init__(self, config_manager=None): """Initialise le service avec le gestionnaire de configuration.""" if config_manager is None: from app_config import config_manager as default_manager self.config_manager = default_manager else: self.config_manager = config_manager def generate_student_report(self, assessment: Assessment, student: Student) -> Dict[str, Any]: """ Génère le rapport individuel d'un élève pour une évaluation. Args: assessment: L'évaluation concernée student: L'élève pour qui générer le rapport Returns: Dict contenant toutes les données du rapport """ # Récupérer les labels des compétences depuis la configuration score_meanings = self.config_manager.get_score_meanings() # Calculer les scores de tous les élèves pour avoir les statistiques students_scores, exercise_scores = assessment.calculate_student_scores() # Vérifier que l'élève est dans les résultats if student.id not in students_scores: raise ValueError(f"L'élève {student.full_name} n'est pas éligible pour cette évaluation") student_data = students_scores[student.id] # Calculer les statistiques de classe statistics = assessment.get_assessment_statistics() total_max_points = assessment.get_total_max_points() # Calculer la position de l'élève dans la classe all_scores = [data['total_score'] for data in students_scores.values()] all_scores_sorted = sorted(all_scores, reverse=True) student_position = all_scores_sorted.index(student_data['total_score']) + 1 total_students = len(all_scores) # Préparer les détails par exercice exercises_details = [] for exercise in sorted(assessment.exercises, key=lambda x: x.order): exercise_score = student_data['exercises'].get(exercise.id, {'score': 0, 'max_points': 0}) # Détails des éléments de notation de l'exercice elements_details = [] for element in exercise.grading_elements: # Trouver la note de l'élève pour cet élément grade = None for g in element.grades: if g.student_id == student.id: grade = g break if grade and grade.value: calculated_score = GradingCalculator.calculate_score( grade.value, element.grading_type, element.max_points ) # Récupérer le label de compétence si c'est un score score_label = '' if element.grading_type == 'score' and grade.value.isdigit(): score_val = int(grade.value) if score_val in score_meanings: score_label = score_meanings[score_val]['label'] elements_details.append({ 'label': element.label, 'description': element.description or '', 'skill': element.skill or '', 'domain': element.domain.name if element.domain else '', 'raw_value': grade.value, 'calculated_score': calculated_score, 'max_points': element.max_points, 'grading_type': element.grading_type, 'score_label': score_label, 'comment': grade.comment or '' }) else: elements_details.append({ 'label': element.label, 'description': element.description or '', 'skill': element.skill or '', 'domain': element.domain.name if element.domain else '', 'raw_value': None, 'calculated_score': None, 'max_points': element.max_points, 'grading_type': element.grading_type, 'score_label': '', 'comment': '' }) exercises_details.append({ 'title': exercise.title, 'description': exercise.description or '', 'score': exercise_score['score'], 'max_points': exercise_score['max_points'], 'percentage': round((exercise_score['score'] / exercise_score['max_points']) * 100, 1) if exercise_score['max_points'] > 0 else 0, 'elements': elements_details }) # Calculer les performances par compétence competences_performance = self._calculate_competences_performance(assessment, student) # Calculer les performances par domaine domains_performance = self._calculate_domains_performance(assessment, student) return { 'assessment': { 'title': assessment.title, 'description': assessment.description or '', 'date': assessment.date, 'trimester': assessment.trimester, 'class_name': assessment.class_group.name, 'coefficient': assessment.coefficient }, 'student': { 'full_name': student.full_name, 'first_name': student.first_name, 'last_name': student.last_name, 'email': student.email }, 'results': { 'total_score': student_data['total_score'], 'total_max_points': student_data['total_max_points'], 'percentage': round((student_data['total_score'] / student_data['total_max_points']) * 100, 1) if student_data['total_max_points'] > 0 else 0, 'position': student_position, 'total_students': total_students }, 'exercises': exercises_details, 'competences': competences_performance, 'domains': domains_performance, 'class_statistics': { 'count': statistics['count'], 'mean': statistics['mean'], 'median': statistics['median'], 'min': statistics['min'], 'max': statistics['max'], 'std_dev': statistics['std_dev'] } } def _calculate_competences_performance(self, assessment: Assessment, student: Student) -> List[Dict[str, Any]]: """Calcule les performances de l'élève par compétence.""" competences_data = {} for exercise in assessment.exercises: for element in exercise.grading_elements: if element.skill: # Trouver la note de l'élève grade = None for g in element.grades: if g.student_id == student.id: grade = g break if grade and grade.value: score = GradingCalculator.calculate_score( grade.value, element.grading_type, element.max_points ) if score is not None: # Exclure les dispensés if element.skill not in competences_data: competences_data[element.skill] = { 'total_score': 0, 'total_max_points': 0, 'elements_count': 0 } competences_data[element.skill]['total_score'] += score competences_data[element.skill]['total_max_points'] += element.max_points competences_data[element.skill]['elements_count'] += 1 # Convertir en liste avec pourcentages competences_performance = [] for competence, data in competences_data.items(): percentage = round((data['total_score'] / data['total_max_points']) * 100, 1) if data['total_max_points'] > 0 else 0 competences_performance.append({ 'name': competence, 'score': data['total_score'], 'max_points': data['total_max_points'], 'percentage': percentage, 'elements_count': data['elements_count'] }) return sorted(competences_performance, key=lambda x: x['name']) def _calculate_domains_performance(self, assessment: Assessment, student: Student) -> List[Dict[str, Any]]: """Calcule les performances de l'élève par domaine.""" domains_data = {} for exercise in assessment.exercises: for element in exercise.grading_elements: if element.domain: # Trouver la note de l'élève grade = None for g in element.grades: if g.student_id == student.id: grade = g break if grade and grade.value: score = GradingCalculator.calculate_score( grade.value, element.grading_type, element.max_points ) if score is not None: # Exclure les dispensés domain_name = element.domain.name if domain_name not in domains_data: domains_data[domain_name] = { 'total_score': 0, 'total_max_points': 0, 'elements_count': 0, 'color': element.domain.color } domains_data[domain_name]['total_score'] += score domains_data[domain_name]['total_max_points'] += element.max_points domains_data[domain_name]['elements_count'] += 1 # Convertir en liste avec pourcentages domains_performance = [] for domain, data in domains_data.items(): percentage = round((data['total_score'] / data['total_max_points']) * 100, 1) if data['total_max_points'] > 0 else 0 domains_performance.append({ 'name': domain, 'score': data['total_score'], 'max_points': data['total_max_points'], 'percentage': percentage, 'elements_count': data['elements_count'], 'color': data['color'] }) return sorted(domains_performance, key=lambda x: x['name']) def generate_multiple_reports(self, assessment: Assessment, student_ids: List[int]) -> Dict[int, Dict[str, Any]]: """ Génère les rapports pour plusieurs élèves. Args: assessment: L'évaluation concernée student_ids: Liste des IDs d'élèves Returns: Dict avec les rapports par ID d'élève """ from models import Student reports = {} errors = {} for student_id in student_ids: try: student = Student.query.get(student_id) if not student: errors[student_id] = "Élève introuvable" continue report = self.generate_student_report(assessment, student) reports[student_id] = report except Exception as e: errors[student_id] = str(e) return { 'reports': reports, 'errors': errors, 'success_count': len(reports), 'error_count': len(errors) } def get_assessment_summary(self, assessment: Assessment) -> Dict[str, Any]: """ Génère un résumé de l'évaluation pour les emails. Args: assessment: L'évaluation concernée Returns: Dict avec le résumé de l'évaluation """ statistics = assessment.get_assessment_statistics() total_max_points = assessment.get_total_max_points() # Compter les exercices et éléments exercises_count = len(assessment.exercises) elements_count = sum(len(ex.grading_elements) for ex in assessment.exercises) # Récupérer les compétences et domaines évalués competences = set() domains = set() for exercise in assessment.exercises: for element in exercise.grading_elements: if element.skill: competences.add(element.skill) if element.domain: domains.add(element.domain.name) return { 'assessment': { 'title': assessment.title, 'description': assessment.description or '', 'date': assessment.date, 'trimester': assessment.trimester, 'class_name': assessment.class_group.name, 'coefficient': assessment.coefficient, 'total_max_points': total_max_points, 'exercises_count': exercises_count, 'elements_count': elements_count }, 'statistics': statistics, 'competences_evaluated': sorted(list(competences)), 'domains_evaluated': sorted(list(domains)) }