325 lines
14 KiB
Python
325 lines
14 KiB
Python
"""
|
|
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))
|
|
} |