feat: improve assessment_grading page
This commit is contained in:
@@ -258,7 +258,7 @@
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">Type de notation</label>
|
||||
<select class="element-grading-type block w-full border border-gray-300 rounded-md px-2 py-1 text-sm focus:ring-purple-500 focus:border-purple-500" required>
|
||||
<option value="">Choisir...</option>
|
||||
<option value="points">Points</option>
|
||||
<option value="notes">Points</option>
|
||||
<option value="score">Score</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -100,6 +100,37 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filtre des élèves -->
|
||||
<div class="bg-white shadow rounded-lg overflow-hidden mb-4">
|
||||
<div class="px-4 py-3 border-b border-gray-200">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-sm font-medium text-gray-700">Filtrer les élèves</h3>
|
||||
<span id="student-count" class="text-xs text-gray-500">{{ students|length }} élève(s)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<div class="relative">
|
||||
<input type="text"
|
||||
id="student-filter"
|
||||
placeholder="Rechercher un élève par nom ou prénom..."
|
||||
class="w-full px-3 py-2 pl-10 text-sm border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200">
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<svg class="h-5 w-5 text-gray-400" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<button type="button"
|
||||
id="clear-filter"
|
||||
class="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 hover:text-gray-600 hidden"
|
||||
onclick="clearStudentFilter()">
|
||||
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tableau de saisie -->
|
||||
<div class="bg-white shadow rounded-lg overflow-hidden">
|
||||
<div class="px-4 py-3 border-b border-gray-200">
|
||||
@@ -108,47 +139,94 @@
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200 table-fixed">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th scope="col" class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider sticky left-0 bg-gray-50 border-r border-gray-200 w-48">
|
||||
<!-- En-têtes des exercices -->
|
||||
<thead class="bg-gradient-to-r from-indigo-50 to-purple-50">
|
||||
<tr class="border-b-2 border-indigo-200">
|
||||
<th scope="col" class="px-3 py-1.5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider sticky left-0 bg-gradient-to-r from-indigo-50 to-purple-50 border-r border-indigo-200 w-48">
|
||||
Élève
|
||||
</th>
|
||||
{% set current_exercise = '' %}
|
||||
{% set exercise_elements = {} %}
|
||||
{% for element in grading_elements %}
|
||||
<th scope="col" class="grading-header px-2 py-2 text-center text-xs font-medium text-gray-500 uppercase tracking-wider min-w-28">
|
||||
<div class="element-label text-xs font-semibold text-gray-900">{{ element.label }}</div>
|
||||
<div class="element-type font-normal text-xs mt-1">
|
||||
{% if element.exercise.title not in exercise_elements %}
|
||||
{% set _ = exercise_elements.update({element.exercise.title: []}) %}
|
||||
{% endif %}
|
||||
{% set _ = exercise_elements[element.exercise.title].append(element) %}
|
||||
{% endfor %}
|
||||
|
||||
{% for exercise_title, elements in exercise_elements.items() %}
|
||||
<th scope="colgroup" colspan="{{ elements|length }}" class="px-2 py-1.5 text-center text-sm font-bold text-indigo-900 bg-gradient-to-r from-indigo-100 to-purple-100 border-x border-indigo-300">
|
||||
{{ exercise_title }}
|
||||
<div class="text-xs font-normal text-indigo-700 mt-0.5">{{ elements|length }} élément{{ 's' if elements|length > 1 else '' }}</div>
|
||||
</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<!-- En-têtes des éléments de notation -->
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th scope="col" class="px-3 py-1.5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider sticky left-0 bg-gray-50 border-r border-gray-200 w-48">
|
||||
<!-- Vide pour alignement -->
|
||||
</th>
|
||||
{% set current_exercise = '' %}
|
||||
{% for element in grading_elements %}
|
||||
{% if element.exercise.title != current_exercise %}
|
||||
{% set current_exercise = element.exercise.title %}
|
||||
{% set exercise_color = 'border-l-4 border-' + ['blue', 'green', 'purple', 'orange', 'pink'][loop.index0 % 5] + '-400' %}
|
||||
{% endif %}
|
||||
|
||||
<th scope="col" class="grading-header px-2 py-1.5 text-center text-xs font-medium text-gray-500 uppercase tracking-wider min-w-28 {{ exercise_color }} bg-gradient-to-b from-gray-50 to-gray-100">
|
||||
<div class="element-label text-xs font-semibold text-gray-900 truncate">{{ element.label }}</div>
|
||||
|
||||
{% if element.description %}
|
||||
<div class="text-xs text-gray-600 mt-1 leading-tight">{{ element.description }}</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="element-type font-normal text-xs mt-1 flex justify-center">
|
||||
{% if element.grading_type == 'score' %}
|
||||
<span class="badge-score inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800">
|
||||
<span class="badge-score inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-purple-100 text-purple-800">
|
||||
0-3
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="badge-notes inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
||||
<span class="badge-notes inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-800">
|
||||
/{{ element.max_points }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if element.skill %}
|
||||
<div class="text-xs text-gray-400 mt-1 truncate">{{ element.skill }}</div>
|
||||
<div class="text-xs text-gray-400 mt-0.5 truncate" title="{{ element.skill }}">{{ element.skill }}</div>
|
||||
{% endif %}
|
||||
</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-100">
|
||||
<tbody class="bg-white divide-y divide-gray-50">
|
||||
{% for student in students %}
|
||||
<tr class="hover:bg-gray-50 text-sm">
|
||||
<td class="px-3 py-2 whitespace-nowrap text-sm font-medium text-gray-900 sticky left-0 bg-white border-r border-gray-200">
|
||||
{{ student.first_name }} {{ student.last_name }}
|
||||
<tr class="student-row hover:bg-gray-25 text-sm transition-colors duration-150" data-student-name="{{ (student.first_name + ' ' + student.last_name)|lower }}">
|
||||
<td class="px-3 py-1.5 whitespace-nowrap text-sm font-medium text-gray-900 sticky left-0 bg-white border-r border-gray-200">
|
||||
<div class="flex items-center">
|
||||
<div class="text-xs text-gray-500 w-6 student-index">{{ loop.index }}</div>
|
||||
<div>{{ student.first_name }} {{ student.last_name }}</div>
|
||||
</div>
|
||||
</td>
|
||||
{% set current_exercise = '' %}
|
||||
{% for element in grading_elements %}
|
||||
{% if element.exercise.title != current_exercise %}
|
||||
{% set current_exercise = element.exercise.title %}
|
||||
{% set exercise_bg_color = ['bg-blue-25', 'bg-green-25', 'bg-purple-25', 'bg-orange-25', 'bg-pink-25'][loop.index0 % 5] %}
|
||||
{% set exercise_border_color = ['border-l-2 border-blue-300', 'border-l-2 border-green-300', 'border-l-2 border-purple-300', 'border-l-2 border-orange-300', 'border-l-2 border-pink-300'][loop.index0 % 5] %}
|
||||
{% endif %}
|
||||
|
||||
{% set grade_key = student.id ~ '_' ~ element.id %}
|
||||
{% set existing_grade = existing_grades.get(grade_key) %}
|
||||
<td class="px-1 py-2 whitespace-nowrap text-center">
|
||||
<td class="px-1 py-1.5 whitespace-nowrap text-center {{ exercise_border_color if loop.first or (grading_elements[loop.index0-1].exercise.title != element.exercise.title) else '' }} {{ exercise_bg_color }}">
|
||||
<div class="space-y-1">
|
||||
<!-- Champs de saisie unifiés -->
|
||||
{% if element.grading_type == 'score' %}
|
||||
<select name="grade_{{ student.id }}_{{ element.id }}"
|
||||
class="grading-input block w-full text-xs border-gray-300 rounded-md focus:ring-2 focus:ring-purple-500 focus:border-purple-500 transition-all duration-200 py-1"
|
||||
class="grading-input block w-full text-xs border-gray-300 rounded focus:ring-2 focus:ring-purple-500 focus:border-purple-500 transition-all duration-200 py-0.5 h-7"
|
||||
data-type="score"
|
||||
data-student-id="{{ student.id }}"
|
||||
data-element-id="{{ element.id }}"
|
||||
@@ -173,14 +251,14 @@
|
||||
<input type="text"
|
||||
name="grade_{{ student.id }}_{{ element.id }}"
|
||||
value="{% if existing_grade %}{{ existing_grade.value }}{% endif %}"
|
||||
class="grading-input block w-full text-xs border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-center transition-all duration-200 py-1"
|
||||
class="grading-input block w-full text-xs border-gray-300 rounded focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-center transition-all duration-200 py-0.5 h-7"
|
||||
data-type="notes"
|
||||
data-max-points="{{ element.max_points }}"
|
||||
data-student-id="{{ student.id }}"
|
||||
data-element-id="{{ element.id }}"
|
||||
data-row="{{ loop.index0 }}"
|
||||
data-col="{{ loop.index0 }}"
|
||||
placeholder="0-{{ element.max_points }} ou {% for v in scale_values.keys() if not v.isdigit() %}{{ v }}{% if not loop.last %} {% endif %}{% endfor %}"
|
||||
placeholder="0-{{ element.max_points }}"
|
||||
oninput="handleGradeChange(this)"
|
||||
onfocus="handleGradeFocus(this)"
|
||||
onkeydown="handleGradeKeydown(event, this)">
|
||||
@@ -188,12 +266,13 @@
|
||||
<input type="text"
|
||||
name="comment_{{ student.id }}_{{ element.id }}"
|
||||
value="{% if existing_grade and existing_grade.comment %}{{ existing_grade.comment }}{% endif %}"
|
||||
class="comment-input block w-full text-xs border-gray-300 rounded focus:ring-blue-500 focus:border-blue-500 transition-all duration-200 py-0.5"
|
||||
class="comment-input block w-full text-xs border-gray-200 rounded focus:ring-blue-400 focus:border-blue-400 transition-all duration-200 py-0.5 h-6 bg-gray-25"
|
||||
data-student-id="{{ student.id }}"
|
||||
data-element-id="{{ element.id }}"
|
||||
data-row="{{ loop.index0 }}"
|
||||
data-col="{{ loop.index0 }}"
|
||||
placeholder="Commentaire (optionnel)"
|
||||
placeholder="💬"
|
||||
title="Commentaire (optionnel)"
|
||||
onkeydown="handleCommentKeydown(event, this)">
|
||||
</div>
|
||||
</td>
|
||||
@@ -278,6 +357,7 @@ let totalCols = {{ grading_elements|length }};
|
||||
let unsavedChanges = new Set();
|
||||
let undoStack = [];
|
||||
let isAutoSaving = false;
|
||||
let filterIsActive = false; // Nouvelle variable pour tracker l'état du filtre
|
||||
|
||||
// Liste dynamique des valeurs spéciales (exclut les scores 0-3)
|
||||
const SPECIAL_VALUES_KEYS = Object.keys(GRADING_CONFIG.scale_values).filter(key => !['0', '1', '2', '3'].includes(key));
|
||||
@@ -286,9 +366,14 @@ const SPECIAL_VALUES_KEYS = Object.keys(GRADING_CONFIG.scale_values).filter(key
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
setupKeyboardNavigation();
|
||||
updateProgressIndicator();
|
||||
focusFirstInput();
|
||||
setupAutosave();
|
||||
applyInitialColors();
|
||||
setupStudentFilter();
|
||||
|
||||
// NE PAS auto-focus - laisser l'utilisateur choisir où cliquer
|
||||
// setTimeout(() => {
|
||||
// focusFirstInput();
|
||||
// }, 100);
|
||||
});
|
||||
|
||||
// Applique les couleurs aux champs déjà remplis au chargement
|
||||
@@ -309,6 +394,12 @@ function applyInitialColors() {
|
||||
function setupKeyboardNavigation() {
|
||||
// Raccourcis globaux
|
||||
document.addEventListener('keydown', function(e) {
|
||||
// PROTECTION ABSOLUE : Ne JAMAIS intercepter si on est dans le champ de filtre
|
||||
if (e.target.id === 'student-filter' || filterIsActive) {
|
||||
console.log('Navigation bloquée - filtre actif');
|
||||
return;
|
||||
}
|
||||
|
||||
// F1 : Aide
|
||||
if (e.key === 'F1') {
|
||||
e.preventDefault();
|
||||
@@ -345,6 +436,12 @@ function handleGradeFocus(input) {
|
||||
|
||||
// Gestion des touches pour les champs de notes
|
||||
function handleGradeKeydown(event, input) {
|
||||
// PROTECTION : Ne pas traiter si le filtre est actif
|
||||
if (filterIsActive) {
|
||||
console.log('Événement bloqué - filtre actif');
|
||||
return;
|
||||
}
|
||||
|
||||
const e = event;
|
||||
|
||||
// Échap : Vider le champ
|
||||
@@ -402,6 +499,11 @@ function handleCommentKeydown(event, input) {
|
||||
|
||||
// Navigation vers le champ suivant
|
||||
function navigateToNext(direction) {
|
||||
// Ne pas naviguer si le filtre est actif
|
||||
if (filterIsActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
let newRow = currentRow;
|
||||
let newCol = currentCol;
|
||||
|
||||
@@ -626,6 +728,11 @@ function updateSaveStatus() {
|
||||
|
||||
// Focuser le premier champ
|
||||
function focusFirstInput() {
|
||||
// Ne pas auto-focus si le filtre est actif ou si l'utilisateur interagit avec le filtre
|
||||
if (filterIsActive || (document.activeElement && document.activeElement.id === 'student-filter')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const firstInput = document.querySelector('.grading-input');
|
||||
if (firstInput) {
|
||||
firstInput.focus();
|
||||
@@ -764,6 +871,166 @@ function showToast(message, type = 'success') {
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Configuration du filtre des élèves
|
||||
function setupStudentFilter() {
|
||||
const filterInput = document.getElementById('student-filter');
|
||||
const clearButton = document.getElementById('clear-filter');
|
||||
const studentCount = document.getElementById('student-count');
|
||||
|
||||
if (!filterInput) return;
|
||||
|
||||
console.log('Configuration du filtre des élèves');
|
||||
|
||||
// Isoler seulement les événements de navigation, pas les événements fonctionnels
|
||||
['keydown', 'keyup', 'keypress'].forEach(eventType => {
|
||||
filterInput.addEventListener(eventType, function(e) {
|
||||
// Ne bloquer que les événements de navigation, pas les caractères normaux
|
||||
if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Tab'].includes(e.key)) {
|
||||
e.stopImmediatePropagation();
|
||||
}
|
||||
}, true);
|
||||
});
|
||||
|
||||
// Gestion du focus
|
||||
filterInput.addEventListener('focus', function(e) {
|
||||
filterIsActive = true;
|
||||
this.select();
|
||||
console.log('🔍 Filtre FOCUS - actif:', filterIsActive);
|
||||
});
|
||||
|
||||
// Gestion du blur
|
||||
filterInput.addEventListener('blur', function(e) {
|
||||
setTimeout(() => {
|
||||
if (document.activeElement !== filterInput) {
|
||||
filterIsActive = false;
|
||||
console.log('🔍 Filtre BLUR - inactif:', filterIsActive);
|
||||
}
|
||||
}, 50);
|
||||
});
|
||||
|
||||
// Gestion de la saisie - SANS bloquer l'événement
|
||||
filterInput.addEventListener('input', function(e) {
|
||||
filterIsActive = true;
|
||||
const searchTerm = this.value.toLowerCase().trim();
|
||||
console.log('🔍 Filtre INPUT:', searchTerm, 'Longueur:', searchTerm.length);
|
||||
|
||||
// Appeler la fonction de filtrage
|
||||
filterStudents(searchTerm);
|
||||
|
||||
// Gérer le bouton de nettoyage
|
||||
if (searchTerm) {
|
||||
clearButton.classList.remove('hidden');
|
||||
} else {
|
||||
clearButton.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
// Gestion des touches spéciales
|
||||
filterInput.addEventListener('keydown', function(e) {
|
||||
console.log('🔍 Filtre KEYDOWN:', e.key);
|
||||
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
clearStudentFilter();
|
||||
} else if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
filterIsActive = false;
|
||||
const firstVisibleInput = document.querySelector('.student-row:not([style*="display: none"]) .grading-input');
|
||||
if (firstVisibleInput) {
|
||||
firstVisibleInput.focus();
|
||||
}
|
||||
}
|
||||
// Pour les flèches et Tab, empêcher la navigation
|
||||
else if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Tab'].includes(e.key)) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Filtrer les élèves selon le terme de recherche
|
||||
function filterStudents(searchTerm) {
|
||||
console.log('🔍 filterStudents appelée avec:', searchTerm);
|
||||
|
||||
const studentRows = document.querySelectorAll('.student-row');
|
||||
const studentCount = document.getElementById('student-count');
|
||||
let visibleCount = 0;
|
||||
let visibleIndex = 1;
|
||||
|
||||
console.log('🔍 Nombre de lignes d\'élèves trouvées:', studentRows.length);
|
||||
|
||||
studentRows.forEach((row, index) => {
|
||||
const studentName = row.dataset.studentName;
|
||||
const isVisible = !searchTerm || studentName.includes(searchTerm);
|
||||
|
||||
console.log(`🔍 Élève ${index + 1}: "${studentName}" - Visible: ${isVisible}`);
|
||||
|
||||
if (isVisible) {
|
||||
row.style.display = '';
|
||||
visibleCount++;
|
||||
// Mettre à jour l'index visible
|
||||
const indexEl = row.querySelector('.student-index');
|
||||
if (indexEl) {
|
||||
indexEl.textContent = visibleIndex;
|
||||
}
|
||||
visibleIndex++;
|
||||
} else {
|
||||
row.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
console.log('🔍 Résultats du filtrage:', visibleCount, 'élèves visibles sur', studentRows.length);
|
||||
|
||||
// Mettre à jour le compteur
|
||||
if (studentCount) {
|
||||
const totalStudents = studentRows.length;
|
||||
if (searchTerm) {
|
||||
studentCount.textContent = `${visibleCount} / ${totalStudents} élève(s)`;
|
||||
studentCount.className = visibleCount === 0 ? 'text-xs text-red-500' : 'text-xs text-blue-600';
|
||||
} else {
|
||||
studentCount.textContent = `${totalStudents} élève(s)`;
|
||||
studentCount.className = 'text-xs text-gray-500';
|
||||
}
|
||||
}
|
||||
|
||||
// Recalculer les variables globales pour la navigation
|
||||
totalRows = visibleCount;
|
||||
|
||||
// Réinitialiser la position si l'élève actuel n'est plus visible
|
||||
const currentRowEl = document.querySelector(`.student-row:nth-child(${currentRow + 1})`);
|
||||
if (currentRowEl && currentRowEl.style.display === 'none') {
|
||||
// Trouver le premier élève visible
|
||||
const firstVisibleRow = document.querySelector('.student-row:not([style*="display: none"])');
|
||||
if (firstVisibleRow) {
|
||||
const firstVisibleInput = firstVisibleRow.querySelector('.grading-input');
|
||||
if (firstVisibleInput && !filterIsActive) {
|
||||
firstVisibleInput.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateProgressIndicator();
|
||||
}
|
||||
|
||||
// Nettoyer le filtre
|
||||
function clearStudentFilter() {
|
||||
const filterInput = document.getElementById('student-filter');
|
||||
const clearButton = document.getElementById('clear-filter');
|
||||
|
||||
if (filterInput) {
|
||||
filterInput.value = '';
|
||||
filterStudents('');
|
||||
clearButton.classList.add('hidden');
|
||||
// Garder le focus et l'état actif
|
||||
filterIsActive = true;
|
||||
setTimeout(() => {
|
||||
filterInput.focus();
|
||||
}, 10);
|
||||
}
|
||||
}
|
||||
|
||||
// Gestion de la fermeture de la page avec modifications non sauvegardées
|
||||
window.addEventListener('beforeunload', function(e) {
|
||||
if (unsavedChanges.size > 0) {
|
||||
|
||||
@@ -86,10 +86,10 @@
|
||||
name="default_grading_system"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
<option value="points" {% if default_grading_system == 'points' %}selected{% endif %}>
|
||||
<option value="notes" {% if default_grading_system == 'notes' %}selected{% endif %}>
|
||||
Points (notation classique)
|
||||
</option>
|
||||
<option value="competences" {% if default_grading_system == 'competences' %}selected{% endif %}>
|
||||
<option value="score" {% if default_grading_system == 'score' %}selected{% endif %}>
|
||||
Compétences (échelle 0-3)
|
||||
</option>
|
||||
</select>
|
||||
|
||||
Reference in New Issue
Block a user