416 lines
17 KiB
Python
416 lines
17 KiB
Python
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('/<int:id>/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('/<int:id>', 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('/<int:id>/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('/<int:id>', 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('/<int:id>/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('/<int:id>/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,
|
|
"student_averages": class_results["student_averages"],
|
|
"student_averages_distribution": class_results["student_averages_distribution"]
|
|
}
|
|
}
|
|
|
|
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('/<int:id>/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)
|
|
|
|
@bp.route('/<int:id>/council')
|
|
@handle_db_errors
|
|
def council_preparation(id):
|
|
"""Page de préparation du conseil de classe."""
|
|
# Le trimestre est obligatoire pour la préparation du conseil
|
|
trimester = request.args.get('trimestre', type=int)
|
|
if not trimester or trimester not in [1, 2, 3]:
|
|
flash('Veuillez sélectionner un trimestre pour préparer le conseil de classe.', 'error')
|
|
return redirect(url_for('classes.dashboard', id=id))
|
|
|
|
# Vérifier que la classe existe
|
|
class_repo = ClassRepository()
|
|
class_group = class_repo.get_or_404(id)
|
|
|
|
try:
|
|
# Injection de dépendances via factory
|
|
from services.council_services import CouncilServiceFactory
|
|
council_service = CouncilServiceFactory.create_council_preparation_service()
|
|
|
|
# Préparer toutes les données du conseil
|
|
council_data = council_service.prepare_council_data(id, trimester)
|
|
|
|
current_app.logger.info(f'Préparation conseil classe {id}, trimestre {trimester} - {len(council_data.student_summaries)} élèves')
|
|
|
|
return render_template('class_council_preparation.html',
|
|
class_group=class_group,
|
|
trimester=trimester,
|
|
council_data=council_data,
|
|
student_summaries=council_data.student_summaries,
|
|
class_statistics=council_data.class_statistics,
|
|
appreciation_stats=council_data.appreciation_stats)
|
|
|
|
except Exception as e:
|
|
current_app.logger.error(f'Erreur préparation conseil classe {id}: {e}')
|
|
flash('Erreur lors de la préparation des données du conseil de classe.', 'error')
|
|
return redirect(url_for('classes.dashboard', id=id))
|
|
|
|
@bp.route('/<int:class_id>/council/appreciation/<int:student_id>', methods=['POST'])
|
|
@handle_db_errors
|
|
def save_appreciation_api(class_id, student_id):
|
|
"""API pour sauvegarde d'appréciations (AJAX)."""
|
|
try:
|
|
# Vérifications de base
|
|
class_repo = ClassRepository()
|
|
class_group = class_repo.get_or_404(class_id)
|
|
|
|
data = request.get_json()
|
|
if not data:
|
|
return jsonify({'success': False, 'error': 'Données manquantes'}), 400
|
|
|
|
# Validation du trimestre
|
|
trimester = data.get('trimester')
|
|
if not trimester or trimester not in [1, 2, 3]:
|
|
return jsonify({'success': False, 'error': 'Trimestre invalide'}), 400
|
|
|
|
# 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:
|
|
return jsonify({'success': False, 'error': 'Élève non trouvé dans cette classe'}), 403
|
|
|
|
# Préparer les données d'appréciation
|
|
appreciation_data = {
|
|
'student_id': student_id,
|
|
'class_group_id': class_id,
|
|
'trimester': trimester,
|
|
'general_appreciation': data.get('appreciation', '').strip() or None,
|
|
'strengths': data.get('strengths', '').strip() or None,
|
|
'areas_for_improvement': data.get('areas_for_improvement', '').strip() or None,
|
|
'status': data.get('status', 'draft')
|
|
}
|
|
|
|
# Sauvegarder via le service
|
|
from services.council_services import CouncilServiceFactory
|
|
appreciation_service = CouncilServiceFactory.create_appreciation_service()
|
|
result = appreciation_service.save_appreciation(appreciation_data)
|
|
|
|
current_app.logger.info(f'Appréciation sauvegardée - Élève {student_id}, Classe {class_id}, T{trimester}')
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'appreciation_id': result.id,
|
|
'last_modified': result.last_modified.isoformat(),
|
|
'status': result.status,
|
|
'has_content': result.has_content
|
|
})
|
|
|
|
except Exception as e:
|
|
current_app.logger.error(f'Erreur sauvegarde appréciation élève {student_id}: {e}')
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'Erreur lors de la sauvegarde'
|
|
}), 500
|
|
|
|
@bp.route('/<int:class_id>/council/api')
|
|
@handle_db_errors
|
|
def council_data_api(class_id):
|
|
"""API JSON pour récupérer les données d'un trimestre (AJAX)."""
|
|
try:
|
|
# Vérifications
|
|
trimester = request.args.get('trimestre', type=int)
|
|
if not trimester or trimester not in [1, 2, 3]:
|
|
return jsonify({'error': 'Trimestre invalide'}), 400
|
|
|
|
class_repo = ClassRepository()
|
|
class_group = class_repo.get_or_404(class_id)
|
|
|
|
# Récupérer les données via le service
|
|
from services.council_services import CouncilServiceFactory
|
|
council_service = CouncilServiceFactory.create_council_preparation_service()
|
|
council_data = council_service.prepare_council_data(class_id, trimester)
|
|
|
|
# Formatter pour JSON
|
|
response_data = {
|
|
'trimester': trimester,
|
|
'class_id': class_id,
|
|
'total_students': council_data.total_students,
|
|
'completed_appreciations': council_data.completed_appreciations,
|
|
'class_statistics': council_data.class_statistics,
|
|
'appreciation_stats': council_data.appreciation_stats,
|
|
'students': []
|
|
}
|
|
|
|
# Ajouter les données des élèves
|
|
for summary in council_data.student_summaries:
|
|
student_data = {
|
|
'id': summary.student.id,
|
|
'name': summary.student.full_name,
|
|
'last_name': summary.student.last_name,
|
|
'first_name': summary.student.first_name,
|
|
'average': summary.overall_average,
|
|
'assessment_count': summary.assessment_count,
|
|
'performance_status': summary.performance_status,
|
|
'has_appreciation': summary.has_appreciation,
|
|
'assessments': {}
|
|
}
|
|
|
|
# Ajouter les détails des évaluations
|
|
for assessment_id, assessment_data in summary.grades_by_assessment.items():
|
|
student_data['assessments'][assessment_id] = {
|
|
'score': assessment_data['score'],
|
|
'max': assessment_data['max'],
|
|
'title': assessment_data['title']
|
|
}
|
|
|
|
response_data['students'].append(student_data)
|
|
|
|
return jsonify(response_data)
|
|
|
|
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 |