336 lines
14 KiB
Python
336 lines
14 KiB
Python
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/<int:student_id>')
|
|
@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/<int:assessment_id>')
|
|
@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')) |