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:
+203
@@ -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)
|
||||
Reference in New Issue
Block a user