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
Executable
+96
View File
@@ -0,0 +1,96 @@
#!/usr/bin/env python3
import random
import io
from fpdf import FPDF
def generer_nombre():
"""Génère un nombre naturel entre 1 et 9"""
return random.randint(1, 9)
def evaluer_expression(expr):
"""Évalue une expression mathématique en remplaçant × par *"""
try:
# Remplacer × par * pour l'évaluation
expr_eval = expr.replace('×', '*')
return eval(expr_eval)
except:
return -1 # En cas d'erreur, considérer comme négatif
def generer_expression():
"""Génère une expression aléatoire respectant les contraintes"""
# On choisit aléatoirement un type d'expression (avec ou sans parenthèses)
# On vérifie que le résultat est positif
expressions_positives = [
lambda: f"({generer_nombre()} + {generer_nombre()}) × {generer_nombre()} - {generer_nombre()}",
lambda: f"{generer_nombre()} + {generer_nombre()} × {generer_nombre()} - {generer_nombre()}",
lambda: f"({generer_nombre()} + {generer_nombre()}) × ({generer_nombre()} + {generer_nombre()})",
lambda: f"{generer_nombre()} + {generer_nombre()} × {generer_nombre()} - {generer_nombre()} + {generer_nombre()}",
lambda: f"({generer_nombre()} + {generer_nombre()}) × {generer_nombre()} - {generer_nombre()} + {generer_nombre()}",
lambda: f"{generer_nombre()} × {generer_nombre()} + {generer_nombre()} - {generer_nombre()} × {generer_nombre()}",
lambda: f"({generer_nombre()} + {generer_nombre()}) + {generer_nombre()} × {generer_nombre()} - {generer_nombre()}",
lambda: f"{generer_nombre()} + {generer_nombre()} × ({generer_nombre()} + {generer_nombre()}) - {generer_nombre()}",
lambda: f"({generer_nombre()} + {generer_nombre()}) + {generer_nombre()} × {generer_nombre()} - {generer_nombre()}",
lambda: f"{generer_nombre()} × ({generer_nombre()} + {generer_nombre()}) + {generer_nombre()} - {generer_nombre()}",
lambda: f"{generer_nombre()} + {generer_nombre()} × {generer_nombre()} - {generer_nombre()} × {generer_nombre()} + {generer_nombre()}",
lambda: f"({generer_nombre()} + {generer_nombre()}) × {generer_nombre()} - {generer_nombre()} + {generer_nombre()} - {generer_nombre()}",
]
# Essayer jusqu'à ce qu'on obtienne une expression avec résultat positif
while True:
choix = random.choice(expressions_positives)
expr = choix()
resultat = evaluer_expression(expr)
if resultat >= 0:
return expr
def generer_feuille_exercices(n_exercices=20):
"""Génère une feuille d'exercices en format 2 colonnes"""
expressions = [generer_expression() for _ in range(n_exercices)]
# Format 2 colonnes : on affiche par paires
output = ""
for i in range(0, n_exercices, 2):
expr1 = f"{i+1}. {expressions[i]}"
expr2 = f"{i+2}. {expressions[i+1]}" if i+1 < len(expressions) else ""
# Alignement pour deux colonnes
output += f"{expr1:<36} {expr2}\n" + " " * 36 + " \n" * 3
return output
def generer_pdf_exercices_en_memoire(n_exercices=20):
"""Génère une feuille d'exercices au format PDF en mémoire"""
pdf = FPDF()
pdf.add_page()
pdf.set_font("Helvetica", size=12)
# Titre
pdf.cell(200, 10, text="Feuille d'exercices d'opérations", new_x="LMARGIN", new_y="NEXT", align='C')
pdf.ln(10)
# Contenu en 2 colonnes
expressions = [generer_expression() for _ in range(n_exercices)]
for i in range(0, n_exercices, 2):
# Colonne 1
expr1 = f"{i+1}. {expressions[i]}"
pdf.cell(90, 10, text=expr1, border=0, align='L')
# Colonne 2
if i+1 < len(expressions):
expr2 = f"{i+2}. {expressions[i+1]}"
pdf.cell(90, 10, text=expr2, border=0, align='L')
pdf.ln(10)
# Espacement supplémentaire - ajout de deux lignes vides supplémentaires
pdf.ln(15)
# Retourner les données PDF en mémoire
return pdf.output()
def generer_pdf_exercices(n_exercices=20, output_file="exercices.pdf"):
"""Génère une feuille d'exercices au format PDF"""
pdf_data = generer_pdf_exercices_en_memoire(n_exercices)
# Sauvegarde du PDF
with open(output_file, "wb") as f:
f.write(pdf_data)
return output_file
+42
View File
@@ -6,6 +6,7 @@ import io
import boto3 import boto3
import zipfile import zipfile
import tempfile import tempfile
import importlib.util
from botocore.exceptions import ClientError from botocore.exceptions import ClientError
from typing import List from typing import List
from fastapi import FastAPI, Request, Form from fastapi import FastAPI, Request, Form
@@ -16,6 +17,9 @@ from fpdf import FPDF
import logging import logging
# Import the functions from gen_op.py
from app.gen_op import generer_pdf_exercices_en_memoire
# Custom filter to exclude health check logs # Custom filter to exclude health check logs
class HealthCheckFilter(logging.Filter): class HealthCheckFilter(logging.Filter):
def filter(self, record: logging.LogRecord) -> bool: def filter(self, record: logging.LogRecord) -> bool:
@@ -163,6 +167,9 @@ class ExerciseRequest(BaseModel):
max_table: int max_table: int
num_exercises: int = 15 num_exercises: int = 15
class OperationExerciseRequest(BaseModel):
num_exercises: int = 20
def generate_exercises( def generate_exercises(
min_table: int, max_table: int, num_exercises: int = 15 min_table: int, max_table: int, num_exercises: int = 15
@@ -305,6 +312,26 @@ def create_math_exercises_pdf(
return pdf_filename return pdf_filename
def create_operation_exercises_pdf(num_exercises: int = 20) -> str:
"""Crée un fichier PDF avec des exercices d'opérations et l'upload sur S3"""
import datetime
# Add timestamp to filename
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
pdf_filename = f"exercices_operations_{num_exercises}_exercices_{timestamp}.pdf"
# Generate PDF in memory using gen-op functions
pdf_data = generer_pdf_exercices_en_memoire(num_exercises)
# Upload to S3
upload_success = upload_to_s3(pdf_data, S3_BUCKET_NAME, pdf_filename, "application/pdf")
if not upload_success:
raise Exception("Failed to upload PDF to S3")
return pdf_filename
@app.get("/", response_class=HTMLResponse) @app.get("/", response_class=HTMLResponse)
async def read_root(request: Request): async def read_root(request: Request):
return templates.TemplateResponse("index.html", {"request": request}) return templates.TemplateResponse("index.html", {"request": request})
@@ -333,6 +360,21 @@ async def generate_exercises_endpoint(request: ExerciseRequest):
return {"error": f"Erreur lors de la création du PDF: {str(e)}"} return {"error": f"Erreur lors de la création du PDF: {str(e)}"}
@app.post("/generate-operations")
async def generate_operation_exercises_endpoint(request: OperationExerciseRequest):
try:
if request.num_exercises < 1:
return {"error": "Le nombre d'exercices doit être supérieur à 0"}
pdf_filename = create_operation_exercises_pdf(request.num_exercises)
# Return redirect to automatically download the file
return RedirectResponse(url=f"/download/{pdf_filename}", status_code=303)
except Exception as e:
return {"error": f"Erreur lors de la création du PDF: {str(e)}"}
@app.get("/download/{filename}") @app.get("/download/{filename}")
async def download_pdf(filename: str): async def download_pdf(filename: str):
# Download file from S3 # Download file from S3
+133 -6
View File
@@ -33,14 +33,15 @@
<div class="col-lg-8"> <div class="col-lg-8">
<div class="text-center mb-5"> <div class="text-center mb-5">
<h1 class="display-4 fw-bold">Générateur d'Exercices de Mathématiques</h1> <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> </div>
<!-- Multiplication/Division Exercises Section -->
<div class="card"> <div class="card">
<div class="card-body"> <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"> <form id="exerciseForm">
<div class="mb-3"> <div class="mb-3">
<label for="minTable" class="form-label">Table minimale</label> <label for="minTable" class="form-label">Table minimale</label>
@@ -62,7 +63,7 @@
<div class="d-grid"> <div class="d-grid">
<button type="submit" class="btn btn-primary btn-lg" id="generateBtn"> <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> <span id="spinner" class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span>
</button> </button>
</div> </div>
@@ -70,6 +71,27 @@
</div> </div>
</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 --> <!-- Existing PDFs Section -->
<div class="card mb-4"> <div class="card mb-4">
<div class="card-body"> <div class="card-body">
@@ -161,8 +183,40 @@
</svg> </svg>
</div> </div>
<div> <div>
<h5 class="fw-bold">Trois Colonnes</h5> <h5 class="fw-bold">Colonnes Variables</h5>
<p>Les exercices sont présentés en trois colonnes pour économiser du papier.</p> <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> </div>
</div> </div>
@@ -522,7 +576,80 @@
resultContainer.classList.remove('d-none'); resultContainer.classList.remove('d-none');
} finally { } finally {
// Reset button state // 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'); spinner.classList.add('d-none');
generateBtn.disabled = false; generateBtn.disabled = false;
} }
@@ -13,7 +13,7 @@ resources:
images: images:
- name: math-exercises - name: math-exercises
newName: harbor.cl1.parano.ch/library/math-exercice newName: harbor.cl1.parano.ch/library/math-exercice
newTag: 1.0.8 newTag: 1.0.9
# Production-specific labels # Production-specific labels