feat: add operation exercises generation using gen_op.py

Moved gen-op.py to app/gen_op.py and integrated it with the FastAPI application

Added a new section in the UI for generating operation exercises

Updated the PDF generation to work in memory and upload to S3

Bumped version to 1.0.9
This commit is contained in:
2025-09-25 19:10:13 +02:00
parent 8bedd8a97a
commit 28b1c80b50
4 changed files with 272 additions and 7 deletions
+133 -6
View File
@@ -33,14 +33,15 @@
<div class="col-lg-8">
<div class="text-center mb-5">
<h1 class="display-4 fw-bold">Générateur d'Exercices de Mathématiques</h1>
<p class="lead">Créez des exercices de multiplication et division personnalisés en PDF</p>
<p class="lead">Créez des exercices de multiplication, division et opérations complexes personnalisés en PDF</p>
</div>
<!-- Multiplication/Division Exercises Section -->
<div class="card">
<div class="card-body">
<h5 class="card-title">Paramètres des Exercices</h5>
<h5 class="card-title">Paramètres des Exercices (Multiplications/Divisions)</h5>
<form id="exerciseForm">
<div class="mb-3">
<label for="minTable" class="form-label">Table minimale</label>
@@ -62,7 +63,7 @@
<div class="d-grid">
<button type="submit" class="btn btn-primary btn-lg" id="generateBtn">
<span id="buttonText">Générer le PDF</span>
<span id="buttonText">Générer le PDF (Mult/Div)</span>
<span id="spinner" class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span>
</button>
</div>
@@ -70,6 +71,27 @@
</div>
</div>
<!-- Operation Exercises Section -->
<div class="card mt-4">
<div class="card-body">
<h5 class="card-title">Paramètres des Exercices d'Opérations</h5>
<form id="operationExerciseForm">
<div class="mb-3">
<label for="numOperationExercises" class="form-label">Nombre d'exercices</label>
<input type="number" class="form-control" id="numOperationExercises" min="1" max="100" value="20" required>
<div class="form-text">Nombre total d'exercices d'opérations à générer (entre 1 et 100)</div>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-success btn-lg" id="generateOperationBtn">
<span id="operationButtonText">Générer le PDF (Opérations)</span>
<span id="operationSpinner" class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span>
</button>
</div>
</form>
</div>
</div>
<!-- Existing PDFs Section -->
<div class="card mb-4">
<div class="card-body">
@@ -161,8 +183,40 @@
</svg>
</div>
<div>
<h5 class="fw-bold">Trois Colonnes</h5>
<p>Les exercices sont présentés en trois colonnes pour économiser du papier.</p>
<h5 class="fw-bold">Colonnes Variables</h5>
<p>Les exercices de multiplication/division sont présentés en trois colonnes, les opérations complexes en deux colonnes.</p>
</div>
</div>
</div>
</div>
<div class="row mt-3">
<div class="col-md-6">
<div class="d-flex align-items-start">
<div class="feature-icon bg-danger bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-calculator text-danger" viewBox="0 0 16 16">
<path d="M12 1a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h8zM4 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H4z"/>
<path d="M4 2.5a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5h-7a.5.5 0 0 1-.5-.5v-2zm0 4a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5V6.5zm3 0a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5V6.5zm3 0a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5V6.5zm-6 3a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5V9.5zm3 0a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5V9.5zm3 0a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5V9.5zm-6 3a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-1zm3 0a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-1z"/>
</svg>
</div>
<div>
<h5 class="fw-bold">Opérations Complexes</h5>
<p>Générez des exercices avec des expressions mathématiques complexes incluant parenthèses, additions, soustractions et multiplications.</p>
</div>
</div>
</div>
<div class="col-md-6">
<div class="d-flex align-items-start">
<div class="feature-icon bg-success bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-cloud-arrow-down text-success" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M7.646 10.854a.5.5 0 0 0 .708 0l2-2a.5.5 0 0 0-.708-.708L8.5 9.293V5.5a.5.5 0 0 0-1 0v3.793L6.354 8.146a.5.5 0 1 0-.708.708l2 2z"/>
<path d="M4.406 3.342A5.53 5.53 0 0 1 8 2c2.69 0 4.923 2 5.166 4.579C14.758 6.804 16 8.137 16 9.773 16 11.569 14.502 13 12.687 13H3.781C1.708 13 0 11.366 0 9.318c0-1.763 1.266-3.223 2.942-3.593.143-.863.698-1.723 1.464-2.383zm.653.757c-.757.653-1.153 1.44-1.153 2.056v.448l-.445.049C2.064 6.805 1 7.952 1 9.318 1 10.785 2.23 12 3.781 12h8.906C13.98 12 15 10.988 15 9.773c0-1.216-1.02-2.228-2.313-2.228h-.5v-.5C12.188 4.825 10.328 3 8 3a4.53 4.53 0 0 0-2.941 1.1z"/>
</svg>
</div>
<div>
<h5 class="fw-bold">Stockage Cloud</h5>
<p>Tous les fichiers générés sont automatiquement stockés dans le cloud et peuvent être téléchargés ultérieurement.</p>
</div>
</div>
</div>
@@ -522,7 +576,80 @@
resultContainer.classList.remove('d-none');
} finally {
// Reset button state
buttonText.textContent = 'Générer le PDF';
buttonText.textContent = 'Générer le PDF (Mult/Div)';
spinner.classList.add('d-none');
generateBtn.disabled = false;
}
});
// Handle operation exercises form submission
document.getElementById('operationExerciseForm').addEventListener('submit', async function(e) {
e.preventDefault();
const numExercises = parseInt(document.getElementById('numOperationExercises').value);
const generateBtn = document.getElementById('generateOperationBtn');
const buttonText = document.getElementById('operationButtonText');
const spinner = document.getElementById('operationSpinner');
// Show loading state
buttonText.textContent = 'Génération en cours...';
spinner.classList.remove('d-none');
generateBtn.disabled = true;
try {
const response = await fetch('/generate-operations', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
num_exercises: numExercises
})
});
// Check if it's a redirect response (for automatic download)
if (response.redirected) {
// Automatically trigger download
window.location.href = response.url;
// Refresh the PDF list (reset to first page)
setTimeout(() => loadPdfList(1), 1000);
// Show success message
const resultContainer = document.getElementById('resultContainer');
const resultMessage = document.getElementById('resultMessage');
resultMessage.innerHTML = `
<div class="alert alert-success">
<h6>PDF d'opérations généré avec succès!</h6>
<p>Le téléchargement devrait commencer automatiquement.</p>
<ul class="mb-0">
<li>${numExercises} exercices d'opérations générés</li>
</ul>
</div>
`;
resultContainer.classList.remove('d-none');
} else {
// Handle JSON response (error case)
const result = await response.json();
const resultContainer = document.getElementById('resultContainer');
const resultMessage = document.getElementById('resultMessage');
if (result.error) {
resultMessage.innerHTML = `<div class="alert alert-danger">${result.error}</div>`;
}
resultContainer.classList.remove('d-none');
}
} catch (error) {
const resultContainer = document.getElementById('resultContainer');
const resultMessage = document.getElementById('resultMessage');
resultMessage.innerHTML = `<div class="alert alert-danger">Erreur: ${error.message}</div>`;
resultContainer.classList.remove('d-none');
} finally {
// Reset button state
buttonText.textContent = 'Générer le PDF (Opérations)';
spinner.classList.add('d-none');
generateBtn.disabled = false;
}