feat: add mailing and bilan to send

This commit is contained in:
2025-09-10 09:04:32 +02:00
parent 2c549c7234
commit 844d4d6bba
14 changed files with 2334 additions and 3 deletions

View File

@@ -0,0 +1,325 @@
"""
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))
}