Files
notytex/templates/assessment_form_unified.html

554 lines
26 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% 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.trimester.id }}" class="block text-sm font-medium text-gray-700 mb-1">
{{ form.trimester.label.text }}
</label>
{{ form.trimester(class="block w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500") }}
{% if form.trimester.errors %}
<div class="mt-1 text-sm text-red-600">
{% for error in form.trimester.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>
<select 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">
<option value="">Non spécifiée</option>
{% for competence in competences %}
<option value="{{ competence.name }}" data-color="{{ competence.color }}" data-icon="{{ competence.icon }}">
{{ competence.name }}
</option>
{% endfor %}
</select>
</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="notes">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'),
trimester: formData.get('trimester'),
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 %}