Files
notytex/backend/domain/services/student_report_service.py
Bertrand Benjamin f76b033d55
All checks were successful
Build and Publish Docker Images / Build Frontend Image (push) Successful in 2m56s
Build and Publish Docker Images / Build Backend Image (push) Successful in 3m5s
Build and Publish Docker Images / Build Summary (push) Successful in 3s
feat(mail): restauration de l'envoie de mail
2025-12-04 06:04:13 +01:00

472 lines
18 KiB
Python

"""
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 en utilisant le template Jinja2.
Args:
report_data: Données du rapport
message: Message personnalisé du professeur
Returns:
HTML du rapport
"""
from jinja2 import Environment, FileSystemLoader, select_autoescape
from pathlib import Path
# Déterminer le chemin du template
template_dir = Path(__file__).parent.parent.parent / "templates" / "email"
# Initialiser Jinja2
env = Environment(
loader=FileSystemLoader(str(template_dir)),
autoescape=select_autoescape(['html', 'xml'])
)
template = env.get_template('student_report.html')
# Déterminer la couleur du score basée sur le pourcentage
results = report_data['results']
percentage = results['percentage']
if percentage >= 80:
score_color = "#22c55e" # vert
elif percentage >= 60:
score_color = "#f6d32d" # jaune
elif percentage >= 40:
score_color = "#f97316" # orange
else:
score_color = "#ef4444" # rouge
# Rendre le template avec les données
html = template.render(
assessment=report_data['assessment'],
student=report_data['student'],
results=report_data['results'],
class_statistics=report_data['class_statistics'],
exercises=report_data['exercises'],
competences=report_data.get('competences', []),
domains=report_data.get('domains', []),
custom_message=message,
score_color=score_color
)
return html