feat: add web interface for math exercise generator

- Create FastAPI backend with endpoints for generating math exercises
- Build Bootstrap frontend with responsive UI for exercise configuration
- Implement PDF generation functionality identical to original script
- Add README with installation and usage instructions
- Update requirements.txt with web dependencies
- Configure .gitignore to exclude compiled files and generated PDFs
This commit is contained in:
2025-09-03 21:25:45 +02:00
parent be6b2b6716
commit fd2f296f44
5 changed files with 496 additions and 0 deletions

18
.gitignore vendored
View File

@@ -1 +1,19 @@
*env
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# Generated PDF files
app/static/*.pdf
# IDE files
.vscode/
.idea/
*.swp
*.swo
# OS files
.DS_Store
Thumbs.db

52
README.md Normal file
View File

@@ -0,0 +1,52 @@
# Générateur d'Exercices de Mathématiques - Version Web
Application web permettant de générer des exercices de multiplication et division personnalisés au format PDF.
## Fonctionnalités
- Génération d'exercices de multiplication et division mélangés
- Personnalisation des tables (minimale et maximale)
- Choix du nombre d'exercices à générer
- Format PDF prêt à l'impression
- Mise en page en trois colonnes pour économiser du papier
- Interface utilisateur responsive avec Bootstrap
## Installation
1. Installer les dépendances :
```bash
pip install -r requirements.txt
```
## Lancement de l'application
```bash
uvicorn app.main:app --reload
```
L'application sera accessible à l'adresse : http://localhost:8000
## Utilisation
1. Accédez à l'interface web
2. Configurez les paramètres :
- Table minimale : la plus petite table de multiplication à inclure
- Table maximale : la plus grande table de multiplication à inclure
- Nombre d'exercices : nombre total d'exercices à générer
3. Cliquez sur "Générer le PDF"
4. Téléchargez et imprimez le fichier PDF généré
## Structure du projet
- `app/main.py` : Application FastAPI principale
- `app/templates/` : Templates HTML
- `app/static/` : Fichiers statiques (PDF générés)
- `generate_math_exercises.py` : Version originale en ligne de commande
## Technologies utilisées
- Python 3
- FastAPI (backend)
- Bootstrap 5 (frontend)
- Jinja2 (templating)
- fpdf2 (génération PDF)

203
app/main.py Normal file
View File

@@ -0,0 +1,203 @@
#!/usr/bin/env python3
import random
import os
from typing import List
from fastapi import FastAPI, Request, Response
from fastapi.responses import HTMLResponse, FileResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from pydantic import BaseModel
from fpdf import FPDF
app = FastAPI()
# Mount static files and templates
app.mount("/static", StaticFiles(directory="app/static"), name="static")
templates = Jinja2Templates(directory="app/templates")
class MathExercisesPDF(FPDF):
def header(self):
self.set_font('Helvetica', 'B', 16)
self.cell(0, 10, 'Exercices de Multiplication et Division', 0, 1, 'C', new_x="LMARGIN", new_y="NEXT")
self.ln(10)
def footer(self):
self.set_y(-15)
self.set_font('Helvetica', 'I', 8)
self.cell(0, 10, f'Page {self.page_no()}', 0, 0, 'C', new_x="RIGHT", new_y="TOP")
class ExerciseRequest(BaseModel):
min_table: int
max_table: int
num_exercises: int = 15
def generate_exercises(min_table: int, max_table: int, num_exercises: int = 15) -> List[str]:
"""Génère des exercices de multiplication et division aléatoires mélangés sans doublons"""
exercises = []
used_operations = set() # Pour éviter les doublons
# Générer le nombre exact d'exercices demandé
attempts = 0
max_attempts = num_exercises * 10 # Limite pour éviter une boucle infinie
while len(exercises) < num_exercises and attempts < max_attempts:
attempts += 1
# Choisir deux nombres aléatoires entre min_table et max_table
a = random.randint(min_table, max_table)
b = random.randint(min_table, max_table)
result = a * b
# Choisir aléatoirement le type d'exercice (seulement multiplication ou division)
exercise_type = random.choice(['multiplication', 'division'])
if exercise_type == 'multiplication':
# Exercice de multiplication
operation_key = f"mult_{a}_{b}" # Clé unique pour cette opération
if operation_key not in used_operations:
exercise = f"{a} · {b} = ____"
exercises.append(exercise)
used_operations.add(operation_key)
else: # division
# Exercice de division
divisor = random.choice([a, b])
operation_key = f"div_{result}_{divisor}" # Clé unique pour cette opération
if operation_key not in used_operations:
exercise = f"{result} : {divisor} = ____"
exercises.append(exercise)
used_operations.add(operation_key)
# Si nous n'avons pas pu générer suffisamment d'exercices uniques,
# compléter avec des variations
if len(exercises) < num_exercises:
remaining = num_exercises - len(exercises)
for _ in range(remaining):
# Générer des variations avec des nombres légèrement différents
a = random.randint(min_table, max_table)
b = random.randint(min_table, max_table)
result = a * b
# Choisir aléatoirement le type d'exercice
exercise_type = random.choice(['multiplication', 'division'])
if exercise_type == 'multiplication':
exercise = f"{a} · {b} = ____"
else: # division
divisor = random.choice([a, b])
exercise = f"{result} : {divisor} = ____"
exercises.append(exercise)
return exercises
def create_math_exercises_pdf(min_table: int, max_table: int, num_exercises: int = 15) -> str:
"""Crée un fichier PDF avec des exercices de mathématiques mélangés en 3 colonnes"""
pdf = MathExercisesPDF()
pdf.add_page()
pdf.set_font('Helvetica', '', 12)
# Ajouter des informations sur la plage de tables
if min_table == max_table:
table_info = f'Tables de multiplication et division pour {min_table}'
else:
table_info = f'Tables de multiplication et division de {min_table} à {max_table}'
pdf.cell(0, 10, table_info, 0, 1, 'C', new_x="LMARGIN", new_y="NEXT")
pdf.ln(5)
# Générer les exercices
exercises = generate_exercises(min_table, max_table, num_exercises)
# Pas d'en-têtes de colonnes
# Répartir les exercices en 3 colonnes
num_per_column = (num_exercises + 2) // 3 # Arrondi vers le haut
col1_exercises = exercises[:num_per_column]
col2_exercises = exercises[num_per_column:num_per_column*2]
col3_exercises = exercises[num_per_column*2:]
# Ajouter les exercices numérotés de 1 à n dans les colonnes
max_rows = max(len(col1_exercises), len(col2_exercises), len(col3_exercises))
for i in range(max_rows):
# Colonne 1
if i < len(col1_exercises):
exercise_num = i + 1
col1_text = f"{exercise_num}. {col1_exercises[i]}"
else:
col1_text = ""
# Colonne 2
if i < len(col2_exercises):
exercise_num = i + 1 + len(col1_exercises)
col2_text = f"{exercise_num}. {col2_exercises[i]}"
else:
col2_text = ""
# Colonne 3
if i < len(col3_exercises):
exercise_num = i + 1 + len(col1_exercises) + len(col2_exercises)
col3_text = f"{exercise_num}. {col3_exercises[i]}"
else:
col3_text = ""
# Ajouter la ligne
pdf.cell(60, 10, col1_text, 0, 0, 'L', new_x="RIGHT", new_y="TOP")
pdf.cell(60, 10, col2_text, 0, 0, 'L', new_x="RIGHT", new_y="TOP")
pdf.cell(60, 10, col3_text, 0, 1, 'L', new_x="LMARGIN", new_y="NEXT")
# Sauvegarder le PDF
if min_table == max_table:
filename = f'app/static/exercices_mathematiques_table_{min_table}_{num_exercises}_exercices.pdf'
else:
filename = f'app/static/exercices_mathematiques_tables_{min_table}_a_{max_table}_{num_exercises}_exercices.pdf'
pdf.output(filename)
return filename
@app.get("/", response_class=HTMLResponse)
async def read_root(request: Request):
return templates.TemplateResponse("index.html", {"request": request})
@app.post("/generate")
async def generate_exercises_endpoint(request: ExerciseRequest):
try:
if request.min_table < 1 or request.max_table < 1:
return {"error": "Les tables doivent être supérieures à 0"}
if request.min_table > request.max_table:
return {"error": "La table minimale doit être inférieure ou égale à la table maximale"}
if request.num_exercises < 1:
return {"error": "Le nombre d'exercices doit être supérieur à 0"}
filename = create_math_exercises_pdf(
request.min_table,
request.max_table,
request.num_exercises
)
# Extract just the filename for the download link
pdf_filename = os.path.basename(filename)
return {
"message": "PDF généré avec succès!",
"pdf_filename": pdf_filename,
"min_table": request.min_table,
"max_table": request.max_table,
"num_exercises": request.num_exercises
}
except Exception as e:
return {"error": f"Erreur lors de la création du PDF: {str(e)}"}
@app.get("/download/{filename}")
async def download_pdf(filename: str):
file_path = f"app/static/{filename}"
if os.path.exists(file_path):
return FileResponse(
path=file_path,
media_type='application/pdf',
filename=filename
)
return {"error": "File not found"}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)

219
app/templates/index.html Normal file
View File

@@ -0,0 +1,219 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Générateur d'Exercices de Mathématiques</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
.card {
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
transition: 0.3s;
margin-bottom: 20px;
}
.card:hover {
box-shadow: 0 8px 16px rgba(0,0,0,0.2);
}
.exercise-result {
background-color: #f8f9fa;
border-radius: 5px;
padding: 15px;
margin-top: 20px;
}
.feature-icon {
width: 3rem;
height: 3rem;
margin-right: 0.75rem;
}
</style>
</head>
<body>
<div class="container mt-5 mb-5">
<div class="row justify-content-center">
<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>
</div>
<div class="card">
<div class="card-body">
<h5 class="card-title">Paramètres des Exercices</h5>
<form id="exerciseForm">
<div class="mb-3">
<label for="minTable" class="form-label">Table minimale</label>
<input type="number" class="form-control" id="minTable" min="1" max="12" value="1" required>
<div class="form-text">La plus petite table de multiplication à inclure</div>
</div>
<div class="mb-3">
<label for="maxTable" class="form-label">Table maximale</label>
<input type="number" class="form-control" id="maxTable" min="1" max="12" value="10" required>
<div class="form-text">La plus grande table de multiplication à inclure</div>
</div>
<div class="mb-3">
<label for="numExercises" class="form-label">Nombre d'exercices</label>
<input type="number" class="form-control" id="numExercises" min="1" max="100" value="15" required>
<div class="form-text">Nombre total d'exercices à générer (entre 1 et 100)</div>
</div>
<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="spinner" class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span>
</button>
</div>
</form>
</div>
</div>
<div id="resultContainer" class="exercise-result d-none">
<h5>Résultat</h5>
<div id="resultMessage"></div>
<div id="downloadLinkContainer" class="mt-3 d-none">
<a id="downloadLink" class="btn btn-success" href="#" download>Télécharger le PDF</a>
</div>
</div>
<div class="row mt-5">
<div class="col-md-6">
<div class="d-flex align-items-start">
<div class="feature-icon bg-primary 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-x-lg text-primary" viewBox="0 0 16 16">
<path d="M2.146 2.854a.5.5 0 1 1 .708-.708L8 7.293l5.146-5.147a.5.5 0 0 1 .708.708L8.707 8l5.147 5.146a.5.5 0 0 1-.708.708L8 8.707l-5.146 5.147a.5.5 0 0 1-.708-.708L7.293 8 2.146 2.854Z"/>
</svg>
</div>
<div>
<h5 class="fw-bold">Multiplications</h5>
<p>Exercices de multiplication avec des opérateurs aléatoires de la table spécifiée.</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-slash-lg text-success" viewBox="0 0 16 16">
<path d="M3.5 1.5l9 13"/>
</svg>
</div>
<div>
<h5 class="fw-bold">Divisions</h5>
<p>Exercices de division correspondants aux tables de multiplication sélectionnées.</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-info 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-file-earmark-pdf text-info" viewBox="0 0 16 16">
<path d="M14 14V4.5L9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2zM9.5 3A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5v2z"/>
</svg>
</div>
<div>
<h5 class="fw-bold">Format PDF</h5>
<p>Générez des fichiers PDF prêts à imprimer avec une mise en page organisée.</p>
</div>
</div>
</div>
<div class="col-md-6">
<div class="d-flex align-items-start">
<div class="feature-icon bg-warning 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-columns-gap text-warning" viewBox="0 0 16 16">
<path d="M6 1H1v3h5V1zM1 0a1 1 0 0 0-1 1v3a1 1 0 0 0 1 1h5a1 1 0 0 0 1-1V1a1 1 0 0 0-1-1H1zm14 12h-5v3h5v-3zm-5-1a1 1 0 0 0-1 1v3a1 1 0 0 0 1 1h5a1 1 0 0 0 1-1v-3a1 1 0 0 0-1-1h-5zM6 8H1v7h5V8zm-1 1a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1h5a1 1 0 0 0 1-1v-5a1 1 0 0 0-1-1H5zm9-9h-5v5h5V1zm-1 1a1 1 0 0 0-1 1v3a1 1 0 0 0 1 1h5a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1h-5z"/>
</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>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<footer class="bg-light text-center text-muted py-4 mt-5">
<div class="container">
<p>Générateur d'Exercices de Mathématiques &copy; 2023</p>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
<script>
document.getElementById('exerciseForm').addEventListener('submit', async function(e) {
e.preventDefault();
const minTable = parseInt(document.getElementById('minTable').value);
const maxTable = parseInt(document.getElementById('maxTable').value);
const numExercises = parseInt(document.getElementById('numExercises').value);
const generateBtn = document.getElementById('generateBtn');
const buttonText = document.getElementById('buttonText');
const spinner = document.getElementById('spinner');
// Show loading state
buttonText.textContent = 'Génération en cours...';
spinner.classList.remove('d-none');
generateBtn.disabled = true;
try {
const response = await fetch('/generate', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
min_table: minTable,
max_table: maxTable,
num_exercises: numExercises
})
});
const result = await response.json();
const resultContainer = document.getElementById('resultContainer');
const resultMessage = document.getElementById('resultMessage');
const downloadLinkContainer = document.getElementById('downloadLinkContainer');
const downloadLink = document.getElementById('downloadLink');
if (result.error) {
resultMessage.innerHTML = `<div class="alert alert-danger">${result.error}</div>`;
downloadLinkContainer.classList.add('d-none');
} else {
resultMessage.innerHTML = `
<div class="alert alert-success">
<h6>${result.message}</h6>
<ul class="mb-0">
<li>Tables de ${result.min_table} à ${result.max_table}</li>
<li>${result.num_exercises} exercices générés</li>
</ul>
</div>
`;
downloadLink.href = `/download/${result.pdf_filename}`;
downloadLinkContainer.classList.remove('d-none');
}
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';
spinner.classList.add('d-none');
generateBtn.disabled = false;
}
});
</script>
</body>
</html>

View File

@@ -2,3 +2,7 @@ defusedxml==0.7.1
fonttools==4.59.2
fpdf2==2.8.4
pillow==11.3.0
fastapi==0.115.0
uvicorn==0.30.6
jinja2==3.1.4
python-multipart==0.0.9