from typing import List, Optional, Dict, Tuple from sqlalchemy.orm import joinedload, selectinload from sqlalchemy import and_ from models import ClassGroup, Student, Assessment, Exercise, GradingElement, Grade, Domain from .base_repository import BaseRepository class ClassRepository(BaseRepository[ClassGroup]): """Repository pour les classes (ClassGroup).""" def __init__(self): super().__init__(ClassGroup) def get_or_404(self, id: int) -> ClassGroup: """ Récupère une classe ou lève une erreur 404. Args: id: Identifiant de la classe Returns: ClassGroup: La classe trouvée Raises: 404: Si la classe n'existe pas """ return ClassGroup.query.get_or_404(id) def find_by_name(self, name: str) -> Optional[ClassGroup]: """ Trouve une classe par son nom. Args: name: Nom de la classe à rechercher Returns: Optional[ClassGroup]: La classe trouvée ou None """ return ClassGroup.query.filter_by(name=name).first() def exists_by_name(self, name: str, exclude_id: Optional[int] = None) -> bool: """ Vérifie si une classe avec ce nom existe déjà. Args: name: Nom à vérifier exclude_id: ID de classe à exclure de la recherche (pour la modification) Returns: bool: True si une classe avec ce nom existe """ query = ClassGroup.query.filter_by(name=name) if exclude_id is not None: query = query.filter(ClassGroup.id != exclude_id) return query.first() is not None def find_all_ordered(self, order_by: str = 'year_name') -> List[ClassGroup]: """ Trouve toutes les classes triées selon le critère spécifié. Args: order_by: Critère de tri ('year_name', 'name', 'year') Returns: List[ClassGroup]: Liste des classes triées """ query = ClassGroup.query if order_by == 'year_name': query = query.order_by(ClassGroup.year, ClassGroup.name) elif order_by == 'name': query = query.order_by(ClassGroup.name) elif order_by == 'year': query = query.order_by(ClassGroup.year) else: # Défaut : tri par nom query = query.order_by(ClassGroup.name) return query.all() def count_all(self) -> int: """ Compte le nombre total de classes. Returns: int: Nombre de classes """ return ClassGroup.query.count() def can_be_deleted(self, id: int) -> Tuple[bool, Dict[str, int]]: """ Vérifie si une classe peut être supprimée et retourne les dépendances. Args: id: Identifiant de la classe Returns: Tuple[bool, Dict[str, int]]: (peut_être_supprimée, statistiques_dépendances) """ students_count = Student.query.filter_by(class_group_id=id).count() assessments_count = Assessment.query.filter_by(class_group_id=id).count() dependencies = { 'students': students_count, 'assessments': assessments_count } can_delete = students_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. Args: id: Identifiant de la classe Returns: Optional[ClassGroup]: La classe avec étudiants triés ou None """ class_group = ClassGroup.query.get(id) 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() # Assigner les étudiants triés à la classe class_group._students_ordered = students return class_group def find_with_recent_assessments(self, id: int, limit: int = 5) -> Optional[ClassGroup]: """ Trouve une classe avec ses évaluations récentes. Args: id: Identifiant de la classe limit: Nombre maximum d'évaluations à récupérer Returns: Optional[ClassGroup]: La classe avec évaluations récentes ou None """ class_group = ClassGroup.query.get(id) if not class_group: return None # Charger les évaluations récentes recent_assessments = Assessment.query.filter_by(class_group_id=id).order_by( Assessment.date.desc() ).limit(limit).all() # Assigner les évaluations récentes à la classe class_group._recent_assessments = recent_assessments return class_group def find_with_full_details(self, id: int) -> Optional[ClassGroup]: """ Trouve une classe avec tous ses détails (étudiants et évaluations). Args: id: Identifiant de la classe Returns: Optional[ClassGroup]: La classe avec tous ses détails ou None """ return ClassGroup.query.options( joinedload(ClassGroup.students), joinedload(ClassGroup.assessments) ).filter_by(id=id).first() def get_students_count(self, id: int) -> int: """ Compte le nombre d'étudiants dans une classe. Args: id: Identifiant de la classe Returns: int: Nombre d'étudiants """ return Student.query.filter_by(class_group_id=id).count() def get_assessments_count(self, id: int) -> int: """ Compte le nombre d'évaluations dans une classe. Args: id: Identifiant de la classe Returns: int: Nombre d'évaluations """ return Assessment.query.filter_by(class_group_id=id).count() def find_for_form_choices(self) -> List[ClassGroup]: """ Trouve toutes les classes pour les choix de formulaires. Optimisé pour n'inclure que les champs nécessaires. Returns: List[ClassGroup]: Liste des classes triées par nom """ 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