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

@@ -71,10 +71,12 @@ class AppreciationRepository(BaseRepository[CouncilAppreciation]):
def get_completion_stats(self, class_group_id: int, trimester: int) -> dict:
"""Statistiques de completion des appréciations pour une classe/trimestre."""
from models import Student
from repositories.temporal_student_repository import TemporalStudentRepository
# Nombre total d'élèves dans la classe
total_students = Student.query.filter_by(class_group_id=class_group_id).count()
# Nombre total d'élèves actuellement dans la classe
temporal_repo = TemporalStudentRepository()
current_students = temporal_repo.find_current_students_in_class(class_group_id)
total_students = len(current_students)
# Nombre d'appréciations existantes
total_appreciations = CouncilAppreciation.query.filter_by(

View File

@@ -99,21 +99,29 @@ class ClassRepository(BaseRepository[ClassGroup]):
Returns:
Tuple[bool, Dict[str, int]]: (peut_être_supprimée, statistiques_dépendances)
"""
students_count = Student.query.filter_by(class_group_id=id).count()
from repositories.temporal_student_repository import TemporalStudentRepository
from models import StudentEnrollment
# Compter les élèves actuellement inscrits ou ayant été inscrits
temporal_repo = TemporalStudentRepository()
current_students = temporal_repo.find_current_students_in_class(id)
enrollments_count = StudentEnrollment.query.filter_by(class_group_id=id).count()
assessments_count = Assessment.query.filter_by(class_group_id=id).count()
dependencies = {
'students': students_count,
'students': len(current_students),
'enrollments': enrollments_count,
'assessments': assessments_count
}
can_delete = students_count == 0 and assessments_count == 0
# Une classe peut être supprimée s'il n'y a aucune inscription historique ni évaluation
can_delete = enrollments_count == 0 and assessments_count == 0
return can_delete, dependencies
def find_with_students_ordered(self, id: int) -> Optional[ClassGroup]:
"""
Trouve une classe avec ses étudiants triés par nom.
Trouve une classe avec ses étudiants actuels triés par nom.
Args:
id: Identifiant de la classe
@@ -125,10 +133,10 @@ class ClassRepository(BaseRepository[ClassGroup]):
if not class_group:
return None
# Charger les étudiants triés
students = Student.query.filter_by(class_group_id=id).order_by(
Student.last_name, Student.first_name
).all()
# Utiliser la logique temporelle pour récupérer les élèves actuels
from repositories.temporal_student_repository import TemporalStudentRepository
temporal_repo = TemporalStudentRepository()
students = temporal_repo.find_current_students_in_class(id)
# Assigner les étudiants triés à la classe
class_group._students_ordered = students
@@ -162,7 +170,7 @@ class ClassRepository(BaseRepository[ClassGroup]):
def find_with_full_details(self, id: int) -> Optional[ClassGroup]:
"""
Trouve une classe avec tous ses détails (étudiants et évaluations).
Trouve une classe avec tous ses détails (étudiants actuels et évaluations).
Args:
id: Identifiant de la classe
@@ -170,22 +178,32 @@ class ClassRepository(BaseRepository[ClassGroup]):
Returns:
Optional[ClassGroup]: La classe avec tous ses détails ou None
"""
return ClassGroup.query.options(
joinedload(ClassGroup.students),
class_group = ClassGroup.query.options(
joinedload(ClassGroup.assessments)
).filter_by(id=id).first()
if class_group:
# Charger manuellement les élèves actuels avec la logique temporelle
from repositories.temporal_student_repository import TemporalStudentRepository
temporal_repo = TemporalStudentRepository()
class_group._current_students = temporal_repo.find_current_students_in_class(id)
return class_group
def get_students_count(self, id: int) -> int:
"""
Compte le nombre d'étudiants dans une classe.
Compte le nombre d'étudiants actuellement dans une classe.
Args:
id: Identifiant de la classe
Returns:
int: Nombre d'étudiants
int: Nombre d'étudiants actuels
"""
return Student.query.filter_by(class_group_id=id).count()
from repositories.temporal_student_repository import TemporalStudentRepository
temporal_repo = TemporalStudentRepository()
current_students = temporal_repo.find_current_students_in_class(id)
return len(current_students)
def get_assessments_count(self, id: int) -> int:
"""
@@ -212,8 +230,7 @@ class ClassRepository(BaseRepository[ClassGroup]):
def find_with_statistics(self, class_id: int, trimester: Optional[int] = None) -> Optional[ClassGroup]:
"""
Récupère une classe avec toutes les données nécessaires pour les statistiques.
Optimise les requêtes pour éviter les problèmes N+1 en chargeant toutes les relations
nécessaires en une seule requête.
Utilise la logique temporelle pour les étudiants et optimise les requêtes.
Args:
class_id: Identifiant de la classe
@@ -223,17 +240,21 @@ class ClassRepository(BaseRepository[ClassGroup]):
Optional[ClassGroup]: La classe avec toutes ses données ou None
"""
try:
# Construire la requête avec toutes les jointures optimisées
# Construire la requête sans la relation students (supprimée du modèle temporel)
query = ClassGroup.query.options(
joinedload(ClassGroup.students),
selectinload(ClassGroup.assessments).selectinload(Assessment.exercises)
.selectinload(Exercise.grading_elements).selectinload(GradingElement.grades)
).filter_by(id=class_id)
class_group = query.first()
# Filtrer les évaluations après récupération pour optimiser les calculs statistiques
if class_group:
# Charger les élèves actuels avec la logique temporelle
from repositories.temporal_student_repository import TemporalStudentRepository
temporal_repo = TemporalStudentRepository()
class_group._current_students = temporal_repo.find_current_students_in_class(class_id)
# Filtrer les évaluations après récupération pour optimiser les calculs statistiques
if trimester is not None:
class_group._filtered_assessments = [
assessment for assessment in class_group.assessments
@@ -303,9 +324,8 @@ class ClassRepository(BaseRepository[ClassGroup]):
Optional[ClassGroup]: La classe avec ses évaluations optimisées ou None
"""
try:
# Single-query avec toutes les relations nécessaires
# Single-query avec toutes les relations nécessaires (sans students)
base_query = ClassGroup.query.options(
joinedload(ClassGroup.students),
selectinload(ClassGroup.assessments).selectinload(Assessment.exercises)
.selectinload(Exercise.grading_elements).selectinload(GradingElement.grades)
)
@@ -315,6 +335,11 @@ class ClassRepository(BaseRepository[ClassGroup]):
if not class_group:
return None
# Charger les élèves actuels avec la logique temporelle
from repositories.temporal_student_repository import TemporalStudentRepository
temporal_repo = TemporalStudentRepository()
class_group._current_students = temporal_repo.find_current_students_in_class(class_id)
# Pré-filtrer les évaluations par trimestre
if trimester is not None:
filtered_assessments = [

View File

@@ -0,0 +1,166 @@
from typing import List, Optional, Tuple
from datetime import date
from sqlalchemy import and_, or_
from models import db, Student, StudentEnrollment, Assessment
from repositories.base_repository import BaseRepository
class TemporalStudentRepository(BaseRepository[Student]):
"""Repository pour gérer les étudiants avec logique temporelle."""
def __init__(self):
super().__init__(Student)
def find_enrolled_in_class_at_date(self, class_group_id: int, check_date: date) -> List[Student]:
"""Trouve les étudiants inscrits dans une classe à une date donnée."""
from sqlalchemy import func
return db.session.query(Student)\
.join(StudentEnrollment)\
.filter(
StudentEnrollment.class_group_id == class_group_id,
StudentEnrollment.enrollment_date <= check_date,
or_(
StudentEnrollment.departure_date.is_(None),
StudentEnrollment.departure_date >= check_date
)
)\
.order_by(func.lower(Student.last_name), func.lower(Student.first_name))\
.all()
def find_eligible_for_assessment(self, assessment: Assessment) -> List[Student]:
"""Trouve les étudiants éligibles pour une évaluation donnée."""
if not assessment.date:
return []
return self.find_enrolled_in_class_at_date(assessment.class_group_id, assessment.date)
def find_current_students_in_class(self, class_group_id: int) -> List[Student]:
"""Trouve les étudiants actuellement inscrits dans une classe."""
from sqlalchemy import func
return db.session.query(Student)\
.join(StudentEnrollment)\
.filter(
StudentEnrollment.class_group_id == class_group_id,
StudentEnrollment.departure_date.is_(None)
)\
.order_by(func.lower(Student.last_name), func.lower(Student.first_name))\
.all()
def get_enrollment_history(self, student_id: int) -> List[StudentEnrollment]:
"""Récupère l'historique complet des inscriptions d'un élève."""
return StudentEnrollment.query\
.filter_by(student_id=student_id)\
.order_by(StudentEnrollment.enrollment_date.desc())\
.all()
def find_students_with_movements_in_period(self, start_date: date, end_date: date) -> List[Tuple[Student, List[StudentEnrollment]]]:
"""Trouve les étudiants qui ont eu des mouvements (arrivée/départ) dans une période avec leurs inscriptions."""
# Récupérer les étudiants qui ont eu des mouvements dans la période
students_with_movements = db.session.query(Student)\
.join(StudentEnrollment)\
.filter(
or_(
# Arrivées dans la période
and_(
StudentEnrollment.enrollment_date >= start_date,
StudentEnrollment.enrollment_date <= end_date
),
# Départs dans la période
and_(
StudentEnrollment.departure_date >= start_date,
StudentEnrollment.departure_date <= end_date
)
)
)\
.distinct()\
.order_by(Student.last_name, Student.first_name)\
.all()
# Pour chaque étudiant, récupérer ses mouvements dans la période
result = []
for student in students_with_movements:
movements = StudentEnrollment.query\
.filter_by(student_id=student.id)\
.filter(
or_(
# Arrivées dans la période
and_(
StudentEnrollment.enrollment_date >= start_date,
StudentEnrollment.enrollment_date <= end_date
),
# Départs dans la période
and_(
StudentEnrollment.departure_date >= start_date,
StudentEnrollment.departure_date <= end_date
)
)
)\
.order_by(StudentEnrollment.enrollment_date.desc())\
.all()
if movements:
result.append((student, movements))
return result
def create_enrollment(self, student_id: int, class_group_id: int,
enrollment_date: date, enrollment_reason: str = None) -> StudentEnrollment:
"""Crée une nouvelle inscription pour un élève."""
# Vérifier s'il y a déjà une inscription active
active_enrollment = StudentEnrollment.query.filter_by(
student_id=student_id,
departure_date=None
).first()
if active_enrollment:
raise ValueError("L'élève a déjà une inscription active")
# Créer la nouvelle inscription
enrollment = StudentEnrollment(
student_id=student_id,
class_group_id=class_group_id,
enrollment_date=enrollment_date,
enrollment_reason=enrollment_reason
)
db.session.add(enrollment)
return enrollment
def end_enrollment(self, student_id: int, departure_date: date,
departure_reason: str = None) -> Optional[StudentEnrollment]:
"""Termine l'inscription active d'un élève."""
active_enrollment = StudentEnrollment.query.filter_by(
student_id=student_id,
departure_date=None
).first()
if not active_enrollment:
return None
active_enrollment.departure_date = departure_date
active_enrollment.departure_reason = departure_reason
return active_enrollment
def transfer_student(self, student_id: int, new_class_group_id: int,
transfer_date: date, transfer_reason: str = None) -> tuple[StudentEnrollment, StudentEnrollment]:
"""Transfère un élève d'une classe à une autre."""
# Terminer l'inscription actuelle
old_enrollment = self.end_enrollment(
student_id,
transfer_date,
f"Transfert: {transfer_reason}" if transfer_reason else "Transfert"
)
if not old_enrollment:
raise ValueError("Aucune inscription active trouvée pour cet élève")
# Créer la nouvelle inscription
new_enrollment = self.create_enrollment(
student_id,
new_class_group_id,
transfer_date,
f"Transfert: {transfer_reason}" if transfer_reason else "Transfert"
)
return old_enrollment, new_enrollment