Feat: add class page

This commit is contained in:
2025-08-09 11:32:36 +02:00
parent 17995f913e
commit 0e87a457af
18 changed files with 6974 additions and 14 deletions

View File

@@ -1,7 +1,7 @@
from typing import List, Optional, Dict, Tuple
from sqlalchemy.orm import joinedload
from sqlalchemy.orm import joinedload, selectinload
from sqlalchemy import and_
from models import ClassGroup, Student, Assessment
from models import ClassGroup, Student, Assessment, Exercise, GradingElement, Grade, Domain
from .base_repository import BaseRepository
@@ -207,4 +207,140 @@ class ClassRepository(BaseRepository[ClassGroup]):
Returns:
List[ClassGroup]: Liste des classes triées par nom
"""
return ClassGroup.query.order_by(ClassGroup.name).all()
return ClassGroup.query.order_by(ClassGroup.name).all()
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.
Args:
class_id: Identifiant de la classe
trimester: Trimestre à filtrer (1, 2, 3) ou None pour tous
Returns:
Optional[ClassGroup]: La classe avec toutes ses données ou None
"""
try:
# Construire la requête avec toutes les jointures optimisées
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:
if trimester is not None:
class_group._filtered_assessments = [
assessment for assessment in class_group.assessments
if assessment.trimester == trimester
]
else:
# Pour le mode global, on garde toutes les évaluations
class_group._filtered_assessments = class_group.assessments
return class_group
except Exception as e:
# Log l'erreur (utilisera le système de logging structuré)
from flask import current_app
current_app.logger.error(
f"Erreur lors de la récupération de la classe {class_id} avec statistiques: {e}",
extra={'class_id': class_id, 'trimester': trimester}
)
return None
def get_assessments_by_trimester(self, class_id: int, trimester: Optional[int] = None) -> List[Assessment]:
"""
Récupère les évaluations d'une classe filtrées par trimestre.
Optimise le chargement pour les calculs de progression.
Args:
class_id: Identifiant de la classe
trimester: Trimestre à filtrer (1, 2, 3) ou None pour toutes
Returns:
List[Assessment]: Liste des évaluations triées par date décroissante
"""
try:
# Requête optimisée avec préchargement des relations pour grading_progress
query = Assessment.query.options(
selectinload(Assessment.exercises).selectinload(Exercise.grading_elements)
.selectinload(GradingElement.grades)
).filter_by(class_group_id=class_id)
# Filtrage par trimestre si spécifié
if trimester is not None:
query = query.filter(Assessment.trimester == trimester)
# Tri par date décroissante (plus récentes d'abord)
assessments = query.order_by(Assessment.date.desc()).all()
return assessments
except Exception as e:
from flask import current_app
current_app.logger.error(
f"Erreur lors de la récupération des évaluations pour la classe {class_id}: {e}",
extra={'class_id': class_id, 'trimester': trimester}
)
return []
def find_with_assessments_optimized(self, class_id: int, trimester: Optional[int] = None) -> Optional[ClassGroup]:
"""
Version optimisée pour la page dashboard avec préchargement intelligent.
Cette méthode évite les requêtes multiples pour les calculs de grading_progress.
Args:
class_id: Identifiant de la classe
trimester: Trimestre à filtrer (1, 2, 3) ou None pour tous
Returns:
Optional[ClassGroup]: La classe avec ses évaluations optimisées ou None
"""
try:
# Single-query avec toutes les relations nécessaires
base_query = ClassGroup.query.options(
joinedload(ClassGroup.students),
selectinload(ClassGroup.assessments).selectinload(Assessment.exercises)
.selectinload(Exercise.grading_elements).selectinload(GradingElement.grades)
)
class_group = base_query.filter_by(id=class_id).first()
if not class_group:
return None
# Pré-filtrer les évaluations par trimestre
if trimester is not None:
filtered_assessments = [
assessment for assessment in class_group.assessments
if assessment.trimester == trimester
]
# Stocker les évaluations filtrées pour éviter les recalculs
class_group._filtered_assessments = sorted(
filtered_assessments,
key=lambda x: x.date,
reverse=True
)
else:
# Trier toutes les évaluations par date décroissante
class_group._filtered_assessments = sorted(
class_group.assessments,
key=lambda x: x.date,
reverse=True
)
return class_group
except Exception as e:
from flask import current_app
current_app.logger.error(
f"Erreur lors de la récupération optimisée de la classe {class_id}: {e}",
extra={'class_id': class_id, 'trimester': trimester}
)
return None