from flask import Blueprint, render_template, request, jsonify, flash, redirect, url_for, current_app from datetime import date, datetime from models import db, Student, ClassGroup, StudentEnrollment from repositories.temporal_student_repository import TemporalStudentRepository from utils import handle_db_errors bp = Blueprint('student_movements', __name__, url_prefix='/movements') @bp.route('/') @handle_db_errors def movements_index(): """Page principale de gestion des mouvements d'élèves.""" temporal_repo = TemporalStudentRepository() # Récupérer les mouvements récents (30 derniers jours) from datetime import date, timedelta recent_start = date.today() - timedelta(days=30) recent_end = date.today() recent_movements = temporal_repo.find_students_with_movements_in_period(recent_start, recent_end) # Récupérer toutes les classes pour les formulaires classes = ClassGroup.query.order_by(ClassGroup.name).all() # Récupérer tous les élèves pour le formulaire d'inscription all_students = Student.query.order_by(Student.last_name, Student.first_name).all() return render_template('student_movements.html', recent_movements=recent_movements, classes=classes, all_students=all_students) @bp.route('/enroll', methods=['POST']) @handle_db_errors def enroll_student(): """Inscrire un élève dans une classe.""" temporal_repo = TemporalStudentRepository() try: data = request.get_json() if request.is_json else request.form student_id = int(data.get('student_id')) class_group_id = int(data.get('class_group_id')) enrollment_date_str = data.get('enrollment_date', str(date.today())) enrollment_reason = data.get('enrollment_reason', '') # Validation de la date enrollment_date = datetime.strptime(enrollment_date_str, '%Y-%m-%d').date() # Vérifier que l'élève et la classe existent student = Student.query.get_or_404(student_id) class_group = ClassGroup.query.get_or_404(class_group_id) # Créer l'inscription enrollment = temporal_repo.create_enrollment( student_id, class_group_id, enrollment_date, enrollment_reason ) db.session.commit() current_app.logger.info(f'Inscription créée: Élève {student.full_name} en {class_group.name}') if request.is_json: return jsonify({ 'success': True, 'message': f'Élève {student.full_name} inscrit en {class_group.name}', 'enrollment_id': enrollment.id }) else: flash(f'Élève {student.full_name} inscrit en {class_group.name}', 'success') return redirect(url_for('student_movements.movements_index')) except ValueError as e: error_msg = str(e) current_app.logger.warning(f'Erreur inscription élève: {error_msg}') if request.is_json: return jsonify({'success': False, 'error': error_msg}), 400 else: flash(error_msg, 'error') return redirect(url_for('student_movements.movements_index')) except Exception as e: db.session.rollback() current_app.logger.error(f'Erreur inscription élève: {e}') if request.is_json: return jsonify({'success': False, 'error': 'Erreur lors de l\'inscription'}), 500 else: flash('Erreur lors de l\'inscription', 'error') return redirect(url_for('student_movements.movements_index')) @bp.route('/departure', methods=['POST']) @handle_db_errors def student_departure(): """Enregistrer le départ d'un élève.""" temporal_repo = TemporalStudentRepository() try: data = request.get_json() if request.is_json else request.form student_id = int(data.get('student_id')) departure_date_str = data.get('departure_date', str(date.today())) departure_reason = data.get('departure_reason', '') # Validation de la date departure_date = datetime.strptime(departure_date_str, '%Y-%m-%d').date() # Vérifier que l'élève existe student = Student.query.get_or_404(student_id) # Terminer l'inscription active enrollment = temporal_repo.end_enrollment( student_id, departure_date, departure_reason ) if not enrollment: raise ValueError("Aucune inscription active trouvée pour cet élève") db.session.commit() current_app.logger.info(f'Départ enregistré: Élève {student.full_name}') if request.is_json: return jsonify({ 'success': True, 'message': f'Départ de {student.full_name} enregistré', 'enrollment_id': enrollment.id }) else: flash(f'Départ de {student.full_name} enregistré', 'success') return redirect(url_for('student_movements.movements_index')) except ValueError as e: error_msg = str(e) current_app.logger.warning(f'Erreur départ élève: {error_msg}') if request.is_json: return jsonify({'success': False, 'error': error_msg}), 400 else: flash(error_msg, 'error') return redirect(url_for('student_movements.movements_index')) except Exception as e: db.session.rollback() current_app.logger.error(f'Erreur départ élève: {e}') if request.is_json: return jsonify({'success': False, 'error': 'Erreur lors de l\'enregistrement du départ'}), 500 else: flash('Erreur lors de l\'enregistrement du départ', 'error') return redirect(url_for('student_movements.movements_index')) @bp.route('/transfer', methods=['POST']) @handle_db_errors def transfer_student(): """Transférer un élève d'une classe à une autre.""" temporal_repo = TemporalStudentRepository() try: data = request.get_json() if request.is_json else request.form student_id = int(data.get('student_id')) new_class_group_id = int(data.get('new_class_group_id')) transfer_date_str = data.get('transfer_date', str(date.today())) transfer_reason = data.get('transfer_reason', '') # Validation de la date transfer_date = datetime.strptime(transfer_date_str, '%Y-%m-%d').date() # Vérifier que l'élève et les classes existent student = Student.query.get_or_404(student_id) new_class_group = ClassGroup.query.get_or_404(new_class_group_id) # Effectuer le transfert old_enrollment, new_enrollment = temporal_repo.transfer_student( student_id, new_class_group_id, transfer_date, transfer_reason ) db.session.commit() current_app.logger.info(f'Transfert effectué: Élève {student.full_name} vers {new_class_group.name}') if request.is_json: return jsonify({ 'success': True, 'message': f'Élève {student.full_name} transféré vers {new_class_group.name}', 'old_enrollment_id': old_enrollment.id, 'new_enrollment_id': new_enrollment.id }) else: flash(f'Élève {student.full_name} transféré vers {new_class_group.name}', 'success') return redirect(url_for('student_movements.movements_index')) except ValueError as e: error_msg = str(e) current_app.logger.warning(f'Erreur transfert élève: {error_msg}') if request.is_json: return jsonify({'success': False, 'error': error_msg}), 400 else: flash(error_msg, 'error') return redirect(url_for('student_movements.movements_index')) except Exception as e: db.session.rollback() current_app.logger.error(f'Erreur transfert élève: {e}') if request.is_json: return jsonify({'success': False, 'error': 'Erreur lors du transfert'}), 500 else: flash('Erreur lors du transfert', 'error') return redirect(url_for('student_movements.movements_index')) @bp.route('/history/') @handle_db_errors def student_history(student_id): """Afficher l'historique des inscriptions d'un élève.""" temporal_repo = TemporalStudentRepository() student = Student.query.get_or_404(student_id) enrollment_history = temporal_repo.get_enrollment_history(student_id) if request.is_json: history_data = [] for enrollment in enrollment_history: history_data.append({ 'id': enrollment.id, 'class_name': enrollment.class_group.name, 'enrollment_date': enrollment.enrollment_date.isoformat(), 'departure_date': enrollment.departure_date.isoformat() if enrollment.departure_date else None, 'enrollment_reason': enrollment.enrollment_reason, 'departure_reason': enrollment.departure_reason, 'is_active': enrollment.is_active }) return jsonify({ 'student': { 'id': student.id, 'name': student.full_name }, 'history': history_data }) else: return render_template('student_history.html', student=student, enrollment_history=enrollment_history) @bp.route('/api/eligible-students/') @handle_db_errors def api_eligible_students(assessment_id): """API pour récupérer les élèves éligibles pour une évaluation.""" from models import Assessment temporal_repo = TemporalStudentRepository() assessment = Assessment.query.get_or_404(assessment_id) eligible_students = temporal_repo.find_eligible_for_assessment(assessment) current_students = temporal_repo.find_current_students_in_class(assessment.class_group_id) eligible_data = [] for student in eligible_students: eligible_data.append({ 'id': student.id, 'name': student.full_name, 'email': student.email, 'eligible': True }) # Ajouter les élèves non-éligibles pour comparaison ineligible_students = [s for s in current_students if s not in eligible_students] for student in ineligible_students: eligible_data.append({ 'id': student.id, 'name': student.full_name, 'email': student.email, 'eligible': False }) return jsonify({ 'assessment': { 'id': assessment.id, 'title': assessment.title, 'date': assessment.date.isoformat() if assessment.date else None }, 'total_eligible': len(eligible_students), 'total_current': len(current_students), 'students': eligible_data }) @bp.route('/cancel-departure', methods=['POST']) @handle_db_errors def cancel_departure(): """Annuler le départ d'un élève (remettre inscription active).""" try: data = request.get_json() if request.is_json else request.form enrollment_id = int(data.get('enrollment_id')) # Récupérer l'inscription enrollment = StudentEnrollment.query.get_or_404(enrollment_id) # Vérifier que l'inscription a bien une date de départ if not enrollment.departure_date: flash('Cette inscription n\'a pas de date de départ à annuler', 'error') return redirect(request.referrer or url_for('student_movements.movements_index')) # Vérifier qu'il n'y a pas déjà une inscription active pour cet élève active_enrollment = StudentEnrollment.query.filter_by( student_id=enrollment.student_id, departure_date=None ).first() if active_enrollment: flash(f'L\'élève {enrollment.student.first_name} {enrollment.student.last_name} est déjà inscrit dans {active_enrollment.class_group.name}', 'error') return redirect(request.referrer or url_for('student_movements.movements_index')) # Annuler le départ en supprimant la date de départ old_departure_date = enrollment.departure_date enrollment.departure_date = None enrollment.departure_reason = None db.session.commit() current_app.logger.info(f'Départ annulé - Élève {enrollment.student_id} réintégré en {enrollment.class_group.name}') flash(f'Départ de {enrollment.student.first_name} {enrollment.student.last_name} annulé. Élève réintégré en {enrollment.class_group.name}', 'success') # Rediriger vers la page d'origine (page élèves de classe si disponible) return redirect(request.referrer or url_for('student_movements.movements_index')) except (ValueError, TypeError) as e: current_app.logger.error(f'Erreur données annulation départ: {e}') flash('Données d\'annulation invalides', 'error') return redirect(request.referrer or url_for('student_movements.movements_index')) except Exception as e: db.session.rollback() current_app.logger.error(f'Erreur annulation départ: {e}') flash('Erreur lors de l\'annulation du départ', 'error') return redirect(request.referrer or url_for('student_movements.movements_index'))