292 lines
12 KiB
Python
292 lines
12 KiB
Python
from flask import Blueprint, render_template, request, jsonify, flash, redirect, url_for
|
|
from models import db, Grade, GradingElement
|
|
from repositories import AssessmentRepository, StudentRepository, GradeRepository
|
|
from app_config import config_manager
|
|
|
|
bp = Blueprint('grading', __name__)
|
|
|
|
@bp.route('/assessments/<int:assessment_id>/grading')
|
|
def assessment_grading(assessment_id):
|
|
assessment_repo = AssessmentRepository()
|
|
student_repo = StudentRepository()
|
|
grade_repo = GradeRepository()
|
|
|
|
assessment = assessment_repo.get_or_404(assessment_id)
|
|
students = student_repo.find_by_class_ordered(assessment.class_group_id)
|
|
|
|
# Get all grading elements for this assessment
|
|
grading_elements = []
|
|
for exercise in assessment.exercises:
|
|
for element in exercise.grading_elements:
|
|
grading_elements.append(element)
|
|
|
|
# Get existing grades
|
|
existing_grades = grade_repo.find_existing_grades_for_assessment(assessment_id)
|
|
|
|
# Préparer les informations d'affichage pour les scores
|
|
scale_values = config_manager.get_competence_scale_values()
|
|
|
|
# Récupérer la configuration du dégradé des notes
|
|
notes_gradient = {
|
|
'min_color': config_manager.get('grading.notes_gradient.min_color', '#dc2626'),
|
|
'max_color': config_manager.get('grading.notes_gradient.max_color', '#059669'),
|
|
'enabled': config_manager.get('grading.notes_gradient.enabled', False)
|
|
}
|
|
|
|
return render_template('assessment_grading.html',
|
|
assessment=assessment,
|
|
students=students,
|
|
grading_elements=grading_elements,
|
|
existing_grades=existing_grades,
|
|
scale_values=scale_values,
|
|
notes_gradient=notes_gradient,
|
|
config_manager=config_manager)
|
|
|
|
@bp.route('/assessments/<int:assessment_id>/grading/save', methods=['POST'])
|
|
def save_grades(assessment_id):
|
|
assessment_repo = AssessmentRepository()
|
|
student_repo = StudentRepository()
|
|
grade_repo = GradeRepository()
|
|
|
|
assessment = assessment_repo.get_or_404(assessment_id)
|
|
errors = []
|
|
saved_count = 0
|
|
|
|
try:
|
|
# Traitement des notes avec protection contre les cas edge
|
|
for key, value in request.form.items():
|
|
if key.startswith('grade_'):
|
|
# Parse key: grade_<student_id>_<element_id>
|
|
parts = key.split('_')
|
|
if len(parts) != 3:
|
|
errors.append(f'Format de clé invalide: {key}')
|
|
continue
|
|
|
|
try:
|
|
student_id = int(parts[1])
|
|
element_id = int(parts[2])
|
|
except (ValueError, IndexError) as e:
|
|
errors.append(f'Identifiants invalides dans {key}: {str(e)}')
|
|
continue
|
|
|
|
# Protection contre les valeurs None ou vides
|
|
if value is None:
|
|
continue
|
|
|
|
# Vérifier que l'étudiant et l'élément existent avec protection
|
|
try:
|
|
student = student_repo.find_by_id(student_id)
|
|
grading_element = GradingElement.query.get(element_id)
|
|
except Exception as e:
|
|
errors.append(f'Erreur DB pour {key}: {str(e)}')
|
|
continue
|
|
|
|
if not student:
|
|
errors.append(f'Étudiant non trouvé: {student_id}')
|
|
continue
|
|
|
|
if not grading_element:
|
|
errors.append(f'Élément de notation non trouvé: {element_id}')
|
|
continue
|
|
|
|
# Find or create grade avec protection
|
|
try:
|
|
grade = grade_repo.find_by_student_and_element(student_id, element_id)
|
|
|
|
if value.strip(): # If value is not empty
|
|
# Passer max_points pour la validation des notes
|
|
max_points = grading_element.max_points if grading_element.grading_type == 'notes' else None
|
|
|
|
# Normaliser virgule en point pour les notes avant sauvegarde
|
|
normalized_value = value.strip()
|
|
if grading_element.grading_type == 'notes' and ',' in normalized_value:
|
|
normalized_value = normalized_value.replace(',', '.')
|
|
|
|
if config_manager.validate_grade_value(normalized_value, grading_element.grading_type, max_points):
|
|
if not grade:
|
|
grade = Grade(
|
|
student_id=student_id,
|
|
grading_element_id=element_id,
|
|
value=normalized_value
|
|
)
|
|
db.session.add(grade)
|
|
else:
|
|
grade.value = normalized_value
|
|
saved_count += 1
|
|
else:
|
|
errors.append(f'Valeur invalide pour {grading_element.label}: "{value}"')
|
|
elif grade: # If value is empty but grade exists, delete it
|
|
db.session.delete(grade)
|
|
saved_count += 1
|
|
|
|
except Exception as e:
|
|
errors.append(f'Erreur traitement grade pour {key}: {str(e)}')
|
|
continue
|
|
|
|
# Traitement des commentaires avec protection
|
|
for key, value in request.form.items():
|
|
if key.startswith('comment_'):
|
|
parts = key.split('_')
|
|
if len(parts) != 3:
|
|
continue # Skip malformed keys
|
|
|
|
try:
|
|
student_id = int(parts[1])
|
|
element_id = int(parts[2])
|
|
except (ValueError, IndexError):
|
|
continue # Skip invalid IDs
|
|
|
|
# Protection contre les valeurs None
|
|
if value is None:
|
|
continue
|
|
|
|
try:
|
|
grade = grade_repo.find_by_student_and_element(student_id, element_id)
|
|
|
|
# Créer une note avec commentaire uniquement si nécessaire
|
|
if value.strip():
|
|
if not grade:
|
|
grade = Grade(
|
|
student_id=student_id,
|
|
grading_element_id=element_id,
|
|
value=None,
|
|
comment=value.strip()
|
|
)
|
|
db.session.add(grade)
|
|
else:
|
|
grade.comment = value.strip()
|
|
elif grade:
|
|
grade.comment = None
|
|
|
|
except Exception as e:
|
|
# Log l'erreur mais ne pas faire planter la sauvegarde
|
|
from flask import current_app
|
|
current_app.logger.warning(f"Erreur commentaire pour {key}: {str(e)}", exc_info=True)
|
|
continue
|
|
|
|
db.session.commit()
|
|
|
|
# Check if it's an AJAX request
|
|
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
|
if errors:
|
|
return jsonify({
|
|
'success': False,
|
|
'message': f'Certaines notes n\'ont pas pu être sauvegardées ({len(errors)} erreurs)',
|
|
'errors': errors,
|
|
'saved_count': saved_count
|
|
}), 400
|
|
else:
|
|
return jsonify({
|
|
'success': True,
|
|
'message': f'{saved_count} notes sauvegardées avec succès !',
|
|
'saved_count': saved_count
|
|
})
|
|
else:
|
|
# Traditional form submission
|
|
if errors:
|
|
for error in errors:
|
|
flash(error, 'warning')
|
|
flash(f'{saved_count} notes sauvegardées avec succès !', 'success')
|
|
return redirect(url_for('grading.assessment_grading', assessment_id=assessment_id))
|
|
|
|
except Exception as e:
|
|
import traceback
|
|
db.session.rollback()
|
|
|
|
# Log détaillé de l'erreur
|
|
error_details = traceback.format_exc()
|
|
from flask import current_app
|
|
current_app.logger.error(
|
|
f"=== ERREUR SAUVEGARDE ASSESSMENT {assessment_id} ===\n"
|
|
f"Exception: {type(e).__name__}: {str(e)}\n"
|
|
f"Traceback:\n{error_details}\n"
|
|
f"{'=' * 50}",
|
|
exc_info=True
|
|
)
|
|
|
|
error_msg = f'Erreur lors de la sauvegarde: {str(e)}'
|
|
|
|
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
|
return jsonify({
|
|
'success': False,
|
|
'message': error_msg,
|
|
'saved_count': 0,
|
|
'debug': error_details if current_app.debug else None
|
|
}), 500
|
|
else:
|
|
flash(error_msg, 'error')
|
|
return redirect(url_for('grading.assessment_grading', assessment_id=assessment_id))
|
|
|
|
|
|
@bp.route('/assessments/<int:assessment_id>/grading/save-single', methods=['POST'])
|
|
def save_single_grade(assessment_id):
|
|
"""Sauvegarde incrémentale d'une seule note"""
|
|
assessment_repo = AssessmentRepository()
|
|
student_repo = StudentRepository()
|
|
grade_repo = GradeRepository()
|
|
|
|
assessment = assessment_repo.get_or_404(assessment_id)
|
|
|
|
try:
|
|
data = request.get_json()
|
|
student_id = int(data.get('student_id'))
|
|
element_id = int(data.get('element_id'))
|
|
value = data.get('value', '').strip()
|
|
comment = data.get('comment', '').strip()
|
|
|
|
# Vérifications
|
|
student = student_repo.find_by_id(student_id)
|
|
grading_element = GradingElement.query.get(element_id)
|
|
|
|
if not student or not grading_element:
|
|
return jsonify({
|
|
'success': False,
|
|
'message': 'Étudiant ou élément de notation non trouvé'
|
|
}), 404
|
|
|
|
# Find or create grade
|
|
grade = grade_repo.find_by_student_and_element(student_id, element_id)
|
|
|
|
if value:
|
|
# Validation
|
|
max_points = grading_element.max_points if grading_element.grading_type == 'notes' else None
|
|
normalized_value = value.replace(',', '.') if grading_element.grading_type == 'notes' else value
|
|
|
|
if not config_manager.validate_grade_value(normalized_value, grading_element.grading_type, max_points):
|
|
return jsonify({
|
|
'success': False,
|
|
'message': f'Valeur invalide: "{value}"'
|
|
}), 400
|
|
|
|
if not grade:
|
|
grade = Grade(
|
|
student_id=student_id,
|
|
grading_element_id=element_id,
|
|
value=normalized_value,
|
|
comment=comment if comment else None
|
|
)
|
|
db.session.add(grade)
|
|
else:
|
|
grade.value = normalized_value
|
|
grade.comment = comment if comment else None
|
|
elif grade:
|
|
# Supprimer si valeur vide
|
|
if comment:
|
|
grade.value = None
|
|
grade.comment = comment
|
|
else:
|
|
db.session.delete(grade)
|
|
|
|
db.session.commit()
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'message': 'Note sauvegardée'
|
|
})
|
|
|
|
except Exception as e:
|
|
db.session.rollback()
|
|
return jsonify({
|
|
'success': False,
|
|
'message': f'Erreur: {str(e)}'
|
|
}), 500 |