""" Service de génération de bilans d'évaluation pour les élèves. Génère les rapports individualisés à envoyer par email. """ from typing import Dict, List, Any, Optional from dataclasses import dataclass from domain.services.grading_calculator import GradingCalculator from domain.services.config_service import ConfigService from domain.services.score_calculator import StudentScoreCalculator from domain.services.statistics_service import StatisticsService @dataclass class StudentReportData: """Données d'un rapport d'élève.""" assessment: Dict[str, Any] student: Dict[str, Any] results: Dict[str, Any] exercises: List[Dict[str, Any]] competences: List[Dict[str, Any]] domains: List[Dict[str, Any]] class_statistics: Dict[str, Any] class StudentReportService: """Service de génération des bilans d'évaluation individuels.""" def __init__(self, config_service: Optional[ConfigService] = None): """ Initialise le service avec le service de configuration. Args: config_service: Service de configuration (utilise les valeurs par défaut si non fourni) """ self.config_service = config_service or ConfigService() # Convertir les valeurs spéciales du ConfigService en format attendu par GradingCalculator special_values_dict = self.config_service.get_special_values_dict() self.grading_calculator = GradingCalculator(special_values_dict) self.stats_service = StatisticsService() def generate_student_report( self, assessment_data: Dict[str, Any], student_data: Dict[str, Any], all_students_grades: Dict[int, List[Dict[str, Any]]], exercises_data: List[Dict[str, Any]] ) -> Dict[str, Any]: """ Génère le rapport individuel d'un élève pour une évaluation. Args: assessment_data: Données de l'évaluation student_data: Données de l'élève all_students_grades: Notes de tous les élèves {student_id: [grades]} exercises_data: Données des exercices avec leurs éléments Returns: Dict contenant toutes les données du rapport """ student_id = student_data['id'] score_meanings = self.config_service.get_score_meanings() # Calculer les scores de tous les élèves all_student_scores = self._calculate_all_student_scores( all_students_grades, exercises_data ) # Vérifier que l'élève est dans les résultats if student_id not in all_student_scores: raise ValueError(f"L'élève {student_data.get('full_name', student_id)} n'a pas de notes pour cette évaluation") student_scores = all_student_scores[student_id] # Calculer les statistiques de classe all_totals = [s['total_score'] for s in all_student_scores.values()] statistics = self.stats_service.calculate_statistics(all_totals) # Calculer la position de l'élève dans la classe all_scores_sorted = sorted(all_totals, reverse=True) student_position = all_scores_sorted.index(student_scores['total_score']) + 1 total_students = len(all_totals) # Préparer les détails par exercice exercises_details = self._prepare_exercises_details( exercises_data, student_scores, all_students_grades.get(student_id, []), score_meanings ) # Calculer les performances par compétence competences_performance = self._calculate_competences_performance( exercises_data, all_students_grades.get(student_id, []) ) # Calculer les performances par domaine domains_performance = self._calculate_domains_performance( exercises_data, all_students_grades.get(student_id, []) ) return { 'assessment': { 'title': assessment_data['title'], 'description': assessment_data.get('description', ''), 'date': assessment_data['date'], 'trimester': assessment_data['trimester'], 'class_name': assessment_data['class_name'], 'coefficient': assessment_data.get('coefficient', 1.0) }, 'student': { 'full_name': f"{student_data['first_name']} {student_data['last_name']}", 'first_name': student_data['first_name'], 'last_name': student_data['last_name'], 'email': student_data.get('email') }, 'results': { 'total_score': student_scores['total_score'], 'total_max_points': student_scores['total_max_points'], 'percentage': round( (student_scores['total_score'] / student_scores['total_max_points']) * 100, 1 ) if student_scores['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_all_student_scores( self, all_students_grades: Dict[int, List[Dict[str, Any]]], exercises_data: List[Dict[str, Any]] ) -> Dict[int, Dict[str, Any]]: """Calcule les scores de tous les élèves.""" results = {} # Créer un mapping element_id -> element_data elements_map = {} for exercise in exercises_data: for element in exercise.get('elements', []): elements_map[element['id']] = element for student_id, grades in all_students_grades.items(): total_score = 0.0 total_max = 0.0 exercises_scores = {} # Grouper les notes par exercice for grade in grades: element_id = grade['element_id'] element = elements_map.get(element_id) if not element: continue exercise_id = element['exercise_id'] value = grade['value'] # Calculer le score calculated = self.grading_calculator.calculate_score( value, element['grading_type'], element['max_points'] ) if calculated is not None: if exercise_id not in exercises_scores: exercises_scores[exercise_id] = {'score': 0, 'max_points': 0} exercises_scores[exercise_id]['score'] += calculated exercises_scores[exercise_id]['max_points'] += element['max_points'] total_score += calculated total_max += element['max_points'] results[student_id] = { 'total_score': round(total_score, 2), 'total_max_points': round(total_max, 2), 'exercises': exercises_scores } return results def _prepare_exercises_details( self, exercises_data: List[Dict[str, Any]], student_scores: Dict[str, Any], student_grades: List[Dict[str, Any]], score_meanings: Dict[int, Any] ) -> List[Dict[str, Any]]: """Prépare les détails par exercice pour le rapport.""" # Créer un mapping grade par element_id grades_map = {g['element_id']: g for g in student_grades} exercises_details = [] for exercise in sorted(exercises_data, key=lambda x: x.get('order', 0)): exercise_id = exercise['id'] exercise_score = student_scores.get('exercises', {}).get(exercise_id, {'score': 0, 'max_points': 0}) # Détails des éléments de notation elements_details = [] for element in exercise.get('elements', []): grade = grades_map.get(element['id']) if grade and grade.get('value'): value = grade['value'] calculated = self.grading_calculator.calculate_score( 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 value.isdigit(): score_val = int(value) meaning = score_meanings.get(score_val) if meaning: score_label = meaning.label elements_details.append({ 'label': element['label'], 'description': element.get('description', ''), 'skill': element.get('skill', ''), 'domain': element.get('domain_name', ''), 'raw_value': value, 'calculated_score': calculated, 'max_points': element['max_points'], 'grading_type': element['grading_type'], 'score_label': score_label, 'comment': grade.get('comment', '') }) else: elements_details.append({ 'label': element['label'], 'description': element.get('description', ''), 'skill': element.get('skill', ''), 'domain': element.get('domain_name', ''), 'raw_value': None, 'calculated_score': None, 'max_points': element['max_points'], 'grading_type': element['grading_type'], 'score_label': '', 'comment': '' }) max_pts = exercise_score['max_points'] exercises_details.append({ 'title': exercise['title'], 'description': exercise.get('description', ''), 'score': exercise_score['score'], 'max_points': max_pts, 'percentage': round((exercise_score['score'] / max_pts) * 100, 1) if max_pts > 0 else 0, 'elements': elements_details }) return exercises_details def _calculate_competences_performance( self, exercises_data: List[Dict[str, Any]], student_grades: List[Dict[str, Any]] ) -> List[Dict[str, Any]]: """Calcule les performances de l'élève par compétence.""" competences_data = {} grades_map = {g['element_id']: g for g in student_grades} for exercise in exercises_data: for element in exercise.get('elements', []): skill = element.get('skill') if not skill: continue grade = grades_map.get(element['id']) if grade and grade.get('value'): calculated = self.grading_calculator.calculate_score( grade['value'], element['grading_type'], element['max_points'] ) if calculated is not None: if skill not in competences_data: competences_data[skill] = { 'total_score': 0, 'total_max_points': 0, 'elements_count': 0 } competences_data[skill]['total_score'] += calculated competences_data[skill]['total_max_points'] += element['max_points'] competences_data[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, exercises_data: List[Dict[str, Any]], student_grades: List[Dict[str, Any]] ) -> List[Dict[str, Any]]: """Calcule les performances de l'élève par domaine.""" domains_data = {} grades_map = {g['element_id']: g for g in student_grades} for exercise in exercises_data: for element in exercise.get('elements', []): domain_name = element.get('domain_name') if not domain_name: continue grade = grades_map.get(element['id']) if grade and grade.get('value'): calculated = self.grading_calculator.calculate_score( grade['value'], element['grading_type'], element['max_points'] ) if calculated is not None: if domain_name not in domains_data: domains_data[domain_name] = { 'total_score': 0, 'total_max_points': 0, 'elements_count': 0, 'color': element.get('domain_color', '#6b7280') } domains_data[domain_name]['total_score'] += calculated 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 get_assessment_summary( self, assessment_data: Dict[str, Any], exercises_data: List[Dict[str, Any]], statistics: Dict[str, Any] ) -> Dict[str, Any]: """ Génère un résumé de l'évaluation pour les emails. Args: assessment_data: Données de l'évaluation exercises_data: Données des exercices statistics: Statistiques de l'évaluation Returns: Dict avec le résumé de l'évaluation """ # Calculer le total des points total_max_points = 0 elements_count = 0 competences = set() domains = set() for exercise in exercises_data: for element in exercise.get('elements', []): total_max_points += element['max_points'] elements_count += 1 if element.get('skill'): competences.add(element['skill']) if element.get('domain_name'): domains.add(element['domain_name']) return { 'assessment': { 'title': assessment_data['title'], 'description': assessment_data.get('description', ''), 'date': assessment_data['date'], 'trimester': assessment_data['trimester'], 'class_name': assessment_data['class_name'], 'coefficient': assessment_data.get('coefficient', 1.0), 'total_max_points': total_max_points, 'exercises_count': len(exercises_data), 'elements_count': elements_count }, 'statistics': statistics, 'competences_evaluated': sorted(list(competences)), 'domains_evaluated': sorted(list(domains)) } def generate_report_html(report_data: Dict[str, Any], message: str = "") -> str: """ Génère le HTML du rapport d'élève. Args: report_data: Données du rapport message: Message personnalisé du professeur Returns: HTML du rapport """ student = report_data['student'] assessment = report_data['assessment'] results = report_data['results'] stats = report_data['class_statistics'] # Couleur basée sur le pourcentage percentage = results['percentage'] if percentage >= 80: color = "#22c55e" # vert elif percentage >= 60: color = "#f6d32d" # jaune elif percentage >= 40: color = "#f97316" # orange else: color = "#ef4444" # rouge html = f"""

{assessment['title']}

{assessment['class_name']} - Trimestre {assessment['trimester']}

Date: {assessment['date']}

Bonjour {student['first_name']},

Voici votre bilan pour l'évaluation {assessment['title']}.

{results['total_score']}/{results['total_max_points']} ({results['percentage']}%)

Position: {results['position']}/{results['total_students']} élèves

Statistiques de la classe

Moyenne
{stats['mean']}
Médiane
{stats['median']}
Écart-type
{stats['std_dev']}

Détail par exercice

""" # Ajouter les exercices for exercise in report_data['exercises']: html += f"""
{exercise['title']} - {exercise['score']}/{exercise['max_points']} ({exercise['percentage']}%)
""" for element in exercise['elements']: value_display = element.get('raw_value', '-') or '-' if element.get('score_label'): value_display = f"{value_display} ({element['score_label']})" html += f"""
{element['label']}: {value_display}
""" html += "
" # Message personnalisé if message: html += f"""
Message du professeur:
{message}
""" html += """ """ return html