Files
math-tables/app/main.py
Rene Luria fd2f296f44 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
2025-09-03 21:25:45 +02:00

203 lines
7.7 KiB
Python

#!/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)