fix: improve saving

This commit is contained in:
2025-08-06 14:55:18 +02:00
parent 25945fc64c
commit 2c1f2a9740
2 changed files with 305 additions and 69 deletions

View File

@@ -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 models import db, Assessment, Student, Grade, GradingElement, Exercise
from app_config import config_manager from app_config import config_manager
@@ -36,66 +36,124 @@ def assessment_grading(assessment_id):
def save_grades(assessment_id): def save_grades(assessment_id):
assessment = Assessment.query.get_or_404(assessment_id) assessment = Assessment.query.get_or_404(assessment_id)
errors = [] errors = []
saved_count = 0
try: try:
# Traitement des notes avec protection contre les cas edge
for key, value in request.form.items(): for key, value in request.form.items():
if key.startswith('grade_'): if key.startswith('grade_'):
# Parse key: grade_<student_id>_<element_id> # Parse key: grade_<student_id>_<element_id>
parts = key.split('_') 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]) student_id = int(parts[1])
element_id = int(parts[2]) element_id = int(parts[2])
except (ValueError, IndexError) as e:
# Find or create grade 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( grade = Grade.query.filter_by(
student_id=student_id, student_id=student_id,
grading_element_id=element_id grading_element_id=element_id
).first() ).first()
if value.strip(): # If value is not empty if value.strip(): # If value is not empty
# Validation unifiée selon le nouveau système # Passer max_points pour la validation des notes
grading_element = GradingElement.query.get(element_id) max_points = grading_element.max_points if grading_element.grading_type == 'notes' else None
if grading_element:
# Passer max_points pour la validation des notes # Normaliser virgule en point pour les notes avant sauvegarde
max_points = grading_element.max_points if grading_element.grading_type == 'notes' else None normalized_value = value.strip()
if grading_element.grading_type == 'notes' and ',' in normalized_value:
# Normaliser virgule en point pour les notes avant sauvegarde normalized_value = normalized_value.replace(',', '.')
normalized_value = value.strip()
if grading_element.grading_type == 'notes' and ',' in normalized_value: if config_manager.validate_grade_value(normalized_value, grading_element.grading_type, max_points):
normalized_value = normalized_value.replace(',', '.') if not grade:
grade = Grade(
if config_manager.validate_grade_value(normalized_value, grading_element.grading_type, max_points): student_id=student_id,
if not grade: grading_element_id=element_id,
grade = Grade( value=normalized_value
student_id=student_id, )
grading_element_id=element_id, db.session.add(grade)
value=normalized_value
)
db.session.add(grade)
else:
grade.value = normalized_value
else: 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: 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 elif grade: # If value is empty but grade exists, delete it
db.session.delete(grade) 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(): for key, value in request.form.items():
if key.startswith('comment_'): if key.startswith('comment_'):
parts = key.split('_') parts = key.split('_')
if len(parts) == 3: if len(parts) != 3:
continue # Skip malformed keys
try:
student_id = int(parts[1]) student_id = int(parts[1])
element_id = int(parts[2]) 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( grade = Grade.query.filter_by(
student_id=student_id, student_id=student_id,
grading_element_id=element_id grading_element_id=element_id
).first() ).first()
if grade: # Créer une note avec commentaire uniquement si nécessaire
grade.comment = value.strip() if value.strip() else None 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() db.session.commit()
@@ -104,29 +162,117 @@ def save_grades(assessment_id):
if errors: if errors:
return jsonify({ return jsonify({
'success': False, 'success': False,
'message': 'Certaines notes n\'ont pas pu être sauvegardées', 'message': f'Certaines notes n\'ont pas pu être sauvegardées ({len(errors)} erreurs)',
'errors': errors 'errors': errors,
}) 'saved_count': saved_count
}), 400
else: else:
return jsonify({ return jsonify({
'success': True, 'success': True,
'message': 'Notes sauvegardées avec succès !' 'message': f'{saved_count} notes sauvegardées avec succès !',
'saved_count': saved_count
}) })
else: else:
# Traditional form submission # Traditional form submission
if errors: if errors:
for error in errors: for error in errors:
flash(error, 'warning') 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)) return redirect(url_for('grading.assessment_grading', assessment_id=assessment_id))
except Exception as e: except Exception as e:
import traceback
db.session.rollback() 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': if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({ return jsonify({
'success': False, '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 }), 500
else: else:
flash(f'Erreur lors de la sauvegarde: {str(e)}', 'error') flash(error_msg, 'error')
return redirect(url_for('grading.assessment_grading', assessment_id=assessment_id)) 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 = 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

View File

@@ -720,6 +720,59 @@ class InputManager {
input.classList.remove('bg-yellow-50', 'border-yellow-300'); input.classList.remove('bg-yellow-50', 'border-yellow-300');
ColorManager.applyColorToInput(input, value, type, isValid, maxPoints); ColorManager.applyColorToInput(input, value, type, isValid, maxPoints);
}, 1000); }, 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() { static updateCurrentPosition() {
@@ -806,24 +859,29 @@ class FormManager {
} }
}); });
if (response.ok) { const result = await response.json();
const result = await response.json();
if (result.success) { if (result.success) {
state.unsavedChanges.clear(); state.unsavedChanges.clear();
state.updateSaveStatus(); state.updateSaveStatus();
UIManager.showToast('Notes sauvegardées avec succès', 'success'); UIManager.showToast(result.message || 'Notes sauvegardées avec succès', 'success');
} else {
UIManager.showToast(result.message || 'Erreur lors de la sauvegarde', 'error');
}
} else { } 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) { } catch (error) {
console.error('Erreur sauvegarde:', error); console.error('Erreur sauvegarde:', error);
UIManager.showToast('Erreur lors de la sauvegarde', 'error'); UIManager.showToast('Erreur de communication avec le serveur', 'error');
} finally { } finally {
saveButton.disabled = false; saveButton.disabled = false;
saveText.textContent = 'Sauvegarder'; saveText.textContent = 'Sauvegarder les notes';
saveSpinner.classList.add('hidden'); saveSpinner.classList.add('hidden');
} }
} }
@@ -855,21 +913,26 @@ class FormManager {
} }
}); });
if (response.ok) { const result = await response.json();
const result = await response.json();
if (result.success) { if (result.success) {
state.unsavedChanges.clear(); state.unsavedChanges.clear();
state.updateSaveStatus(); state.updateSaveStatus();
UIManager.showToast('Notes sauvegardées avec succès', 'success'); UIManager.showToast(result.message || 'Notes sauvegardées avec succès', 'success');
} else {
UIManager.showToast(result.message || 'Erreur lors de la sauvegarde', 'error');
}
} else { } 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) { } catch (error) {
console.error('Erreur sauvegarde:', error); console.error('Erreur sauvegarde:', error);
UIManager.showToast('Erreur lors de la sauvegarde', 'error'); UIManager.showToast('Erreur de communication avec le serveur', 'error');
} finally { } finally {
if (saveButtonFs) { if (saveButtonFs) {
saveButtonFs.disabled = false; saveButtonFs.disabled = false;
@@ -946,18 +1009,45 @@ function setupAutosave() {
}, 30000); }, 30000);
} }
function autoSave() { async function autoSave() {
if (state.isAutoSaving) return; if (state.isAutoSaving) return;
state.isAutoSaving = true; state.isAutoSaving = true;
UIManager.showToast('Sauvegarde automatique...', 'info'); 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.isAutoSaving = false;
state.unsavedChanges.clear(); }
state.updateSaveStatus();
UIManager.showToast('Sauvegardé automatiquement', 'success');
}, 2000);
} }
// Fonctions de compatibilité // Fonctions de compatibilité