fix: improve saving
This commit is contained in:
@@ -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
|
||||||
@@ -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é
|
||||||
|
|||||||
Reference in New Issue
Block a user