from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify, current_app, abort from models import db, ClassGroup, Student, Assessment from forms import ClassGroupForm from utils import handle_db_errors, ValidationError from repositories.class_repository import ClassRepository bp = Blueprint('classes', __name__, url_prefix='/classes') @bp.route('/new') @handle_db_errors def new(): """Formulaire de création d'une nouvelle classe.""" form = ClassGroupForm() return render_template('class_form.html', form=form, title="Créer une nouvelle classe", is_edit=False) @bp.route('/', methods=['POST']) @handle_db_errors def create(): """Traitement de la création d'une classe.""" form = ClassGroupForm() class_repo = ClassRepository() if form.validate_on_submit(): try: # Vérification d'unicité du nom de classe if class_repo.exists_by_name(form.name.data): flash('Une classe avec ce nom existe déjà.', 'error') return render_template('class_form.html', form=form, title="Créer une nouvelle classe", is_edit=False) # Création de la nouvelle classe class_group = ClassGroup( name=form.name.data, description=form.description.data, year=form.year.data ) db.session.add(class_group) db.session.commit() current_app.logger.info(f'Nouvelle classe créée: {class_group.name}') flash(f'Classe "{class_group.name}" créée avec succès !', 'success') return redirect(url_for('classes')) except Exception as e: db.session.rollback() current_app.logger.error(f'Erreur lors de la création de la classe: {e}') flash('Erreur lors de la création de la classe.', 'error') return render_template('class_form.html', form=form, title="Créer une nouvelle classe", is_edit=False) @bp.route('//edit') @handle_db_errors def edit(id): """Formulaire de modification d'une classe.""" class_repo = ClassRepository() class_group = class_repo.get_or_404(id) form = ClassGroupForm(obj=class_group) return render_template('class_form.html', form=form, class_group=class_group, title=f"Modifier la classe {class_group.name}", is_edit=True) @bp.route('/', methods=['POST']) @handle_db_errors def update(id): """Traitement de la modification d'une classe.""" class_repo = ClassRepository() class_group = class_repo.get_or_404(id) form = ClassGroupForm() if form.validate_on_submit(): try: # Vérification d'unicité du nom (sauf si c'est le même nom) if class_repo.exists_by_name(form.name.data, exclude_id=id): flash('Une autre classe avec ce nom existe déjà.', 'error') return render_template('class_form.html', form=form, class_group=class_group, title=f"Modifier la classe {class_group.name}", is_edit=True) # Mise à jour des données class_group.name = form.name.data class_group.description = form.description.data class_group.year = form.year.data db.session.commit() current_app.logger.info(f'Classe modifiée: {class_group.name}') flash(f'Classe "{class_group.name}" modifiée avec succès !', 'success') return redirect(url_for('classes')) except Exception as e: db.session.rollback() current_app.logger.error(f'Erreur lors de la modification de la classe: {e}') flash('Erreur lors de la modification de la classe.', 'error') return render_template('class_form.html', form=form, class_group=class_group, title=f"Modifier la classe {class_group.name}", is_edit=True) @bp.route('//delete', methods=['POST']) @handle_db_errors def delete(id): """Suppression d'une classe avec vérifications.""" class_repo = ClassRepository() class_group = class_repo.get_or_404(id) try: # Vérifier s'il y a des étudiants ou des évaluations liés can_delete, dependencies = class_repo.can_be_deleted(id) if not can_delete: students_count = dependencies['students'] assessments_count = dependencies['assessments'] flash( f'Impossible de supprimer la classe "{class_group.name}". ' f'Elle contient {students_count} élève(s) et {assessments_count} évaluation(s). ' f'Supprimez d\'abord ces éléments.', 'error' ) return redirect(url_for('classes')) # Suppression de la classe db.session.delete(class_group) db.session.commit() current_app.logger.info(f'Classe supprimée: {class_group.name}') flash(f'Classe "{class_group.name}" supprimée avec succès.', 'success') except Exception as e: db.session.rollback() current_app.logger.error(f'Erreur lors de la suppression de la classe: {e}') flash('Erreur lors de la suppression de la classe.', 'error') return redirect(url_for('classes')) @bp.route('/', methods=['GET']) @handle_db_errors def details(id): """Redirection transparente vers le dashboard de classe.""" return redirect(url_for('classes.dashboard', id=id)) @bp.route('//dashboard') @handle_db_errors def dashboard(id): """Page de présentation de classe avec statistiques.""" # Récupération paramètre trimestre trimester = request.args.get('trimestre', type=int) if trimester and trimester not in [1, 2, 3]: trimester = None # Repository optimisé class_repo = ClassRepository() class_group = class_repo.find_with_statistics(id, trimester) if not class_group: abort(404) current_app.logger.debug(f'Dashboard classe {id} affiché pour trimestre {trimester}') return render_template('class_dashboard.html', class_group=class_group, selected_trimester=trimester) @bp.route('//stats') @handle_db_errors def get_stats_api(id): """API JSON pour statistiques dynamiques (AJAX).""" # Récupération paramètre trimestre trimester = request.args.get('trimestre', type=int) if trimester and trimester not in [1, 2, 3]: trimester = None # Repository optimisé class_repo = ClassRepository() class_group = class_repo.find_with_statistics(id, trimester) if not class_group: abort(404) try: # Construction de la réponse JSON avec les nouvelles méthodes du modèle quantity_stats = class_group.get_trimester_statistics(trimester) domain_analysis = class_group.get_domain_analysis(trimester) competence_analysis = class_group.get_competence_analysis(trimester) class_results = class_group.get_class_results(trimester) # Compter le nombre d'évaluations selon le trimestre if hasattr(class_group, '_filtered_assessments'): assessments_count = len(class_group._filtered_assessments) current_app.logger.debug(f'Assessments count from _filtered_assessments: {assessments_count}') else: # Fallback si _filtered_assessments n'existe pas if trimester: assessments_count = len([a for a in class_group.assessments if a.trimester == trimester]) else: assessments_count = len(class_group.assessments) current_app.logger.debug(f'Assessments count from fallback: {assessments_count}') current_app.logger.debug(f'Final assessments_count value: {assessments_count}, type: {type(assessments_count)}') stats = { "quantity": { "total": quantity_stats["total"], "completed": quantity_stats["completed"], "in_progress": quantity_stats["in_progress"], "not_started": quantity_stats["not_started"] }, "domains": domain_analysis["domains"], # Extraire directement le tableau "competences": competence_analysis["competences"], # Extraire directement le tableau "results": { "average": class_results["overall_statistics"]["mean"], "min": class_results["overall_statistics"]["min"], "max": class_results["overall_statistics"]["max"], "median": class_results["overall_statistics"]["median"], "std_dev": class_results["overall_statistics"]["std_dev"], "assessments_count": assessments_count } } current_app.logger.debug(f'Statistiques API générées pour classe {id}, trimestre {trimester}') return jsonify(stats) except Exception as e: current_app.logger.error(f'Erreur génération statistiques API classe {id}: {e}') return jsonify({"error": "Erreur lors de la génération des statistiques"}), 500 @bp.route('//details') @handle_db_errors def details_legacy(id): """Page de détail d'une classe avec ses étudiants et évaluations (legacy).""" class_repo = ClassRepository() class_group = class_repo.find_with_full_details(id) if not class_group: abort(404) # Trier les étudiants par nom (optimisé en Python car déjà chargés) students = sorted(class_group.students, key=lambda s: (s.last_name, s.first_name)) # Prendre les 5 évaluations les plus récentes (optimisé en Python car déjà chargées) recent_assessments = sorted(class_group.assessments, key=lambda a: a.date, reverse=True)[:5] return render_template('class_details.html', class_group=class_group, students=students, recent_assessments=recent_assessments)