From 2c1f2a9740d03f47121270ba3bc013b2980f2e00 Mon Sep 17 00:00:00 2001 From: Bertrand Benjamin Date: Wed, 6 Aug 2025 14:55:18 +0200 Subject: [PATCH] fix: improve saving --- routes/grading.py | 226 ++++++++++++++++++++++++------ templates/assessment_grading.html | 148 +++++++++++++++---- 2 files changed, 305 insertions(+), 69 deletions(-) diff --git a/routes/grading.py b/routes/grading.py index eb05e27..caa5768 100644 --- a/routes/grading.py +++ b/routes/grading.py @@ -1,4 +1,4 @@ -from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify +from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify, current_app from models import db, Assessment, Student, Grade, GradingElement, Exercise from app_config import config_manager @@ -36,66 +36,124 @@ def assessment_grading(assessment_id): def save_grades(assessment_id): assessment = Assessment.query.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: + if len(parts) != 3: + errors.append(f'Format de clé invalide: {key}') + continue + + try: student_id = int(parts[1]) element_id = int(parts[2]) - - # Find or create grade + 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.query.get(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.query.filter_by( student_id=student_id, grading_element_id=element_id ).first() if value.strip(): # If value is not empty - # Validation unifiée selon le nouveau système - grading_element = GradingElement.query.get(element_id) - if grading_element: - # 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 + # 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: - errors.append(f'Valeur invalide pour {grading_element.label if grading_element else "cet élément"}: {value}') + grade.value = normalized_value + saved_count += 1 else: - errors.append(f'Élément de notation non trouvé: {element_id}') + 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 - # Handle comments + # Traitement des commentaires avec protection for key, value in request.form.items(): if key.startswith('comment_'): parts = key.split('_') - if len(parts) == 3: + 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.query.filter_by( student_id=student_id, grading_element_id=element_id ).first() - if grade: - grade.comment = value.strip() if value.strip() else None + # 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 + print(f"Erreur commentaire pour {key}: {str(e)}") + continue db.session.commit() @@ -104,29 +162,117 @@ def save_grades(assessment_id): if errors: return jsonify({ 'success': False, - 'message': 'Certaines notes n\'ont pas pu être sauvegardées', - 'errors': errors - }) + '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': 'Notes sauvegardées avec succès !' + '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('Notes sauvegardées avec succès !', 'success') + 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() + print(f"=== ERREUR SAUVEGARDE ASSESSMENT {assessment_id} ===") + print(f"Exception: {type(e).__name__}: {str(e)}") + print(f"Traceback:\n{error_details}") + print("=" * 50) + + error_msg = f'Erreur lors de la sauvegarde: {str(e)}' + if request.headers.get('X-Requested-With') == 'XMLHttpRequest': return jsonify({ 'success': False, - 'message': f'Erreur lors de la sauvegarde: {str(e)}' + 'message': error_msg, + 'saved_count': 0, + 'debug': error_details if current_app.debug else None }), 500 else: - flash(f'Erreur lors de la sauvegarde: {str(e)}', 'error') - return redirect(url_for('grading.assessment_grading', assessment_id=assessment_id)) \ No newline at end of file + 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 = Assessment.query.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.query.get(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.query.filter_by( + student_id=student_id, + grading_element_id=element_id + ).first() + + 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 \ No newline at end of file diff --git a/templates/assessment_grading.html b/templates/assessment_grading.html index 7156519..16a24c8 100644 --- a/templates/assessment_grading.html +++ b/templates/assessment_grading.html @@ -720,6 +720,59 @@ class InputManager { input.classList.remove('bg-yellow-50', 'border-yellow-300'); ColorManager.applyColorToInput(input, value, type, isValid, maxPoints); }, 1000); + + // Option de sauvegarde incrémentale après 3 secondes d'inactivité + clearTimeout(input.saveTimeout); + input.saveTimeout = setTimeout(() => { + this.saveIncremental(input); + }, 3000); + } + + static async saveIncremental(input) { + if (state.isAutoSaving) return; + + const studentId = input.dataset.studentId; + const elementId = input.dataset.elementId; + const value = input.value; + + // Chercher le commentaire associé + const commentInput = document.querySelector(`[name="comment_${studentId}_${elementId}"]`); + const comment = commentInput ? commentInput.value : ''; + + try { + const response = await fetch(`{{ url_for('grading.save_single_grade', assessment_id=assessment.id) }}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Requested-With': 'XMLHttpRequest' + }, + body: JSON.stringify({ + student_id: parseInt(studentId), + element_id: parseInt(elementId), + value: value, + comment: comment + }) + }); + + if (response.ok) { + const result = await response.json(); + if (result.success) { + const key = `${studentId}_${elementId}`; + state.unsavedChanges.delete(key); + state.updateSaveStatus(); + + // Feedback visuel discret + input.classList.add('border-green-400'); + setTimeout(() => { + input.classList.remove('border-green-400'); + }, 1000); + } else { + console.error('Erreur sauvegarde incrémentale:', result.message); + } + } + } catch (error) { + console.error('Erreur réseau sauvegarde incrémentale:', error); + } } static updateCurrentPosition() { @@ -806,24 +859,29 @@ class FormManager { } }); - if (response.ok) { - const result = await response.json(); - if (result.success) { - state.unsavedChanges.clear(); - state.updateSaveStatus(); - UIManager.showToast('Notes sauvegardées avec succès', 'success'); - } else { - UIManager.showToast(result.message || 'Erreur lors de la sauvegarde', 'error'); - } + const result = await response.json(); + + if (result.success) { + state.unsavedChanges.clear(); + state.updateSaveStatus(); + UIManager.showToast(result.message || 'Notes sauvegardées avec succès', 'success'); } else { - UIManager.showToast('Erreur réseau lors de la sauvegarde', 'error'); + UIManager.showToast(result.message || 'Erreur lors de la sauvegarde', 'error'); + + // Afficher les erreurs détaillées si disponibles + if (result.errors && result.errors.length > 0) { + console.error('Erreurs détaillées:', result.errors); + setTimeout(() => { + UIManager.showToast(`${result.errors.length} erreur(s) de validation`, 'warning'); + }, 2000); + } } } catch (error) { console.error('Erreur sauvegarde:', error); - UIManager.showToast('Erreur lors de la sauvegarde', 'error'); + UIManager.showToast('Erreur de communication avec le serveur', 'error'); } finally { saveButton.disabled = false; - saveText.textContent = 'Sauvegarder'; + saveText.textContent = 'Sauvegarder les notes'; saveSpinner.classList.add('hidden'); } } @@ -855,21 +913,26 @@ class FormManager { } }); - if (response.ok) { - const result = await response.json(); - if (result.success) { - state.unsavedChanges.clear(); - state.updateSaveStatus(); - UIManager.showToast('Notes sauvegardées avec succès', 'success'); - } else { - UIManager.showToast(result.message || 'Erreur lors de la sauvegarde', 'error'); - } + const result = await response.json(); + + if (result.success) { + state.unsavedChanges.clear(); + state.updateSaveStatus(); + UIManager.showToast(result.message || 'Notes sauvegardées avec succès', 'success'); } else { - UIManager.showToast('Erreur réseau lors de la sauvegarde', 'error'); + UIManager.showToast(result.message || 'Erreur lors de la sauvegarde', 'error'); + + // Afficher les erreurs détaillées si disponibles + if (result.errors && result.errors.length > 0) { + console.error('Erreurs détaillées:', result.errors); + setTimeout(() => { + UIManager.showToast(`${result.errors.length} erreur(s) de validation`, 'warning'); + }, 2000); + } } } catch (error) { console.error('Erreur sauvegarde:', error); - UIManager.showToast('Erreur lors de la sauvegarde', 'error'); + UIManager.showToast('Erreur de communication avec le serveur', 'error'); } finally { if (saveButtonFs) { saveButtonFs.disabled = false; @@ -946,18 +1009,45 @@ function setupAutosave() { }, 30000); } -function autoSave() { +async function autoSave() { if (state.isAutoSaving) return; state.isAutoSaving = true; UIManager.showToast('Sauvegarde automatique...', 'info'); - setTimeout(() => { + try { + const form = document.getElementById('grading-form'); + if (!form) { + throw new Error('Formulaire non trouvé'); + } + + const formData = new FormData(form); + const response = await fetch(form.action, { + method: 'POST', + body: formData, + headers: { + 'X-Requested-With': 'XMLHttpRequest' + } + }); + + if (response.ok) { + const result = await response.json(); + if (result.success) { + state.unsavedChanges.clear(); + state.updateSaveStatus(); + UIManager.showToast('Sauvegardé automatiquement', 'success'); + } else { + UIManager.showToast(result.message || 'Erreur lors de la sauvegarde automatique', 'error'); + } + } else { + throw new Error('Erreur réseau lors de la sauvegarde automatique'); + } + } catch (error) { + console.error('Erreur sauvegarde automatique:', error); + UIManager.showToast('Erreur lors de la sauvegarde automatique', 'error'); + } finally { state.isAutoSaving = false; - state.unsavedChanges.clear(); - state.updateSaveStatus(); - UIManager.showToast('Sauvegardé automatiquement', 'success'); - }, 2000); + } } // Fonctions de compatibilité