feat: improve assessment creation and edition

This commit is contained in:
2025-08-04 08:25:21 +02:00
parent e0a3ea2764
commit a0608e27aa
20 changed files with 1265 additions and 519 deletions

View File

@@ -13,8 +13,8 @@
<p class="text-gray-600">{{ assessment.class_group.name }} - {{ assessment.date.strftime('%d/%m/%Y') }}</p>
</div>
<div class="flex space-x-3">
<a href="{{ url_for('assessments.edit', id=assessment.id) }}" class="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded-md text-sm font-medium transition-colors">
Modifier
<a href="{{ url_for('assessments.edit', id=assessment.id) }}" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium transition-colors">
Modifier l'évaluation complète
</a>
<button onclick="if(confirm('Êtes-vous sûr de vouloir supprimer cette évaluation ?')) { document.getElementById('delete-form').submit(); }"
class="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-md text-sm font-medium transition-colors">
@@ -62,9 +62,9 @@
<div class="bg-white shadow rounded-lg">
<div class="px-6 py-4 border-b border-gray-200 flex justify-between items-center">
<h2 class="text-lg font-medium text-gray-900">Exercices</h2>
<a href="{{ url_for('exercises.new', assessment_id=assessment.id) }}" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium transition-colors">
Ajouter un exercice
</a>
<span class="text-sm text-gray-500">
Utilisez "Modifier l'évaluation complète" pour ajouter des exercices
</span>
</div>
<div class="px-6 py-4">
{% if assessment.exercises %}
@@ -85,9 +85,6 @@
<a href="{{ url_for('exercises.detail', assessment_id=assessment.id, id=exercise.id) }}" class="text-blue-600 hover:text-blue-800 text-sm font-medium">
Voir détails
</a>
<a href="{{ url_for('exercises.edit', assessment_id=assessment.id, id=exercise.id) }}" class="text-gray-600 hover:text-gray-800 text-sm font-medium">
Modifier
</a>
</div>
</div>
</div>
@@ -101,8 +98,8 @@
<h3 class="mt-2 text-sm font-medium text-gray-900">Aucun exercice</h3>
<p class="mt-1 text-sm text-gray-500">Commencez par ajouter le premier exercice de cette évaluation.</p>
<div class="mt-6">
<a href="{{ url_for('exercises.new', assessment_id=assessment.id) }}" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium transition-colors">
Ajouter un exercice
<a href="{{ url_for('assessments.edit', id=assessment.id) }}" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium transition-colors">
Modifier l'évaluation complète
</a>
</div>
</div>

View File

@@ -1,102 +0,0 @@
{% extends "base.html" %}
{% block title %}{{ title }} - Gestion Scolaire{% endblock %}
{% block content %}
<div class="max-w-2xl mx-auto">
<div class="mb-6">
<a href="{{ url_for('assessments.list') }}" class="text-blue-600 hover:text-blue-800 text-sm font-medium">
← Retour aux évaluations
</a>
</div>
<div class="bg-white shadow rounded-lg">
<div class="px-6 py-4 border-b border-gray-200">
<h1 class="text-xl font-semibold text-gray-900">{{ title }}</h1>
</div>
<form method="POST" class="px-6 py-4 space-y-6">
{{ form.hidden_tag() }}
<div>
<label for="{{ form.title.id }}" class="block text-sm font-medium text-gray-700 mb-1">
{{ form.title.label.text }}
</label>
{{ form.title(class="block w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500") }}
{% if form.title.errors %}
<div class="mt-1 text-sm text-red-600">
{% for error in form.title.errors %}
<p>{{ error }}</p>
{% endfor %}
</div>
{% endif %}
</div>
<div>
<label for="{{ form.description.id }}" class="block text-sm font-medium text-gray-700 mb-1">
{{ form.description.label.text }}
</label>
{{ form.description(class="block w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500", rows="3") }}
{% if form.description.errors %}
<div class="mt-1 text-sm text-red-600">
{% for error in form.description.errors %}
<p>{{ error }}</p>
{% endfor %}
</div>
{% endif %}
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label for="{{ form.date.id }}" class="block text-sm font-medium text-gray-700 mb-1">
{{ form.date.label.text }}
</label>
{{ form.date(class="block w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500") }}
{% if form.date.errors %}
<div class="mt-1 text-sm text-red-600">
{% for error in form.date.errors %}
<p>{{ error }}</p>
{% endfor %}
</div>
{% endif %}
</div>
<div>
<label for="{{ form.coefficient.id }}" class="block text-sm font-medium text-gray-700 mb-1">
{{ form.coefficient.label.text }}
</label>
{{ form.coefficient(class="block w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500", step="0.1") }}
{% if form.coefficient.errors %}
<div class="mt-1 text-sm text-red-600">
{% for error in form.coefficient.errors %}
<p>{{ error }}</p>
{% endfor %}
</div>
{% endif %}
</div>
</div>
<div>
<label for="{{ form.class_group_id.id }}" class="block text-sm font-medium text-gray-700 mb-1">
{{ form.class_group_id.label.text }}
</label>
{{ form.class_group_id(class="block w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500") }}
{% if form.class_group_id.errors %}
<div class="mt-1 text-sm text-red-600">
{% for error in form.class_group_id.errors %}
<p>{{ error }}</p>
{% endfor %}
</div>
{% endif %}
</div>
<div class="flex justify-end space-x-3 pt-4 border-t border-gray-200">
<a href="{{ url_for('assessments.list') }}" class="px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors">
Annuler
</a>
{{ form.submit(class="px-4 py-2 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700 transition-colors") }}
</div>
</form>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,532 @@
{% extends "base.html" %}
{% block title %}{{ title }} - Gestion Scolaire{% endblock %}
{% block content %}
<div class="max-w-6xl mx-auto">
<div class="mb-6">
{% if is_edit %}
<a href="{{ url_for('assessments.detail', id=assessment.id) }}" class="text-blue-600 hover:text-blue-800 text-sm font-medium">
← Retour à l'évaluation
</a>
{% else %}
<a href="{{ url_for('assessments.list') }}" class="text-blue-600 hover:text-blue-800 text-sm font-medium">
← Retour aux évaluations
</a>
{% endif %}
</div>
<div class="bg-white shadow rounded-lg">
<div class="px-6 py-4 border-b border-gray-200">
<h1 class="text-xl font-semibold text-gray-900">{{ title }}</h1>
{% if is_edit %}
<p class="text-sm text-gray-600 mt-1">Modifiez votre évaluation complète avec exercices et éléments de notation</p>
{% else %}
<p class="text-sm text-gray-600 mt-1">Créez votre évaluation complète avec exercices et éléments de notation</p>
{% endif %}
</div>
<form id="unified-form" method="POST" class="px-6 py-6 space-y-8">
{{ form.hidden_tag() }}
<!-- Section Évaluation -->
<div class="bg-blue-50 border border-blue-200 rounded-lg p-6">
<h2 class="text-lg font-medium text-blue-900 mb-4">📝 Informations de l'évaluation</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label for="{{ form.title.id }}" class="block text-sm font-medium text-gray-700 mb-1">
{{ form.title.label.text }}
</label>
{{ form.title(class="block w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500") }}
{% if form.title.errors %}
<div class="mt-1 text-sm text-red-600">
{% for error in form.title.errors %}
<p>{{ error }}</p>
{% endfor %}
</div>
{% endif %}
</div>
<div>
<label for="{{ form.class_group_id.id }}" class="block text-sm font-medium text-gray-700 mb-1">
{{ form.class_group_id.label.text }}
</label>
{{ form.class_group_id(class="block w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500") }}
{% if form.class_group_id.errors %}
<div class="mt-1 text-sm text-red-600">
{% for error in form.class_group_id.errors %}
<p>{{ error }}</p>
{% endfor %}
</div>
{% endif %}
</div>
<div>
<label for="{{ form.date.id }}" class="block text-sm font-medium text-gray-700 mb-1">
{{ form.date.label.text }}
</label>
{{ form.date(class="block w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500") }}
{% if form.date.errors %}
<div class="mt-1 text-sm text-red-600">
{% for error in form.date.errors %}
<p>{{ error }}</p>
{% endfor %}
</div>
{% endif %}
</div>
<div>
<label for="{{ form.coefficient.id }}" class="block text-sm font-medium text-gray-700 mb-1">
{{ form.coefficient.label.text }}
</label>
{{ form.coefficient(class="block w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500", step="0.5") }}
{% if form.coefficient.errors %}
<div class="mt-1 text-sm text-red-600">
{% for error in form.coefficient.errors %}
<p>{{ error }}</p>
{% endfor %}
</div>
{% endif %}
</div>
</div>
<div class="mt-4">
<label for="{{ form.description.id }}" class="block text-sm font-medium text-gray-700 mb-1">
{{ form.description.label.text }}
</label>
{{ form.description(class="block w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500", rows="3") }}
{% if form.description.errors %}
<div class="mt-1 text-sm text-red-600">
{% for error in form.description.errors %}
<p>{{ error }}</p>
{% endfor %}
</div>
{% endif %}
</div>
</div>
<!-- Section Exercices -->
<div class="bg-green-50 border border-green-200 rounded-lg p-6">
<div class="mb-4">
<h2 class="text-lg font-medium text-green-900">🏃 Exercices</h2>
</div>
<div id="exercises-container" class="space-y-4">
<!-- Les exercices seront ajoutés ici dynamiquement -->
</div>
<div id="no-exercises" class="text-center py-8 text-green-700">
<p class="text-sm">Aucun exercice ajouté. Utilisez le bouton ci-dessous pour commencer.</p>
</div>
<!-- Bouton d'ajout placé après la liste pour une meilleure navigation clavier -->
<div class="mt-4 pt-4 border-t border-green-200">
<button type="button" id="add-exercise" class="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-md text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2">
Ajouter un exercice
</button>
</div>
</div>
<!-- Guide d'aide -->
<div class="bg-gray-50 border border-gray-200 rounded-lg p-4">
<h3 class="text-sm font-medium text-gray-900 mb-2">💡 Guide rapide</h3>
<div class="text-xs text-gray-700 space-y-1">
<p><strong>Points :</strong> Notation classique (ex: 2.5/4 points)</p>
<p><strong>Score :</strong> Évaluation par niveaux (0=non acquis, 1=en cours, 2=acquis, 3=expert, .=non évalué)</p>
<p><strong>Ordre :</strong> Les exercices et éléments seront affichés dans l'ordre numérique</p>
</div>
</div>
<!-- Actions -->
<div class="flex justify-end space-x-3 pt-4 border-t border-gray-200">
{% if is_edit %}
<a href="{{ url_for('assessments.detail', id=assessment.id) }}" class="px-6 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2">
Annuler
</a>
<button type="submit" class="px-6 py-2 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700 transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">
💾 Modifier l'évaluation complète
</button>
{% else %}
<a href="{{ url_for('assessments.list') }}" class="px-6 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2">
Annuler
</a>
<button type="submit" class="px-6 py-2 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700 transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">
💾 Créer l'évaluation complète
</button>
{% endif %}
</div>
</form>
</div>
</div>
<!-- Template pour un exercice -->
<template id="exercise-template">
<div class="exercise-item border border-green-300 rounded-lg p-4 bg-white">
<div class="flex justify-between items-start mb-4">
<h3 class="text-md font-medium text-green-800">Exercice <span class="exercise-number"></span></h3>
<button type="button" class="remove-exercise text-red-600 hover:text-red-800 text-sm font-medium">
Supprimer
</button>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Titre de l'exercice</label>
<input type="text" class="exercise-title block w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-green-500 focus:border-green-500" required>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Ordre</label>
<input type="number" class="exercise-order block w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-green-500 focus:border-green-500" min="1" required>
</div>
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">Description (optionnel)</label>
<textarea class="exercise-description block w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-green-500 focus:border-green-500" rows="2"></textarea>
</div>
<!-- Éléments de notation -->
<div class="bg-purple-50 border border-purple-200 rounded p-4">
<div class="mb-3">
<h4 class="text-sm font-medium text-purple-900">Éléments de notation</h4>
</div>
<div class="grading-elements-container space-y-3">
<!-- Les éléments de notation seront ajoutés ici -->
</div>
<div class="no-grading-elements text-center py-4 text-purple-700 text-xs">
Aucun élément de notation. Utilisez le bouton ci-dessous pour commencer.
</div>
<!-- Bouton d'ajout placé après la liste pour une meilleure navigation clavier -->
<div class="mt-3 pt-3 border-t border-purple-200">
<button type="button" class="add-grading-element bg-purple-600 hover:bg-purple-700 text-white px-3 py-1 rounded text-xs font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-1">
Ajouter élément
</button>
</div>
</div>
</div>
</template>
<!-- Template pour un élément de notation -->
<template id="grading-element-template">
<div class="grading-element-item border border-purple-300 rounded p-3 bg-white">
<div class="flex justify-between items-start mb-3">
<h5 class="text-sm font-medium text-purple-800">Élément de notation</h5>
<button type="button" class="remove-grading-element text-red-600 hover:text-red-800 text-xs font-medium">
Supprimer
</button>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">Label</label>
<input type="text" class="element-label block w-full border border-gray-300 rounded-md px-2 py-1 text-sm focus:ring-purple-500 focus:border-purple-500" required>
</div>
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">Compétence</label>
<input type="text" class="element-skill block w-full border border-gray-300 rounded-md px-2 py-1 text-sm focus:ring-purple-500 focus:border-purple-500">
</div>
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">Points max</label>
<input type="number" step="0.1" min="0" class="element-max-points block w-full border border-gray-300 rounded-md px-2 py-1 text-sm focus:ring-purple-500 focus:border-purple-500" required>
</div>
<div>
<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="score">Score</option>
</select>
</div>
</div>
<div class="mt-3">
<label class="block text-xs font-medium text-gray-700 mb-1">Description (optionnel)</label>
<textarea class="element-description block w-full border border-gray-300 rounded-md px-2 py-1 text-sm focus:ring-purple-500 focus:border-purple-500" rows="2"></textarea>
</div>
</div>
</template>
<script>
let exerciseCounter = 0;
document.addEventListener('DOMContentLoaded', function() {
const addExerciseBtn = document.getElementById('add-exercise');
const exercisesContainer = document.getElementById('exercises-container');
const noExercisesMsg = document.getElementById('no-exercises');
const form = document.getElementById('unified-form');
// Pré-remplir les données en mode édition
{% if is_edit %}
loadExistingData();
{% endif %}
// Ajouter un exercice
addExerciseBtn.addEventListener('click', function() {
addExercise();
});
// Soumission du formulaire
form.addEventListener('submit', function(e) {
e.preventDefault();
submitForm();
});
function addExercise() {
exerciseCounter++;
const template = document.getElementById('exercise-template');
const exerciseDiv = template.content.cloneNode(true);
// Mettre à jour le numéro d'exercice
exerciseDiv.querySelector('.exercise-number').textContent = exerciseCounter;
exerciseDiv.querySelector('.exercise-order').value = exerciseCounter;
// Ajouter les event listeners
const removeBtn = exerciseDiv.querySelector('.remove-exercise');
removeBtn.addEventListener('click', function() {
removeExercise(this);
});
const addElementBtn = exerciseDiv.querySelector('.add-grading-element');
addElementBtn.addEventListener('click', function() {
addGradingElement(this);
});
exercisesContainer.appendChild(exerciseDiv);
updateExercisesVisibility();
// Focus automatique sur le champ titre du nouvel exercice pour faciliter la navigation clavier
setTimeout(() => {
const titleInput = exercisesContainer.lastElementChild.querySelector('.exercise-title');
if (titleInput) {
titleInput.focus();
}
}, 100);
}
function removeExercise(btn) {
btn.closest('.exercise-item').remove();
updateExercisesVisibility();
renumberExercises();
}
function addGradingElement(btn) {
const exerciseDiv = btn.closest('.exercise-item');
const elementsContainer = exerciseDiv.querySelector('.grading-elements-container');
const noElementsMsg = exerciseDiv.querySelector('.no-grading-elements');
const template = document.getElementById('grading-element-template');
const elementDiv = template.content.cloneNode(true);
// Ajouter l'event listener pour supprimer
const removeBtn = elementDiv.querySelector('.remove-grading-element');
removeBtn.addEventListener('click', function() {
removeGradingElement(this);
});
elementsContainer.appendChild(elementDiv);
noElementsMsg.style.display = 'none';
// Focus automatique sur le champ label du nouvel élément pour faciliter la navigation clavier
setTimeout(() => {
const labelInput = elementsContainer.lastElementChild.querySelector('.element-label');
if (labelInput) {
labelInput.focus();
}
}, 100);
}
function removeGradingElement(btn) {
const exerciseDiv = btn.closest('.exercise-item');
const elementsContainer = exerciseDiv.querySelector('.grading-elements-container');
const noElementsMsg = exerciseDiv.querySelector('.no-grading-elements');
btn.closest('.grading-element-item').remove();
if (elementsContainer.children.length === 0) {
noElementsMsg.style.display = 'block';
}
}
function updateExercisesVisibility() {
const hasExercises = exercisesContainer.children.length > 0;
noExercisesMsg.style.display = hasExercises ? 'none' : 'block';
}
function renumberExercises() {
const exercises = exercisesContainer.querySelectorAll('.exercise-item');
exercises.forEach((exercise, index) => {
const number = index + 1;
exercise.querySelector('.exercise-number').textContent = number;
exercise.querySelector('.exercise-order').value = number;
});
exerciseCounter = exercises.length;
}
function collectFormData() {
const exercises = [];
const exerciseItems = exercisesContainer.querySelectorAll('.exercise-item');
exerciseItems.forEach(exerciseItem => {
const title = exerciseItem.querySelector('.exercise-title').value;
const description = exerciseItem.querySelector('.exercise-description').value;
const order = parseInt(exerciseItem.querySelector('.exercise-order').value);
if (!title.trim()) return;
const gradingElements = [];
const elementItems = exerciseItem.querySelectorAll('.grading-element-item');
elementItems.forEach(elementItem => {
const label = elementItem.querySelector('.element-label').value;
const skill = elementItem.querySelector('.element-skill').value;
const maxPoints = parseFloat(elementItem.querySelector('.element-max-points').value);
const gradingType = elementItem.querySelector('.element-grading-type').value;
const description = elementItem.querySelector('.element-description').value;
if (!label.trim() || !maxPoints || !gradingType) return;
gradingElements.push({
label: label.trim(),
skill: skill.trim(),
max_points: maxPoints,
grading_type: gradingType,
description: description.trim()
});
});
exercises.push({
title: title.trim(),
description: description.trim(),
order: order,
grading_elements: gradingElements
});
});
return exercises;
}
function submitForm() {
const formData = new FormData(form);
const exercises = collectFormData();
// Validation côté client
if (exercises.length === 0) {
alert('Vous devez ajouter au moins un exercice.');
return;
}
let hasElementsWithoutGrading = false;
exercises.forEach(ex => {
if (ex.grading_elements.length === 0) {
hasElementsWithoutGrading = true;
}
});
if (hasElementsWithoutGrading) {
if (!confirm('Certains exercices n\'ont pas d\'éléments de notation. Voulez-vous continuer ?')) {
return;
}
}
// Envoyer via AJAX
const data = {
title: formData.get('title'),
description: formData.get('description'),
date: formData.get('date'),
class_group_id: formData.get('class_group_id'),
coefficient: formData.get('coefficient'),
exercises: exercises
};
// Ajouter le CSRF token aux données
data.csrf_token = formData.get('csrf_token');
fetch(form.action, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
})
.then(response => response.json())
.then(data => {
if (data.success) {
window.location.href = `/assessments/${data.assessment_id}`;
} else {
alert('Erreur lors de la création : ' + JSON.stringify(data.errors));
}
})
.catch(error => {
console.error('Erreur:', error);
alert('Une erreur est survenue lors de la création de l\'évaluation.');
});
}
{% if is_edit %}
function loadExistingData() {
// Charger les exercices existants
const existingExercises = {{ exercises_json|tojson if exercises_json else "[]" }};
existingExercises.forEach(exercise => {
exerciseCounter++;
const template = document.getElementById('exercise-template');
const exerciseDiv = template.content.cloneNode(true);
// Remplir les données de l'exercice
exerciseDiv.querySelector('.exercise-number').textContent = exerciseCounter;
exerciseDiv.querySelector('.exercise-title').value = exercise.title;
exerciseDiv.querySelector('.exercise-description').value = exercise.description || '';
exerciseDiv.querySelector('.exercise-order').value = exercise.order;
// Ajouter les event listeners
const removeBtn = exerciseDiv.querySelector('.remove-exercise');
removeBtn.addEventListener('click', function() {
removeExercise(this);
});
const addElementBtn = exerciseDiv.querySelector('.add-grading-element');
addElementBtn.addEventListener('click', function() {
addGradingElement(this);
});
const elementsContainer = exerciseDiv.querySelector('.grading-elements-container');
const noElementsMsg = exerciseDiv.querySelector('.no-grading-elements');
// Charger les éléments de notation existants
exercise.grading_elements.forEach(element => {
const elementTemplate = document.getElementById('grading-element-template');
const elementDiv = elementTemplate.content.cloneNode(true);
// Remplir les données de l'élément
elementDiv.querySelector('.element-label').value = element.label;
elementDiv.querySelector('.element-skill').value = element.skill || '';
elementDiv.querySelector('.element-max-points').value = element.max_points;
elementDiv.querySelector('.element-grading-type').value = element.grading_type;
elementDiv.querySelector('.element-description').value = element.description || '';
// Ajouter l'event listener pour supprimer
const removeElementBtn = elementDiv.querySelector('.remove-grading-element');
removeElementBtn.addEventListener('click', function() {
removeGradingElement(this);
});
elementsContainer.appendChild(elementDiv);
});
if (exercise.grading_elements.length > 0) {
noElementsMsg.style.display = 'none';
}
exercisesContainer.appendChild(exerciseDiv);
});
updateExercisesVisibility();
}
{% endif %}
});
</script>
{% endblock %}

View File

@@ -6,9 +6,11 @@
<div class="space-y-6">
<div class="flex justify-between items-center">
<h1 class="text-2xl font-bold text-gray-900">Gestion des évaluations</h1>
<a href="{{ url_for('assessments.new') }}" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md transition-colors">
Nouvelle évaluation
</a>
<div>
<a href="{{ url_for('assessments.new') }}" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md transition-colors text-sm font-medium focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">
✨ Nouvelle évaluation
</a>
</div>
</div>
{% if assessments %}
@@ -42,14 +44,14 @@
</div>
</div>
<div class="flex space-x-2">
<a href="{{ url_for('assessments.detail', id=assessment.id) }}" class="text-indigo-600 hover:text-indigo-900 text-sm font-medium">
Voir détails
<a href="{{ url_for('assessments.detail', id=assessment.id) }}" class="text-indigo-600 hover:text-indigo-900 text-sm font-medium focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-1 rounded-sm px-1 py-1" aria-label="Voir détails de {{ assessment.title }}">
👁️ Voir détails
</a>
<a href="{{ url_for('grading.assessment_grading', assessment_id=assessment.id) }}" class="text-green-600 hover:text-green-900 text-sm font-medium">
Saisir notes
<a href="{{ url_for('grading.assessment_grading', assessment_id=assessment.id) }}" class="text-green-600 hover:text-green-900 text-sm font-medium focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-1 rounded-sm px-1 py-1" aria-label="Saisir notes pour {{ assessment.title }}">
📝 Saisir notes
</a>
<a href="{{ url_for('assessments.edit', id=assessment.id) }}" class="text-gray-600 hover:text-gray-900 text-sm font-medium">
Modifier
<a href="{{ url_for('assessments.edit', id=assessment.id) }}" class="text-gray-600 hover:text-gray-900 text-sm font-medium focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-1 rounded-sm px-1 py-1" aria-label="Modifier {{ assessment.title }}">
✏️ Modifier
</a>
</div>
</div>
@@ -66,8 +68,8 @@
<h3 class="mt-2 text-sm font-medium text-gray-900">Aucune évaluation</h3>
<p class="mt-1 text-sm text-gray-500">Commencez par créer votre première évaluation.</p>
<div class="mt-6">
<a href="{{ url_for('assessments.new') }}" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md transition-colors">
Nouvelle évaluation
<a href="{{ url_for('assessments.new') }}" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md transition-colors text-sm font-medium focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">
✨ Créer votre première évaluation
</a>
</div>
</div>

View File

@@ -5,34 +5,66 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Gestion Scolaire{% endblock %}</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
// Configuration Tailwind pour dark mode et focus
tailwind.config = {
theme: {
extend: {
colors: {
primary: {
50: '#eff6ff',
600: '#2563eb',
700: '#1d4ed8',
}
}
}
}
}
</script>
</head>
<body class="bg-gray-100 min-h-screen">
<nav class="bg-blue-600 text-white shadow-lg">
<body class="bg-gray-100 min-h-screen" role="document">
<nav class="bg-blue-600 text-white shadow-lg" role="navigation" aria-label="Navigation principale">
<div class="max-w-7xl mx-auto px-4">
<div class="flex justify-between items-center py-4">
<div class="flex items-center space-x-4">
<h1 class="text-xl font-bold">
<a href="{{ url_for('index') }}" class="hover:text-blue-200">Gestion Scolaire</a>
<a href="{{ url_for('index') }}" class="hover:text-blue-200 focus:outline-none focus:ring-2 focus:ring-blue-300 focus:ring-offset-2 focus:ring-offset-blue-600 rounded-sm" aria-label="Accueil - Gestion Scolaire">Gestion Scolaire</a>
</h1>
</div>
<div class="flex space-x-6">
<a href="{{ url_for('index') }}" class="hover:text-blue-200 transition-colors">Accueil</a>
<a href="{{ url_for('classes') }}" class="hover:text-blue-200 transition-colors">Classes</a>
<a href="{{ url_for('students') }}" class="hover:text-blue-200 transition-colors">Élèves</a>
<a href="{{ url_for('assessments.list') }}" class="hover:text-blue-200 transition-colors">Évaluations</a>
<div class="flex space-x-6" role="menubar">
<a href="{{ url_for('index') }}" class="hover:text-blue-200 transition-colors focus:outline-none focus:ring-2 focus:ring-blue-300 focus:ring-offset-2 focus:ring-offset-blue-600 rounded-sm px-2 py-1" role="menuitem">Accueil</a>
<a href="{{ url_for('classes') }}" class="hover:text-blue-200 transition-colors focus:outline-none focus:ring-2 focus:ring-blue-300 focus:ring-offset-2 focus:ring-offset-blue-600 rounded-sm px-2 py-1" role="menuitem">Classes</a>
<a href="{{ url_for('students') }}" class="hover:text-blue-200 transition-colors focus:outline-none focus:ring-2 focus:ring-blue-300 focus:ring-offset-2 focus:ring-offset-blue-600 rounded-sm px-2 py-1" role="menuitem">Élèves</a>
<a href="{{ url_for('assessments.list') }}" class="hover:text-blue-200 transition-colors focus:outline-none focus:ring-2 focus:ring-blue-300 focus:ring-offset-2 focus:ring-offset-blue-600 rounded-sm px-2 py-1" role="menuitem">Évaluations</a>
</div>
</div>
</div>
</nav>
<main class="max-w-7xl mx-auto px-4 py-8">
<main class="max-w-7xl mx-auto px-4 py-8" role="main">
<!-- Messages flash améliorés pour l'accessibilité -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="mb-4 p-4 rounded-md {% if category == 'error' %}bg-red-100 text-red-700 border border-red-300{% else %}bg-green-100 text-green-700 border border-green-300{% endif %}">
{{ message }}
</div>
{% endfor %}
<div role="alert" aria-live="polite">
{% for category, message in messages %}
<div class="mb-4 p-4 rounded-md {% if category == 'error' %}bg-red-100 text-red-700 border border-red-300{% else %}bg-green-100 text-green-700 border border-green-300{% endif %}" role="alert">
<div class="flex items-center">
{% if category == 'error' %}
<svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"></path>
</svg>
<span class="sr-only">Erreur:</span>
{% else %}
<svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path>
</svg>
<span class="sr-only">Succès:</span>
{% endif %}
{{ message }}
</div>
</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}

20
templates/error.html Normal file
View File

@@ -0,0 +1,20 @@
{% extends "base.html" %}
{% block title %}Erreur - Gestion Scolaire{% endblock %}
{% block content %}
<div class="max-w-md mx-auto mt-16">
<div class="bg-red-50 border border-red-200 rounded-lg p-6 text-center">
<div class="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100 mb-4">
<svg class="h-6 w-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.732-.833-2.5 0L4.268 18.5c-.77.833.192 2.5 1.732 2.5z"></path>
</svg>
</div>
<h1 class="text-lg font-medium text-gray-900 mb-2">Une erreur s'est produite</h1>
<p class="text-sm text-gray-600 mb-4">{{ error or "Une erreur inattendue s'est produite." }}</p>
<a href="{{ url_for('index') }}" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
Retour à l'accueil
</a>
</div>
</div>
{% endblock %}

View File

@@ -13,18 +13,12 @@
<p class="text-gray-600">{{ assessment.title }} - {{ assessment.class_group.name }}</p>
</div>
<div class="flex space-x-3">
<a href="{{ url_for('exercises.edit', assessment_id=assessment.id, id=exercise.id) }}" class="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded-md text-sm font-medium transition-colors">
Modifier
<a href="{{ url_for('assessments.edit', id=assessment.id) }}" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium transition-colors">
Modifier l'évaluation complète
</a>
<button onclick="if(confirm('Êtes-vous sûr de vouloir supprimer cet exercice ?')) { document.getElementById('delete-form').submit(); }"
class="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-md text-sm font-medium transition-colors">
Supprimer
</button>
</div>
</div>
<form id="delete-form" method="POST" action="{{ url_for('exercises.delete', assessment_id=assessment.id, id=exercise.id) }}" style="display: none;"></form>
<!-- Informations de l'exercice -->
<div class="bg-white shadow rounded-lg">
<div class="px-6 py-4 border-b border-gray-200">
@@ -54,8 +48,8 @@
<div class="bg-white shadow rounded-lg">
<div class="px-6 py-4 border-b border-gray-200 flex justify-between items-center">
<h2 class="text-lg font-medium text-gray-900">Éléments de notation</h2>
<a href="{{ url_for('exercises.new_grading_element', assessment_id=assessment.id, exercise_id=exercise.id) }}" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium transition-colors">
Ajouter un élément
<a href="{{ url_for('assessments.edit', id=assessment.id) }}" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium transition-colors">
Modifier l'évaluation complète
</a>
</div>
<div class="px-6 py-4">
@@ -83,13 +77,9 @@
</div>
</div>
<div class="flex space-x-2 ml-4">
<a href="{{ url_for('exercises.edit_grading_element', assessment_id=assessment.id, exercise_id=exercise.id, id=element.id) }}" class="text-gray-600 hover:text-gray-800 text-sm font-medium">
Modifier
</a>
<button onclick="if(confirm('Êtes-vous sûr de vouloir supprimer cet élément ?')) { document.getElementById('delete-element-{{ element.id }}').submit(); }" class="text-red-600 hover:text-red-800 text-sm font-medium">
Supprimer
</button>
<form id="delete-element-{{ element.id }}" method="POST" action="{{ url_for('exercises.delete_grading_element', assessment_id=assessment.id, exercise_id=exercise.id, id=element.id) }}" style="display: none;"></form>
<span class="text-xs text-gray-500">
Utilisez "Modifier l'évaluation complète" pour modifier
</span>
</div>
</div>
</div>
@@ -103,8 +93,8 @@
<h3 class="mt-2 text-sm font-medium text-gray-900">Aucun élément de notation</h3>
<p class="mt-1 text-sm text-gray-500">Commencez par ajouter le premier élément de notation pour cet exercice.</p>
<div class="mt-6">
<a href="{{ url_for('exercises.new_grading_element', assessment_id=assessment.id, exercise_id=exercise.id) }}" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium transition-colors">
Ajouter un élément
<a href="{{ url_for('assessments.edit', id=assessment.id) }}" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium transition-colors">
Modifier l'évaluation complète
</a>
</div>
</div>

View File

@@ -1,74 +0,0 @@
{% extends "base.html" %}
{% block title %}{{ title }} - Gestion Scolaire{% endblock %}
{% block content %}
<div class="max-w-2xl mx-auto">
<div class="mb-6">
<a href="{{ url_for('assessments.detail', id=assessment.id) }}" class="text-blue-600 hover:text-blue-800 text-sm font-medium">
← Retour à l'évaluation "{{ assessment.title }}"
</a>
</div>
<div class="bg-white shadow rounded-lg">
<div class="px-6 py-4 border-b border-gray-200">
<h1 class="text-xl font-semibold text-gray-900">{{ title }}</h1>
<p class="text-sm text-gray-600 mt-1">Évaluation : {{ assessment.title }} ({{ assessment.class_group.name }})</p>
</div>
<form method="POST" class="px-6 py-4 space-y-6">
{{ form.hidden_tag() }}
<div>
<label for="{{ form.title.id }}" class="block text-sm font-medium text-gray-700 mb-1">
{{ form.title.label.text }}
</label>
{{ form.title(class="block w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500") }}
{% if form.title.errors %}
<div class="mt-1 text-sm text-red-600">
{% for error in form.title.errors %}
<p>{{ error }}</p>
{% endfor %}
</div>
{% endif %}
</div>
<div>
<label for="{{ form.description.id }}" class="block text-sm font-medium text-gray-700 mb-1">
{{ form.description.label.text }}
</label>
{{ form.description(class="block w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500", rows="3") }}
{% if form.description.errors %}
<div class="mt-1 text-sm text-red-600">
{% for error in form.description.errors %}
<p>{{ error }}</p>
{% endfor %}
</div>
{% endif %}
</div>
<div>
<label for="{{ form.order.id }}" class="block text-sm font-medium text-gray-700 mb-1">
{{ form.order.label.text }}
</label>
{{ form.order(class="block w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500") }}
{% if form.order.errors %}
<div class="mt-1 text-sm text-red-600">
{% for error in form.order.errors %}
<p>{{ error }}</p>
{% endfor %}
</div>
{% endif %}
<p class="mt-1 text-xs text-gray-500">L'ordre détermine l'affichage des exercices dans l'évaluation</p>
</div>
<div class="flex justify-end space-x-3 pt-4 border-t border-gray-200">
<a href="{{ url_for('assessments.detail', id=assessment.id) }}" class="px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors">
Annuler
</a>
{{ form.submit(class="px-4 py-2 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700 transition-colors") }}
</div>
</form>
</div>
</div>
{% endblock %}

View File

@@ -1,114 +0,0 @@
{% extends "base.html" %}
{% block title %}{{ title }} - Gestion Scolaire{% endblock %}
{% block content %}
<div class="max-w-2xl mx-auto">
<div class="mb-6">
<a href="{{ url_for('exercises.detail', assessment_id=assessment.id, id=exercise.id) }}" class="text-blue-600 hover:text-blue-800 text-sm font-medium">
← Retour à l'exercice "{{ exercise.title }}"
</a>
</div>
<div class="bg-white shadow rounded-lg">
<div class="px-6 py-4 border-b border-gray-200">
<h1 class="text-xl font-semibold text-gray-900">{{ title }}</h1>
<p class="text-sm text-gray-600 mt-1">{{ assessment.title }} > {{ exercise.title }}</p>
</div>
<form method="POST" class="px-6 py-4 space-y-6">
{{ form.hidden_tag() }}
<div>
<label for="{{ form.label.id }}" class="block text-sm font-medium text-gray-700 mb-1">
{{ form.label.label.text }}
</label>
{{ form.label(class="block w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500") }}
{% if form.label.errors %}
<div class="mt-1 text-sm text-red-600">
{% for error in form.label.errors %}
<p>{{ error }}</p>
{% endfor %}
</div>
{% endif %}
<p class="mt-1 text-xs text-gray-500">Ex: "Calcul de base", "Méthode", "Présentation"</p>
</div>
<div>
<label for="{{ form.description.id }}" class="block text-sm font-medium text-gray-700 mb-1">
{{ form.description.label.text }}
</label>
{{ form.description(class="block w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500", rows="3") }}
{% if form.description.errors %}
<div class="mt-1 text-sm text-red-600">
{% for error in form.description.errors %}
<p>{{ error }}</p>
{% endfor %}
</div>
{% endif %}
</div>
<div>
<label for="{{ form.skill.id }}" class="block text-sm font-medium text-gray-700 mb-1">
{{ form.skill.label.text }}
</label>
{{ form.skill(class="block w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500") }}
{% if form.skill.errors %}
<div class="mt-1 text-sm text-red-600">
{% for error in form.skill.errors %}
<p>{{ error }}</p>
{% endfor %}
</div>
{% endif %}
<p class="mt-1 text-xs text-gray-500">Ex: "Calculer", "Raisonner", "Communiquer"</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label for="{{ form.max_points.id }}" class="block text-sm font-medium text-gray-700 mb-1">
{{ form.max_points.label.text }}
</label>
{{ form.max_points(class="block w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500", step="0.1") }}
{% if form.max_points.errors %}
<div class="mt-1 text-sm text-red-600">
{% for error in form.max_points.errors %}
<p>{{ error }}</p>
{% endfor %}
</div>
{% endif %}
</div>
<div>
<label for="{{ form.grading_type.id }}" class="block text-sm font-medium text-gray-700 mb-1">
{{ form.grading_type.label.text }}
</label>
{{ form.grading_type(class="block w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500") }}
{% if form.grading_type.errors %}
<div class="mt-1 text-sm text-red-600">
{% for error in form.grading_type.errors %}
<p>{{ error }}</p>
{% endfor %}
</div>
{% endif %}
</div>
</div>
<!-- Aide sur les types de notation -->
<div class="bg-blue-50 border border-blue-200 rounded-md p-4">
<h4 class="text-sm font-medium text-blue-900 mb-2">Types de notation</h4>
<div class="text-xs text-blue-800 space-y-1">
<p><strong>Points :</strong> Notation classique (ex: 2.5/4 points)</p>
<p><strong>Score :</strong> Évaluation par niveaux (0=non acquis, 1=en cours, 2=acquis, 3=expert, .=non évalué)</p>
</div>
</div>
<div class="flex justify-end space-x-3 pt-4 border-t border-gray-200">
<a href="{{ url_for('exercises.detail', assessment_id=assessment.id, id=exercise.id) }}" class="px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors">
Annuler
</a>
{{ form.submit(class="px-4 py-2 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700 transition-colors") }}
</div>
</form>
</div>
</div>
{% endblock %}

View File

@@ -114,7 +114,7 @@
<div class="text-sm font-medium text-gray-900">Gérer les élèves</div>
</div>
</a>
<a href="{{ url_for('assessments.list') }}" class="block p-4 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors">
<a href="{{ url_for('assessments.new') }}" class="block p-4 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors">
<div class="text-center">
<div class="w-8 h-8 bg-purple-100 rounded-full flex items-center justify-center mx-auto mb-2">
<svg class="w-4 h-4 text-purple-600" fill="currentColor" viewBox="0 0 20 20">