feat: add mailing and bilan to send
This commit is contained in:
325
services/student_report_service.py
Normal file
325
services/student_report_service.py
Normal 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))
|
||||
}
|
||||
Reference in New Issue
Block a user