feat: use saved colors in assessment_grading
This commit is contained in:
@@ -122,29 +122,63 @@ function calculateNoteColor(note, maxPoints, minColor, maxColor) {
|
||||
|
||||
## Utilisation dans le Code
|
||||
|
||||
### Calcul des Couleurs de Notes
|
||||
### Application dans la Page de Notation
|
||||
|
||||
Le dégradé HSL est **automatiquement appliqué dans la page de notation** (`/assessments/<id>/grading`) pour tous les éléments de type `"notes"`.
|
||||
|
||||
#### Transmission de la Configuration
|
||||
|
||||
```python
|
||||
# Côté serveur : Transmission de la configuration
|
||||
# routes/grading.py - Route assessment_grading()
|
||||
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',
|
||||
notes_gradient=notes_gradient, # ← Nouvelle transmission
|
||||
# ... autres paramètres
|
||||
)
|
||||
```
|
||||
|
||||
#### Calcul Dynamique des Couleurs
|
||||
|
||||
```javascript
|
||||
// Côté client : Calcul dynamique
|
||||
// Fonction globale pour calculer la couleur d'une note
|
||||
window.getNotesGradientColor = function(note, maxPoints) {
|
||||
const minColor = '{{ notes_gradient.min_color }}';
|
||||
const maxColor = '{{ notes_gradient.max_color }}';
|
||||
const enabled = {{ notes_gradient.enabled|tojson }};
|
||||
if (!GRADING_CONFIG.notes_gradient.enabled) return '#6b7280';
|
||||
|
||||
if (!enabled) return '#6b7280';
|
||||
return calculateNoteColor(note, maxPoints, minColor, maxColor);
|
||||
const factor = Math.min(1, Math.max(0, note / maxPoints));
|
||||
return interpolateColorHSL(
|
||||
GRADING_CONFIG.notes_gradient.min_color,
|
||||
GRADING_CONFIG.notes_gradient.max_color,
|
||||
factor
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
#### Application Automatique
|
||||
|
||||
```javascript
|
||||
// ColorManager.applyColorToInput() - Logique de colorisation
|
||||
if (type === 'notes') {
|
||||
// Valeurs spéciales (., d, etc.) → couleurs échelle
|
||||
if (GRADING_CONFIG.scale_values[value] && isNaN(value)) {
|
||||
// Couleur spéciale avec style italique
|
||||
}
|
||||
|
||||
// Notes numériques (2, 2.0, 15.5, etc.) → dégradé HSL
|
||||
const numValue = parseFloat(value);
|
||||
if (!isNaN(numValue)) {
|
||||
const noteColor = window.getNotesGradientColor(numValue, maxPoints);
|
||||
input.style.color = '#374151'; // Texte gris doux
|
||||
input.style.backgroundColor = hexToRgba(noteColor, 0.25); // Fond coloré
|
||||
input.style.borderColor = hexToRgba(noteColor, 0.5); // Bordure accentuée
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Accès aux Valeurs d'Échelle
|
||||
|
||||
```python
|
||||
@@ -267,21 +301,54 @@ Dégradé notes : Orange → Bleu (évaluations /10)
|
||||
Valeurs spéciales : "NA" pour non applicable, "O" pour oral seulement
|
||||
```
|
||||
|
||||
## Indicateurs Visuels d'Erreur
|
||||
|
||||
### Validation des Saisies
|
||||
|
||||
Le système détecte automatiquement les **valeurs interdites** et applique un indicateur visuel très visible :
|
||||
|
||||
```javascript
|
||||
// Valeurs interdites → Indicateur rouge vif
|
||||
if (!isValid) {
|
||||
input.style.color = '#ffffff'; // Texte blanc
|
||||
input.style.backgroundColor = '#dc2626'; // Fond rouge vif
|
||||
input.style.borderColor = '#dc2626'; // Bordure rouge
|
||||
input.style.borderWidth = '2px'; // Bordure épaisse
|
||||
input.style.boxShadow = '0 0 0 3px rgba(220, 38, 38, 0.3)'; // Ombre rouge
|
||||
input.style.fontWeight = 'bold'; // Texte en gras
|
||||
}
|
||||
```
|
||||
|
||||
### Exemples d'Erreurs Détectées
|
||||
|
||||
**Pour les scores (type="score")** :
|
||||
- Valeurs autres que 0, 1, 2, 3 ou valeurs spéciales configurées
|
||||
- Exemples : `"5"`, `"1.5"`, `"abc"`, `"-1"`
|
||||
|
||||
**Pour les notes (type="notes")** :
|
||||
- Valeurs négatives : `"-5"`
|
||||
- Valeurs supérieures au maximum : `"25"` (si max=20)
|
||||
- Format invalide : `"abc"`, `"2.5.3"`, `"10,5,2"`
|
||||
|
||||
### Reset Automatique
|
||||
Dès qu'une valeur valide est saisie, tous les styles d'erreur sont automatiquement effacés et remplacés par la colorisation normale.
|
||||
|
||||
## Impact UX
|
||||
|
||||
### Bénéfices Utilisateur
|
||||
- **Cohérence visuelle** : Couleurs automatiques selon performance
|
||||
- **Simplicité** : Un seul bouton de sauvegarde
|
||||
- **Feedback immédiat** : Prévisualisation temps réel
|
||||
- **Feedback immédiat** : Erreurs visibles instantanément + dégradé temps réel
|
||||
- **Distinction claire** : Notes vs Scores vs Valeurs spéciales
|
||||
- **Simplicité** : Configuration centralisée, application automatique
|
||||
- **Flexibilité** : Adaptation aux pratiques pédagogiques
|
||||
|
||||
### Performance
|
||||
- **Client** : Calculs JavaScript optimisés
|
||||
- **Serveur** : Configuration mise en cache
|
||||
- **Client** : Calculs JavaScript optimisés avec interpolation HSL native
|
||||
- **Serveur** : Configuration mise en cache, transmission optimisée
|
||||
- **Base** : Index sur valeurs d'échelle
|
||||
- **Rendu** : Templates précompilés
|
||||
- **Rendu** : Colorisation temps réel sans rechargement
|
||||
|
||||
---
|
||||
|
||||
**Documentation maintenue à jour - Version 2025**
|
||||
*Dernière modification : Janvier 2025*
|
||||
*Dernière modification : 9 août 2025 - Ajout dégradé HSL et indicateurs d'erreur*
|
||||
@@ -156,6 +156,8 @@ notytex/
|
||||
- ✅ **Dynamic Settings** : Configuration runtime modifiable
|
||||
- ✅ **Scales Management** : Échelles de notation configurables (0-3 + spéciales)
|
||||
- ✅ **Color Gradients** : Système dégradé couleurs notes avec HSL
|
||||
- ✅ **Visual Feedback** : Indicateurs d'erreur visuels pour validation
|
||||
- ✅ **Grading Integration** : Application automatique dans page de notation
|
||||
- ✅ **Business Rules** : Règles métier configurables
|
||||
- ✅ **Unified Interface** : Interface de configuration unifiée
|
||||
|
||||
|
||||
@@ -26,12 +26,20 @@ def assessment_grading(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'])
|
||||
|
||||
@@ -450,7 +450,12 @@ const GRADING_CONFIG = {
|
||||
'{{ value }}' : {{ scale_values[value] | tojson }}{% if not loop.last %},{% endif %}
|
||||
{% endfor %}
|
||||
},
|
||||
special_keys: Object.keys({{ scale_values | tojson }}).filter(key => !['0', '1', '2', '3'].includes(key))
|
||||
special_keys: Object.keys({{ scale_values | tojson }}).filter(key => !['0', '1', '2', '3'].includes(key)),
|
||||
notes_gradient: {
|
||||
min_color: '{{ notes_gradient.min_color }}',
|
||||
max_color: '{{ notes_gradient.max_color }}',
|
||||
enabled: {{ notes_gradient.enabled|tojson }}
|
||||
}
|
||||
};
|
||||
|
||||
// ====== ÉTAT GLOBAL ======
|
||||
@@ -508,6 +513,118 @@ class GradingState {
|
||||
|
||||
const state = new GradingState();
|
||||
|
||||
// ====== FONCTIONS D'INTERPOLATION HSL ======
|
||||
function hexToRgb(hex) {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
return result ? {
|
||||
r: parseInt(result[1], 16),
|
||||
g: parseInt(result[2], 16),
|
||||
b: parseInt(result[3], 16)
|
||||
} : null;
|
||||
}
|
||||
|
||||
function rgbToHex(r, g, b) {
|
||||
const toHex = (c) => {
|
||||
const hex = Math.round(Math.max(0, Math.min(255, c))).toString(16);
|
||||
return hex.length == 1 ? "0" + hex : hex;
|
||||
};
|
||||
return "#" + toHex(r) + toHex(g) + toHex(b);
|
||||
}
|
||||
|
||||
function rgbToHsl(r, g, b) {
|
||||
r /= 255;
|
||||
g /= 255;
|
||||
b /= 255;
|
||||
|
||||
const max = Math.max(r, g, b), min = Math.min(r, g, b);
|
||||
let h, s, l = (max + min) / 2;
|
||||
|
||||
if (max === min) {
|
||||
h = s = 0;
|
||||
} else {
|
||||
const d = max - min;
|
||||
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
||||
switch (max) {
|
||||
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
|
||||
case g: h = (b - r) / d + 2; break;
|
||||
case b: h = (r - g) / d + 4; break;
|
||||
}
|
||||
h /= 6;
|
||||
}
|
||||
|
||||
return { h: h * 360, s: s * 100, l: l * 100 };
|
||||
}
|
||||
|
||||
function hslToRgb(h, s, l) {
|
||||
h = h / 360;
|
||||
s = s / 100;
|
||||
l = l / 100;
|
||||
|
||||
const hue2rgb = (p, q, t) => {
|
||||
if (t < 0) t += 1;
|
||||
if (t > 1) t -= 1;
|
||||
if (t < 1/6) return p + (q - p) * 6 * t;
|
||||
if (t < 1/2) return q;
|
||||
if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
|
||||
return p;
|
||||
};
|
||||
|
||||
let r, g, b;
|
||||
|
||||
if (s === 0) {
|
||||
r = g = b = l;
|
||||
} else {
|
||||
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
||||
const p = 2 * l - q;
|
||||
r = hue2rgb(p, q, h + 1/3);
|
||||
g = hue2rgb(p, q, h);
|
||||
b = hue2rgb(p, q, h - 1/3);
|
||||
}
|
||||
|
||||
return { r: Math.round(r * 255), g: Math.round(g * 255), b: Math.round(b * 255) };
|
||||
}
|
||||
|
||||
function interpolateColorHSL(color1, color2, factor) {
|
||||
const rgb1 = hexToRgb(color1);
|
||||
const rgb2 = hexToRgb(color2);
|
||||
|
||||
if (!rgb1 || !rgb2) return color1;
|
||||
|
||||
const hsl1 = rgbToHsl(rgb1.r, rgb1.g, rgb1.b);
|
||||
const hsl2 = rgbToHsl(rgb2.r, rgb2.g, rgb2.b);
|
||||
|
||||
// Gérer la transition de teinte (chemin le plus court)
|
||||
let h1 = hsl1.h;
|
||||
let h2 = hsl2.h;
|
||||
let deltaH = h2 - h1;
|
||||
if (deltaH > 180) {
|
||||
h2 -= 360;
|
||||
} else if (deltaH < -180) {
|
||||
h2 += 360;
|
||||
}
|
||||
|
||||
const h = h1 + (h2 - h1) * factor;
|
||||
const s = hsl1.s + (hsl2.s - hsl1.s) * factor;
|
||||
const l = hsl1.l + (hsl2.l - hsl1.l) * factor;
|
||||
|
||||
const normalizedH = ((h % 360) + 360) % 360;
|
||||
|
||||
const rgb = hslToRgb(normalizedH, s, l);
|
||||
return rgbToHex(rgb.r, rgb.g, rgb.b);
|
||||
}
|
||||
|
||||
// Fonction globale pour calculer la couleur d'une note
|
||||
window.getNotesGradientColor = function(note, maxPoints) {
|
||||
if (!GRADING_CONFIG.notes_gradient.enabled) return '#6b7280';
|
||||
|
||||
const factor = Math.min(1, Math.max(0, note / maxPoints));
|
||||
return interpolateColorHSL(
|
||||
GRADING_CONFIG.notes_gradient.min_color,
|
||||
GRADING_CONFIG.notes_gradient.max_color,
|
||||
factor
|
||||
);
|
||||
};
|
||||
|
||||
// ====== INITIALISATION ======
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
setupKeyboardNavigation();
|
||||
@@ -533,31 +650,24 @@ class ColorManager {
|
||||
}
|
||||
|
||||
static applyColorToInput(input, value, type, isValid, maxPoints) {
|
||||
// Reset styles
|
||||
// Reset styles complètement
|
||||
input.className = input.className.replace(/border-\w+-\d+|bg-\w+-\d+/g, '').trim();
|
||||
input.style.cssText = '';
|
||||
input.style.borderWidth = '1px'; // Reset border width
|
||||
input.style.boxShadow = 'none'; // Reset box shadow
|
||||
|
||||
if (!value || value.trim() === '') {
|
||||
input.style.color = '#6b7280';
|
||||
return;
|
||||
}
|
||||
|
||||
// Couleurs configurées
|
||||
if (GRADING_CONFIG.scale_values[value]) {
|
||||
const config = GRADING_CONFIG.scale_values[value];
|
||||
input.style.color = config.color;
|
||||
input.style.backgroundColor = this.hexToRgba(config.color, 0.1);
|
||||
input.style.borderColor = this.hexToRgba(config.color, 0.3);
|
||||
input.style.fontWeight = 'bold';
|
||||
|
||||
if (isNaN(value)) input.style.fontStyle = 'italic';
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isValid) {
|
||||
input.style.color = '#dc2626';
|
||||
input.style.backgroundColor = '#fef2f2';
|
||||
input.style.borderColor = '#fca5a5';
|
||||
input.style.color = '#ffffff';
|
||||
input.style.backgroundColor = '#dc2626';
|
||||
input.style.borderColor = '#dc2626';
|
||||
input.style.borderWidth = '2px';
|
||||
input.style.fontWeight = 'bold';
|
||||
input.style.boxShadow = '0 0 0 3px rgba(220, 38, 38, 0.3)';
|
||||
|
||||
const message = type === 'score' ?
|
||||
'Valeur autorisée : 0, 1, 2, 3 ou valeurs spéciales' :
|
||||
@@ -566,22 +676,43 @@ class ColorManager {
|
||||
return;
|
||||
}
|
||||
|
||||
// Gradient pour notes numériques
|
||||
// Pour les scores : toujours utiliser les couleurs configurées de l'échelle
|
||||
if (type === 'score') {
|
||||
if (GRADING_CONFIG.scale_values[value]) {
|
||||
const config = GRADING_CONFIG.scale_values[value];
|
||||
input.style.color = '#374151';
|
||||
input.style.backgroundColor = this.hexToRgba(config.color, 0.25);
|
||||
input.style.borderColor = this.hexToRgba(config.color, 0.5);
|
||||
input.style.fontWeight = 'bold';
|
||||
|
||||
if (isNaN(value)) input.style.fontStyle = 'italic';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Pour les notes : d'abord vérifier les valeurs spéciales, sinon utiliser le dégradé
|
||||
if (type === 'notes') {
|
||||
const percentage = Math.min(100, (parseFloat(value) / maxPoints) * 100);
|
||||
const colors = ['#ef4444', '#f6d32d', '#22c55e', '#059669'];
|
||||
const thresholds = [40, 60, 80];
|
||||
|
||||
let colorIndex = 0;
|
||||
for (let i = 0; i < thresholds.length; i++) {
|
||||
if (percentage >= thresholds[i]) colorIndex = i + 1;
|
||||
// Valeurs spéciales (non numériques comme ".", "d", etc.)
|
||||
if (GRADING_CONFIG.scale_values[value] && isNaN(value)) {
|
||||
const config = GRADING_CONFIG.scale_values[value];
|
||||
input.style.color = '#374151';
|
||||
input.style.backgroundColor = this.hexToRgba(config.color, 0.25);
|
||||
input.style.borderColor = this.hexToRgba(config.color, 0.5);
|
||||
input.style.fontWeight = 'bold';
|
||||
input.style.fontStyle = 'italic';
|
||||
return;
|
||||
}
|
||||
|
||||
const noteColor = colors[colorIndex];
|
||||
input.style.color = noteColor;
|
||||
input.style.backgroundColor = this.hexToRgba(noteColor, 0.1);
|
||||
input.style.borderColor = this.hexToRgba(noteColor, 0.3);
|
||||
input.style.fontWeight = 'bold';
|
||||
// Notes numériques : utiliser le dégradé HSL
|
||||
const numValue = parseFloat(value);
|
||||
if (!isNaN(numValue)) {
|
||||
const noteColor = window.getNotesGradientColor(numValue, maxPoints);
|
||||
input.style.color = '#374151';
|
||||
input.style.backgroundColor = this.hexToRgba(noteColor, 0.25);
|
||||
input.style.borderColor = this.hexToRgba(noteColor, 0.5);
|
||||
input.style.fontWeight = 'bold';
|
||||
input.style.fontStyle = 'normal';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user