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 zipfile
import tempfile
import importlib.util
from botocore.exceptions import ClientError
from typing import List
from fastapi import FastAPI, Request, Form
@@ -16,6 +17,9 @@ from fpdf import FPDF
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
class HealthCheckFilter(logging.Filter):
def filter(self, record: logging.LogRecord) -> bool:
@@ -163,6 +167,9 @@ class ExerciseRequest(BaseModel):
max_table: int
num_exercises: int = 15
class OperationExerciseRequest(BaseModel):
num_exercises: int = 20
def generate_exercises(
min_table: int, max_table: int, num_exercises: int = 15
@@ -305,6 +312,26 @@ def create_math_exercises_pdf(
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)
async def read_root(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)}"}
@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}")
async def download_pdf(filename: str):
# Download file from S3
+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;
}
@@ -13,7 +13,7 @@ resources:
images:
- name: math-exercises
newName: harbor.cl1.parano.ch/library/math-exercice
newTag: 1.0.8
newTag: 1.0.9
# Production-specific labels