feat: add temporal student gestion

This commit is contained in:
2025-08-16 06:42:47 +02:00
parent f438082c4c
commit 6549591f63
15 changed files with 2212 additions and 148 deletions

View File

@@ -51,9 +51,14 @@ class StudentEvaluationService:
def calculate_student_trimester_average(self, student_id: int, trimester: int) -> Optional[float]:
"""Calcule la moyenne d'un élève pour un trimestre donné."""
# Récupérer la classe actuelle de l'élève avec la logique temporelle
student = Student.query.get(student_id)
current_class = student.get_current_class()
if not current_class:
return None
assessments = self.assessment_repo.find_completed_by_class_trimester(
# On récupère d'abord la classe de l'élève
Student.query.get(student_id).class_group_id,
current_class.id,
trimester
)
@@ -74,10 +79,20 @@ class StudentEvaluationService:
def get_student_trimester_summary(self, student_id: int, trimester: int) -> StudentTrimesterSummary:
"""Génère le résumé d'un élève pour un trimestre."""
student = Student.query.get(student_id)
current_class = student.get_current_class()
if not current_class:
return StudentTrimesterSummary(
student=student,
overall_average=None,
assessment_count=0,
grades_by_assessment={},
appreciation=None,
performance_status='no_data'
)
# Récupérer les évaluations du trimestre
assessments = self.assessment_repo.find_by_class_trimester_with_details(
student.class_group_id, trimester
current_class.id, trimester
)
# Calculer les scores par évaluation
@@ -96,7 +111,7 @@ class StudentEvaluationService:
# Récupérer l'appréciation existante
appreciation_repo = AppreciationRepository()
appreciation = appreciation_repo.find_by_student_trimester(
student_id, student.class_group_id, trimester
student_id, current_class.id, trimester
)
# Calculer les données de compétences et domaines
@@ -121,9 +136,10 @@ class StudentEvaluationService:
)
def get_students_summaries(self, class_group_id: int, trimester: int) -> List[StudentTrimesterSummary]:
"""Génère les résumés de tous les élèves d'une classe pour un trimestre."""
student_repo = StudentRepository()
students = student_repo.find_by_class_group(class_group_id)
"""Génère les résumés de tous les élèves actuellement dans une classe pour un trimestre."""
from repositories.temporal_student_repository import TemporalStudentRepository
temporal_repo = TemporalStudentRepository()
students = temporal_repo.find_current_students_in_class(class_group_id)
summaries = []
for student in students:
@@ -148,9 +164,13 @@ class StudentEvaluationService:
if not student:
return {'competences': [], 'domains': []}
# Récupérer toutes les évaluations du trimestre
# Récupérer la classe actuelle et toutes les évaluations du trimestre
current_class = student.get_current_class()
if not current_class:
return {'competences': [], 'domains': []}
assessments = self.assessment_repo.find_by_class_trimester_with_details(
student.class_group_id, trimester
current_class.id, trimester
)
# Structures pour accumuler les données
@@ -375,8 +395,18 @@ class StudentEvaluationService:
global_details = self.grade_repo.get_special_values_details_by_student_trimester(student_id, trimester)
# 2. Comptes par évaluation
student = Student.query.get(student_id)
current_class = student.get_current_class()
if not current_class:
return {
'global': {},
'by_assessment': {},
'total_special_values': 0,
'has_special_values': False
}
assessments = self.assessment_repo.find_by_class_trimester_with_details(
Student.query.get(student_id).class_group_id, trimester
current_class.id, trimester
)
by_assessment = {}

View File

@@ -0,0 +1,207 @@
from dataclasses import dataclass
from typing import List, Dict, Tuple
from collections import defaultdict
from models import db, Grade, GradingElement, Exercise
from repositories.temporal_student_repository import TemporalStudentRepository
from services.assessment_services import AssessmentProgressService, StudentScoreCalculator, ProgressResult, StudentScore
from providers.concrete_providers import SQLAlchemyDatabaseProvider
from sqlalchemy import and_, func
@dataclass
class TemporalProgressResult(ProgressResult):
"""Extension avec information temporelle."""
eligible_students_count: int
total_students_in_class: int
class TemporalAssessmentProgressService(AssessmentProgressService):
"""
Service de progression avec logique temporelle.
Hérite du service existant et ajoute la logique temporelle.
"""
def __init__(self, db_provider: SQLAlchemyDatabaseProvider, student_repo: TemporalStudentRepository):
super().__init__(db_provider)
self.student_repo = student_repo
def calculate_grading_progress(self, assessment) -> TemporalProgressResult:
"""
Calcule la progression en ne comptant que les élèves éligibles.
"""
# Étudiants éligibles à la date de l'évaluation
eligible_students = self.student_repo.find_eligible_for_assessment(assessment)
current_students = self.student_repo.find_current_students_in_class(assessment.class_group_id)
if not eligible_students:
return TemporalProgressResult(
percentage=0,
completed=0,
total=0,
status='no_eligible_students',
students_count=0,
eligible_students_count=0,
total_students_in_class=len(current_students)
)
# Calcul basé uniquement sur les élèves éligibles
eligible_student_ids = [s.id for s in eligible_students]
# Requête optimisée pour compter les notes des élèves éligibles uniquement
grading_elements_data = self._get_grading_elements_for_eligible_students(
assessment.id, eligible_student_ids
)
total_elements = 0
completed_elements = 0
for element_data in grading_elements_data:
total_elements += len(eligible_students)
completed_elements += element_data.completed_grades_count
if total_elements == 0:
return TemporalProgressResult(
percentage=0,
completed=0,
total=0,
status='no_elements',
students_count=len(eligible_students),
eligible_students_count=len(eligible_students),
total_students_in_class=len(current_students)
)
percentage = round((completed_elements / total_elements) * 100)
status = self._determine_status(percentage)
return TemporalProgressResult(
percentage=percentage,
completed=completed_elements,
total=total_elements,
status=status,
students_count=len(eligible_students),
eligible_students_count=len(eligible_students),
total_students_in_class=len(current_students)
)
def _get_grading_elements_for_eligible_students(self, assessment_id: int, eligible_student_ids: List[int]):
"""Requête optimisée pour récupérer les notes des élèves éligibles."""
return db.session.query(
GradingElement.id,
func.count(Grade.id).label('completed_grades_count')
).select_from(GradingElement)\
.join(Exercise, GradingElement.exercise_id == Exercise.id)\
.outerjoin(
Grade,
and_(
Grade.grading_element_id == GradingElement.id,
Grade.student_id.in_(eligible_student_ids),
Grade.value.isnot(None),
Grade.value != ''
)
)\
.filter(Exercise.assessment_id == assessment_id)\
.group_by(GradingElement.id)\
.all()
class TemporalStudentScoreCalculator(StudentScoreCalculator):
"""Calculateur de scores avec logique temporelle."""
def __init__(self,
grading_calculator,
db_provider: SQLAlchemyDatabaseProvider,
student_repo: TemporalStudentRepository):
super().__init__(grading_calculator, db_provider)
self.student_repo = student_repo
def calculate_student_scores(self, assessment) -> Tuple[Dict[int, StudentScore], Dict[int, Dict[int, float]]]:
"""
Calcule les scores uniquement pour les élèves éligibles.
"""
# Récupérer uniquement les élèves éligibles
eligible_students = self.student_repo.find_eligible_for_assessment(assessment)
if not eligible_students:
return {}, {}
# Requête optimisée pour les notes des élèves éligibles
eligible_student_ids = [s.id for s in eligible_students]
grades_data = self._get_grades_for_eligible_students(assessment.id, eligible_student_ids)
students_scores = {}
exercise_scores = defaultdict(lambda: defaultdict(float))
# Calcul pour chaque élève éligible
for student in eligible_students:
student_score = self._calculate_single_student_score(
student, assessment, grades_data
)
students_scores[student.id] = student_score
for exercise_id, exercise_data in student_score.exercises.items():
exercise_scores[exercise_id][student.id] = exercise_data['score']
return students_scores, dict(exercise_scores)
def _get_grades_for_eligible_students(self, assessment_id: int, eligible_student_ids: List[int]):
"""Requête optimisée pour les notes des élèves éligibles."""
grades = db.session.query(Grade)\
.join(GradingElement, Grade.grading_element_id == GradingElement.id)\
.join(Exercise, GradingElement.exercise_id == Exercise.id)\
.filter(
Exercise.assessment_id == assessment_id,
Grade.student_id.in_(eligible_student_ids)
).all()
# Convertir en format dict compatible avec le service parent
return [
{
'student_id': grade.student_id,
'grading_element_id': grade.grading_element_id,
'value': grade.value,
'grading_type': grade.grading_element.grading_type,
'max_points': grade.grading_element.max_points
}
for grade in grades
]
class TemporalClassStatisticsService:
"""Service pour calculer les statistiques de classe avec logique temporelle."""
def __init__(self, student_repo: TemporalStudentRepository):
self.student_repo = student_repo
def get_class_enrollment_summary(self, class_group_id: int, trimester: int = None):
"""Résumé des inscriptions pour une classe."""
current_students = self.student_repo.find_current_students_in_class(class_group_id)
# Calcul des mouvements si on a un trimestre spécifique
movements = {}
if trimester:
from datetime import date
# Approximation des dates de trimestre
trimester_dates = {
1: (date(2024, 9, 1), date(2024, 12, 31)),
2: (date(2025, 1, 1), date(2025, 3, 31)),
3: (date(2025, 4, 1), date(2025, 6, 30))
}
if trimester in trimester_dates:
start_date, end_date = trimester_dates[trimester]
students_with_movements = self.student_repo.find_students_with_movements_in_period(
start_date, end_date
)
movements = {
'arrivals': [],
'departures': [],
'total_movements': len(students_with_movements)
}
return {
'current_count': len(current_students),
'current_students': current_students,
'movements': movements
}

View File

@@ -0,0 +1,70 @@
from typing import List
from models import Student
from repositories.temporal_student_repository import TemporalStudentRepository
from services.temporal_assessment_services import (
TemporalAssessmentProgressService,
TemporalStudentScoreCalculator,
TemporalClassStatisticsService,
TemporalProgressResult
)
from services.assessment_services import AssessmentStatisticsService, UnifiedGradingCalculator
from providers.concrete_providers import ConfigManagerProvider, SQLAlchemyDatabaseProvider
class TemporalAssessmentServicesFacade:
"""
Facade temporelle qui remplace la facade standard.
Intègre toute la logique temporelle de manière transparente.
"""
def __init__(self,
config_provider = None,
db_provider = None):
# Injection des repositories temporels
self.student_repo = TemporalStudentRepository()
# Providers par défaut si non fournis
if config_provider is None:
config_provider = ConfigManagerProvider()
if db_provider is None:
db_provider = SQLAlchemyDatabaseProvider()
# Services temporels
self.grading_calculator = UnifiedGradingCalculator(config_provider)
self.progress_service = TemporalAssessmentProgressService(db_provider, self.student_repo)
self.score_calculator = TemporalStudentScoreCalculator(
self.grading_calculator, db_provider, self.student_repo
)
self.statistics_service = AssessmentStatisticsService(self.score_calculator)
self.class_statistics_service = TemporalClassStatisticsService(self.student_repo)
def get_eligible_students(self, assessment) -> List[Student]:
"""Nouvelle méthode pour récupérer les élèves éligibles."""
return self.student_repo.find_eligible_for_assessment(assessment)
def get_grading_progress(self, assessment) -> TemporalProgressResult:
"""Progression avec logique temporelle."""
return self.progress_service.calculate_grading_progress(assessment)
def get_class_enrollment_summary(self, class_group_id: int, trimester: int = None):
"""Résumé des inscriptions pour une classe."""
return self.class_statistics_service.get_class_enrollment_summary(class_group_id, trimester)
# Compatibilité avec l'interface existante
def get_assessment_statistics(self, assessment):
"""Statistiques d'évaluation (héritée du service existant)."""
return self.statistics_service.get_assessment_statistics(assessment)
class TemporalAssessmentServicesFactory:
"""Factory pour créer les services temporels."""
@staticmethod
def create_temporal_facade(config_provider = None) -> TemporalAssessmentServicesFacade:
"""Crée une facade temporelle."""
return TemporalAssessmentServicesFacade(config_provider)
@staticmethod
def create_temporal_student_repository() -> TemporalStudentRepository:
"""Crée un repository d'élèves temporel."""
return TemporalStudentRepository()