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//grading') def assessment_grading(assessment_id): assessment_repo = AssessmentRepository() grade_repo = GradeRepository() assessment = assessment_repo.get_or_404(assessment_id) # Utilisation de la logique temporelle pour récupérer les élèves éligibles from repositories.temporal_student_repository import TemporalStudentRepository temporal_student_repo = TemporalStudentRepository() students = temporal_student_repo.find_eligible_for_assessment(assessment) # 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//grading/save', methods=['POST']) def save_grades(assessment_id): assessment_repo = AssessmentRepository() grade_repo = GradeRepository() from repositories.temporal_student_repository import TemporalStudentRepository temporal_student_repo = TemporalStudentRepository() 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__ 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 = temporal_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//grading/save-single', methods=['POST']) def save_single_grade(assessment_id): """Sauvegarde incrémentale d'une seule note""" assessment_repo = AssessmentRepository() grade_repo = GradeRepository() from repositories.temporal_student_repository import TemporalStudentRepository temporal_student_repo = TemporalStudentRepository() 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 = temporal_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