feat: add temporal student gestion
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify, current_app, abort
|
||||
from models import db, ClassGroup, Student, Assessment
|
||||
from models import db, ClassGroup, Student, Assessment, StudentEnrollment
|
||||
from forms import ClassGroupForm
|
||||
from utils import handle_db_errors, ValidationError
|
||||
from repositories.class_repository import ClassRepository
|
||||
from datetime import date, datetime
|
||||
|
||||
bp = Blueprint('classes', __name__, url_prefix='/classes')
|
||||
|
||||
@@ -320,7 +321,8 @@ def save_appreciation_api(class_id, student_id):
|
||||
# Vérifier que l'élève appartient à cette classe
|
||||
from models import Student
|
||||
student = Student.query.get_or_404(student_id)
|
||||
if student.class_group_id != class_id:
|
||||
current_class = student.get_current_class()
|
||||
if not current_class or current_class.id != class_id:
|
||||
return jsonify({'success': False, 'error': 'Élève non trouvé dans cette classe'}), 403
|
||||
|
||||
# Préparer les données d'appréciation
|
||||
@@ -413,4 +415,363 @@ def council_data_api(class_id):
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(f'Erreur API données conseil classe {class_id}: {e}')
|
||||
return jsonify({'error': 'Erreur lors de la récupération des données'}), 500
|
||||
return jsonify({'error': 'Erreur lors de la récupération des données'}), 500
|
||||
|
||||
@bp.route('/<int:id>/students')
|
||||
@handle_db_errors
|
||||
def students(id):
|
||||
"""Page de gestion des élèves d'une classe."""
|
||||
class_repo = ClassRepository()
|
||||
class_group = class_repo.get_or_404(id)
|
||||
|
||||
try:
|
||||
# Repository temporel pour les élèves
|
||||
from repositories.temporal_student_repository import TemporalStudentRepository
|
||||
temporal_repo = TemporalStudentRepository()
|
||||
|
||||
# Élèves actuellement inscrits
|
||||
current_students = temporal_repo.find_current_students_in_class(id)
|
||||
|
||||
# Historique des mouvements (derniers 6 mois)
|
||||
from datetime import date, timedelta
|
||||
six_months_ago = date.today() - timedelta(days=180)
|
||||
movements = temporal_repo.find_students_with_movements_in_period(six_months_ago, date.today())
|
||||
|
||||
# Filtrer les mouvements de cette classe
|
||||
class_movements = []
|
||||
for student, enrollments in movements:
|
||||
class_enrollments = [e for e in enrollments if e.class_group_id == id]
|
||||
if class_enrollments:
|
||||
class_movements.append((student, class_enrollments))
|
||||
|
||||
# Statistiques d'effectifs
|
||||
total_current = len(current_students)
|
||||
|
||||
# Compter les arrivées et départs récents (30 derniers jours)
|
||||
thirty_days_ago = date.today() - timedelta(days=30)
|
||||
recent_arrivals = 0
|
||||
recent_departures = 0
|
||||
|
||||
for student, enrollments in class_movements:
|
||||
for enrollment in enrollments:
|
||||
if enrollment.enrollment_date and enrollment.enrollment_date >= thirty_days_ago:
|
||||
recent_arrivals += 1
|
||||
if enrollment.departure_date and enrollment.departure_date >= thirty_days_ago:
|
||||
recent_departures += 1
|
||||
|
||||
# Toutes les classes pour les transferts
|
||||
all_classes = ClassRepository().find_all_ordered('name')
|
||||
other_classes = [c for c in all_classes if c.id != id]
|
||||
|
||||
# Élèves non inscrits dans cette classe pour inscription
|
||||
from sqlalchemy import func
|
||||
all_students = Student.query.order_by(func.lower(Student.last_name), func.lower(Student.first_name)).all()
|
||||
available_students = []
|
||||
for student in all_students:
|
||||
current_class = student.get_current_class()
|
||||
if not current_class or current_class.id != id:
|
||||
available_students.append(student)
|
||||
|
||||
current_app.logger.debug(f'Page élèves classe {id} - {total_current} élèves actuels, {len(class_movements)} mouvements')
|
||||
|
||||
return render_template('class_students.html',
|
||||
class_group=class_group,
|
||||
current_students=current_students,
|
||||
class_movements=class_movements,
|
||||
other_classes=other_classes,
|
||||
available_students=available_students,
|
||||
stats={
|
||||
'total_current': total_current,
|
||||
'recent_arrivals': recent_arrivals,
|
||||
'recent_departures': recent_departures
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(f'Erreur page élèves classe {id}: {e}')
|
||||
flash('Erreur lors du chargement des données des élèves.', 'error')
|
||||
return redirect(url_for('classes.dashboard', id=id))
|
||||
|
||||
@bp.route('/enroll', methods=['POST'])
|
||||
@handle_db_errors
|
||||
def enroll_student():
|
||||
"""Inscrire un élève dans une classe (existant ou nouveau)."""
|
||||
from repositories.temporal_student_repository import TemporalStudentRepository
|
||||
temporal_repo = TemporalStudentRepository()
|
||||
|
||||
try:
|
||||
data = request.get_json() if request.is_json else request.form
|
||||
|
||||
class_group_id = int(data.get('class_id'))
|
||||
enrollment_date_str = data.get('enrollment_date', str(date.today()))
|
||||
enrollment_reason = data.get('enrollment_reason', '')
|
||||
mode = data.get('mode', 'existing')
|
||||
|
||||
# Validation de la date
|
||||
enrollment_date = datetime.strptime(enrollment_date_str, '%Y-%m-%d').date()
|
||||
|
||||
# Vérifier que la classe existe
|
||||
class_group = ClassGroup.query.get_or_404(class_group_id)
|
||||
|
||||
if mode == 'new':
|
||||
# Mode création d'un nouvel élève
|
||||
first_name = data.get('new_first_name', '').strip()
|
||||
last_name = data.get('new_last_name', '').strip()
|
||||
email = data.get('new_email', '').strip() or None
|
||||
|
||||
if not first_name or not last_name:
|
||||
raise ValueError("Le prénom et le nom sont obligatoires pour un nouvel élève")
|
||||
|
||||
# Vérifier que l'email n'est pas déjà utilisé si fourni
|
||||
if email:
|
||||
existing_email = Student.query.filter_by(email=email).first()
|
||||
if existing_email:
|
||||
raise ValueError("Un élève avec cet email existe déjà")
|
||||
|
||||
# Créer le nouvel élève
|
||||
student = Student(
|
||||
first_name=first_name,
|
||||
last_name=last_name,
|
||||
email=email
|
||||
)
|
||||
db.session.add(student)
|
||||
db.session.flush() # Pour obtenir l'ID du nouvel élève
|
||||
|
||||
current_app.logger.info(f'Nouvel élève créé: {student.full_name}')
|
||||
|
||||
else:
|
||||
# Mode élève existant
|
||||
student_id = data.get('student_id')
|
||||
if not student_id:
|
||||
raise ValueError("Veuillez sélectionner un élève")
|
||||
|
||||
student_id = int(student_id)
|
||||
student = Student.query.get_or_404(student_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,
|
||||
'student_id': student.id,
|
||||
'is_new_student': mode == 'new'
|
||||
})
|
||||
else:
|
||||
if mode == 'new':
|
||||
flash(f'Nouvel élève {student.full_name} créé et inscrit en {class_group.name}', 'success')
|
||||
else:
|
||||
flash(f'Élève {student.full_name} inscrit en {class_group.name}', 'success')
|
||||
|
||||
# Pour une mise à jour immédiate de la liste, utiliser JavaScript pour recharger
|
||||
return redirect(url_for('classes.students', id=class_group_id) + '?reload=1')
|
||||
|
||||
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(request.referrer or url_for('classes'))
|
||||
|
||||
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(request.referrer or url_for('classes'))
|
||||
|
||||
@bp.route('/transfer', methods=['POST'])
|
||||
@handle_db_errors
|
||||
def transfer_student():
|
||||
"""Transférer un élève d'une classe à une autre."""
|
||||
from repositories.temporal_student_repository import TemporalStudentRepository
|
||||
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_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')
|
||||
# Retourner à la page d'origine avec rechargement
|
||||
referrer = request.referrer
|
||||
if referrer and 'classes/' in referrer and '/students' in referrer:
|
||||
return redirect(referrer + ('&' if '?' in referrer else '?') + 'reload=1')
|
||||
return redirect(request.referrer or url_for('classes'))
|
||||
|
||||
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(request.referrer or url_for('classes'))
|
||||
|
||||
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(request.referrer or url_for('classes'))
|
||||
|
||||
@bp.route('/departure', methods=['POST'])
|
||||
@handle_db_errors
|
||||
def student_departure():
|
||||
"""Enregistrer le départ d'un élève."""
|
||||
from repositories.temporal_student_repository import TemporalStudentRepository
|
||||
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')
|
||||
# Retourner à la page d'origine avec rechargement
|
||||
referrer = request.referrer
|
||||
if referrer and 'classes/' in referrer and '/students' in referrer:
|
||||
return redirect(referrer + ('&' if '?' in referrer else '?') + 'reload=1')
|
||||
return redirect(request.referrer or url_for('classes'))
|
||||
|
||||
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(request.referrer or url_for('classes'))
|
||||
|
||||
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(request.referrer or url_for('classes'))
|
||||
|
||||
@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('classes'))
|
||||
|
||||
# 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('classes'))
|
||||
|
||||
# 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 avec rechargement
|
||||
referrer = request.referrer
|
||||
if referrer and 'classes/' in referrer and '/students' in referrer:
|
||||
return redirect(referrer + ('&' if '?' in referrer else '?') + 'reload=1')
|
||||
return redirect(request.referrer or url_for('classes'))
|
||||
|
||||
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('classes'))
|
||||
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('classes'))
|
||||
Reference in New Issue
Block a user