feat: add temporal student gestion
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
166
repositories/temporal_student_repository.py
Normal file
166
repositories/temporal_student_repository.py
Normal 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
|
||||
Reference in New Issue
Block a user