Compare commits

...

27 Commits

Author SHA1 Message Date
herel e4db258bc5 fix: update version 2025-12-10 11:38:16 +01:00
herel 449adc60e0 feat: dark mode and python 3.14 2025-12-10 11:36:40 +01:00
herel d68dc8411f chore: speedup readyness 2025-10-15 17:25:08 +02:00
herel 60f776c56d chore: service internal traffic policy 2025-10-15 17:24:13 +02:00
herel 7228dc4ec6 chore: add scale-to-zero overlay 2025-10-15 17:11:30 +02:00
herel e4989dcc8d chore: bump production image tag to 1.2.0 2025-10-07 08:47:52 +02:00
herel 3fdbfcf20e feat: replace checkbox with radio buttons for exercise type selection
- Replace the checkbox "Générer uniquement des multiplications" with radio buttons for "Multiplications uniquement" and "Multiplications ET divisions"
- Update JavaScript code to handle radio button values instead of checkbox
- Maintain backward compatibility with old localStorage format
- Ensure all functionality remains intact
2025-10-07 08:46:38 +02:00
herel b17afbb5cd chore: bump production image tag to 1.1.0 2025-10-07 08:34:21 +02:00
herel 711a26c26c chore: bump production image tag to 1.0.10 2025-10-07 08:33:13 +02:00
herel 21c9bfd359 feat: implement localStorage for form parameter persistence
- Add saveFormValues() function to store all form parameters in localStorage
- Add restoreFormValues() function to retrieve form parameters from localStorage
- Modify DOMContentLoaded event to restore form values on page load
- Add calls to saveFormValues() in both form submission handlers
- Add event listeners to save form values whenever any input changes

This enhancement improves user experience by preserving form settings between visits.
2025-10-07 08:29:42 +02:00
herel 3d720ff426 feat: add configurable max_first_operand for multiplication exercises with randomized operand order 2025-10-07 08:23:34 +02:00
herel d9dd1281c3 refactor: move S3 functions to separate file for clarity 2025-10-07 08:12:54 +02:00
herel edefce9703 chore: use cache for pip 2025-10-07 08:09:42 +02:00
herel eb6da3d9cf feat: multi-stage build for slim image 2025-10-07 07:58:08 +02:00
herel bc55b95bda feat: permit to generate only multiplications 2025-10-07 07:55:18 +02:00
herel 7284c60b93 fix: resolve ruff linting issues and improve code formatting 2025-09-26 08:25:02 +02:00
herel 28b1c80b50 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
2025-09-25 19:10:13 +02:00
herel 8bedd8a97a feat: remove footer from PDF generation and update production image tag 2025-09-04 21:09:46 +02:00
herel e9db1b012d chore: enable seccompProfile again 2025-09-04 13:39:49 +02:00
herel 9e2d6eeb12 fix: add tmp volume for bulk download functionality 2025-09-04 13:08:30 +02:00
herel 3f1a3cf7a4 chore: add sops encrypted credentials 2025-09-04 08:43:17 +02:00
herel b3426f7493 feat: add health check endpoint and suppress health check logs
Added a /health endpoint for application health monitoring

Implemented logging filter to suppress health check requests from logs

Updated Dockerfile and Kubernetes deployment to use the new health check endpoint

Incremented production image tag version
2025-09-04 00:42:53 +02:00
herel cadc34f797 chore: update dependencies and bump version to 1.0.5 2025-09-04 00:37:14 +02:00
herel cdaf3d9500 refactor: standardize string quotes and improve code formatting
Updated both main.py and generate_math_exercises.py to use consistent double quotes for strings and improved code formatting with better line breaks for readability. Also added JSON import to main.py for bulk operations.
2025-09-04 00:08:13 +02:00
herel 1d47b5490e chore: update .gitignore 2025-09-03 23:57:41 +02:00
herel 5d11fe9763 chore: simpler network policy 2025-09-03 23:57:25 +02:00
herel e0dddb70ec fix: update network policy to allow S3 traffic and kubelet health checks 2025-09-03 23:45:16 +02:00
19 changed files with 1166 additions and 338 deletions
+1
View File
@@ -18,3 +18,4 @@ app/static/*.pdf
.DS_Store
Thumbs.db
.envrc
s3cfg
+5
View File
@@ -0,0 +1,5 @@
creation_rules:
- path_regex: .*
pgp: >-
7120085E6161EB9ABC2FD0117C26F0446A4D9603,
E21007D7E97694AC900FF3434C6659F378169546
+20 -24
View File
@@ -1,28 +1,25 @@
# Use Python 3.11 slim image as base
FROM python:3.11-slim
# Use Python 3.13 slim image as base
FROM python:3.14-slim AS builder
# Set working directory
WORKDIR /app
# Set environment variables
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
POETRY_NO_INTERACTION=1 \
POETRY_VENV_IN_PROJECT=1 \
POETRY_CACHE_DIR=/tmp/poetry_cache
# Install system dependencies
RUN apt-get update && apt-get install -y \
build-essential \
curl \
&& rm -rf /var/lib/apt/lists/*
# Create and set pip cache directory
ENV PIP_CACHE_DIR=/root/.cache/pip
# Copy requirements file
COPY requirements.txt .
# Install Python dependencies
RUN pip install --no-cache-dir --upgrade pip && \
pip install --no-cache-dir -r requirements.txt
# Install Python dependencies with bind-mounted cache
RUN --mount=type=cache,id=pip,target=/root/.cache/pip \
pip install --no-deps \
--disable-pip-version-check \
--target=/app/site-packages \
-r requirements.txt
FROM python:3.14-slim
COPY --from=builder /app/site-packages /app/site-packages
# Create a non-root user
RUN adduser --disabled-password --gecos '' appuser
@@ -31,8 +28,11 @@ RUN adduser --disabled-password --gecos '' appuser
COPY ./app /app/app
COPY ./generate_math_exercises.py /app/
# Change ownership of the app directory to the non-root user
RUN chown -R appuser:appuser /app
ENV PYTHONPATH=/app/site-packages
ENV PATH=/app/site-packages/bin:/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
# Set working directory
WORKDIR /app
# Switch to non-root user
USER appuser
@@ -40,9 +40,5 @@ USER appuser
# Expose port
EXPOSE 8000
# Health check
HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8000/ || exit 1
# Run the application
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
Executable
+108
View File
@@ -0,0 +1,108 @@
#!/usr/bin/env python3
import random
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 Exception:
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
+248 -205
View File
@@ -1,64 +1,46 @@
#!/usr/bin/env python3
import json
import random
import os
import io
import boto3
import zipfile
import tempfile
from botocore.exceptions import ClientError
from typing import List
from fastapi import FastAPI, Request, Response, Form
from fastapi.responses import HTMLResponse, StreamingResponse
from fastapi import FastAPI, Request, Form
from fastapi.responses import HTMLResponse, RedirectResponse, StreamingResponse
from fastapi.templating import Jinja2Templates
from pydantic import BaseModel
from fpdf import FPDF
app = FastAPI()
import logging
# S3 Configuration
S3_BUCKET_NAME = os.environ.get('S3_BUCKET_NAME', 'math-exercises')
# Import the functions from gen_op.py
from app.gen_op import generer_pdf_exercices_en_memoire
# Import S3 functions
from app.s3_utils import (
S3_BUCKET_NAME,
create_bucket_if_not_exists,
upload_to_s3,
download_from_s3,
list_objects_in_s3,
delete_from_s3,
)
# Custom filter to exclude health check logs
class HealthCheckFilter(logging.Filter):
def filter(self, record: logging.LogRecord) -> bool:
# Exclude health check requests from logs
return "GET /health " not in record.getMessage()
# Apply the filter to Uvicorn's access logger
logging.getLogger("uvicorn.access").addFilter(HealthCheckFilter())
app = FastAPI()
templates = Jinja2Templates(directory="app/templates")
def get_s3_client():
"""Create and return an S3 client using environment variables"""
s3_access_key = os.environ.get('S3_ACCESS_KEY')
s3_secret_key = os.environ.get('S3_SECRET_KEY')
s3_host_base = os.environ.get('S3_HOST_BASE')
s3_host_bucket = os.environ.get('S3_HOST_BUCKET')
if not all([s3_access_key, s3_secret_key, s3_host_base]):
raise ValueError("S3 environment variables not properly set")
s3 = boto3.client(
's3',
aws_access_key_id=s3_access_key,
aws_secret_access_key=s3_secret_key,
endpoint_url=s3_host_base,
region_name='us-east-1' # Required but unused for Infomaniak
)
return s3
def create_bucket_if_not_exists(bucket_name):
"""Create S3 bucket if it doesn't exist"""
s3_client = get_s3_client()
try:
s3_client.head_bucket(Bucket=bucket_name)
except ClientError as e:
error_code = int(e.response['Error']['Code'])
if error_code == 404:
# Bucket doesn't exist, create it
try:
s3_client.create_bucket(Bucket=bucket_name)
print(f"Bucket {bucket_name} created successfully")
except ClientError as create_error:
print(f"Error creating bucket: {create_error}")
raise
else:
# Some other error
print(f"Error checking bucket: {e}")
raise
# Create bucket on startup
try:
@@ -66,172 +48,177 @@ try:
except Exception as e:
print(f"Warning: Could not create/check S3 bucket: {e}")
def upload_to_s3(file_data, bucket_name, object_name, content_type='application/pdf'):
"""Upload file data to S3 bucket"""
s3_client = get_s3_client()
try:
s3_client.put_object(
Bucket=bucket_name,
Key=object_name,
Body=file_data,
ContentType=content_type
)
return True
except ClientError as e:
print(f"Error uploading to S3: {e}")
return False
def download_from_s3(bucket_name, object_name):
"""Download file data from S3 bucket"""
s3_client = get_s3_client()
try:
response = s3_client.get_object(Bucket=bucket_name, Key=object_name)
return response['Body'].read()
except ClientError as e:
print(f"Error downloading from S3: {e}")
return None
def list_objects_in_s3(bucket_name):
"""List all objects in S3 bucket (sorted from newest to oldest)"""
s3_client = get_s3_client()
try:
response = s3_client.list_objects_v2(Bucket=bucket_name)
if 'Contents' in response:
# Filter for PDF files only and sort by last modified (newest first)
pdf_files = [obj for obj in response['Contents'] if obj['Key'].endswith('.pdf')]
pdf_files.sort(key=lambda x: x['LastModified'], reverse=True)
return pdf_files
else:
return []
except ClientError as e:
print(f"Error listing objects in S3: {e}")
return []
def delete_from_s3(bucket_name, object_name):
"""Delete file from S3 bucket"""
s3_client = get_s3_client()
try:
s3_client.delete_object(Bucket=bucket_name, Key=object_name)
return True
except ClientError as e:
print(f"Error deleting from S3: {e}")
return False
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.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
multiplication_only: bool = False
max_first_operand: int = 12 # New parameter for the maximum value of the first operand
def generate_exercises(min_table: int, max_table: int, num_exercises: int = 15) -> List[str]:
class OperationExerciseRequest(BaseModel):
num_exercises: int = 20
def generate_exercises(
min_table: int,
max_table: int,
num_exercises: int = 15,
multiplication_only: bool = False,
max_first_operand: int = 12, # New parameter for the maximum value of the first operand
) -> 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
exercises: List[str] = []
used_operations: set = 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':
# Pour les multiplications, un des opérandes est entre 1 et max_first_operand
# et l'autre est entre min_table et max_table
if multiplication_only or random.choice([True, False]):
# Exercice de multiplication
operation_key = f"mult_{a}_{b}" # Clé unique pour cette opération
a = random.randint(1, max_first_operand) # Premier opérande entre 1 et max_first_operand
b = random.randint(min_table, max_table) # Second opérande entre min_table et max_table
# Randomize the order of operands
if random.choice([True, False]):
a, b = b, a
result = a * b
operation_key = f"mult_{min(a,b)}_{max(a,b)}" # Clé unique pour cette opération (order-independent)
if operation_key not in used_operations:
exercise = f"{a} · {b} = ____"
exercises.append(exercise)
used_operations.add(operation_key)
else: # division
elif not multiplication_only: # division
# Exercice de division
a = random.randint(min_table, max_table)
b = random.randint(min_table, max_table)
result = a * b
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':
# Pour les multiplications, un des opérandes est entre 1 et max_first_operand
# et l'autre est entre min_table et max_table
if multiplication_only or random.choice([True, False]):
# Exercice de multiplication
a = random.randint(1, max_first_operand) # Premier opérande entre 1 et max_first_operand
b = random.randint(min_table, max_table) # Second opérande entre min_table et max_table
# Randomize the order of operands
if random.choice([True, False]):
a, b = b, a
result = a * b
exercise = f"{a} · {b} = ____"
else: # division
elif not multiplication_only: # division
a = random.randint(min_table, max_table)
b = random.randint(min_table, max_table)
result = a * b
divisor = random.choice([a, b])
exercise = f"{result} : {divisor} = ____"
else:
# Fallback to multiplication if multiplication_only is True
a = random.randint(1, max_first_operand)
b = random.randint(min_table, max_table)
# Randomize the order of operands
if random.choice([True, False]):
a, b = b, a
exercise = f"{a} · {b} = ____"
exercises.append(exercise)
return exercises
def create_math_exercises_pdf(min_table: int, max_table: int, num_exercises: int = 15) -> str:
def create_math_exercises_pdf(
min_table: int,
max_table: int,
num_exercises: int = 15,
multiplication_only: bool = False,
max_first_operand: int = 12, # New parameter for the maximum value of the first operand
) -> str:
"""Crée un fichier PDF avec des exercices de mathématiques mélangés en 3 colonnes et l'upload sur S3"""
import datetime
# Add timestamp to filename
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
# Ajouter des informations sur la plage de tables
if min_table == max_table:
table_info = f'Tables de multiplication et division pour {min_table}'
pdf_filename = f'exercices_mathematiques_table_{min_table}_{num_exercises}_exercices_{timestamp}.pdf'
if multiplication_only:
if min_table == max_table:
table_info = f"Tables de multiplication seulement pour {min_table}"
pdf_filename = f"exercices_multiplication_seulement_table_{min_table}_{num_exercises}_exercices_{timestamp}.pdf"
else:
table_info = (
f"Tables de multiplication seulement de {min_table} à {max_table}"
)
pdf_filename = f"exercices_multiplication_seulement_tables_{min_table}_a_{max_table}_{num_exercises}_exercices_{timestamp}.pdf"
else:
table_info = f'Tables de multiplication et division de {min_table} à {max_table}'
pdf_filename = f'exercices_mathematiques_tables_{min_table}_a_{max_table}_{num_exercises}_exercices_{timestamp}.pdf'
if min_table == max_table:
table_info = f"Tables de multiplication et division pour {min_table}"
pdf_filename = f"exercices_mathematiques_table_{min_table}_{num_exercises}_exercices_{timestamp}.pdf"
else:
table_info = (
f"Tables de multiplication et division de {min_table} à {max_table}"
)
pdf_filename = f"exercices_mathematiques_tables_{min_table}_a_{max_table}_{num_exercises}_exercices_{timestamp}.pdf"
pdf = MathExercisesPDF()
pdf.add_page()
pdf.set_font('Helvetica', '', 12)
pdf.cell(0, 10, table_info, 0, 1, 'C', new_x="LMARGIN", new_y="NEXT")
pdf.set_font("Helvetica", "", 12)
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)
exercises = generate_exercises(
min_table, max_table, num_exercises, multiplication_only, max_first_operand
)
# 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:]
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):
@@ -239,42 +226,64 @@ def create_math_exercises_pdf(min_table: int, max_table: int, num_exercises: int
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")
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 en mémoire
pdf_data = pdf.output()
# Upload to S3
upload_success = upload_to_s3(pdf_data, S3_BUCKET_NAME, pdf_filename)
if not upload_success:
raise Exception("Failed to upload PDF to S3")
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})
from fastapi.responses import RedirectResponse
@app.post("/generate")
async def generate_exercises_endpoint(request: ExerciseRequest):
@@ -282,37 +291,58 @@ async def generate_exercises_endpoint(request: ExerciseRequest):
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"}
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"}
pdf_filename = create_math_exercises_pdf(
request.min_table,
request.max_table,
request.num_exercises
request.min_table,
request.max_table,
request.num_exercises,
request.multiplication_only,
request.max_first_operand, # Pass the new parameter
)
# 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.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
file_data = download_from_s3(S3_BUCKET_NAME, filename)
if file_data is None:
return {"error": "File not found"}
# Return streaming response with PDF data
return StreamingResponse(
io.BytesIO(file_data),
media_type='application/pdf',
headers={'Content-Disposition': f'attachment; filename="{filename}"'}
media_type="application/pdf",
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
)
@app.get("/list")
async def list_pdfs(page: int = 1, page_size: int = 10):
"""List PDF files in the S3 bucket with pagination (sorted from newest to oldest)"""
@@ -322,18 +352,18 @@ async def list_pdfs(page: int = 1, page_size: int = 10):
page = 1
if page_size < 1 or page_size > 100:
page_size = 10
pdf_files = list_objects_in_s3(S3_BUCKET_NAME)
# Calculate pagination
total_files = len(pdf_files)
total_pages = (total_files + page_size - 1) // page_size # Ceiling division
# Slice the files for the current page
start_index = (page - 1) * page_size
end_index = start_index + page_size
paginated_files = pdf_files[start_index:end_index]
return {
"files": paginated_files,
"pagination": {
@@ -342,90 +372,103 @@ async def list_pdfs(page: int = 1, page_size: int = 10):
"total_files": total_files,
"total_pages": total_pages,
"has_next": page < total_pages,
"has_prev": page > 1
}
"has_prev": page > 1,
},
}
except Exception as e:
return {"error": f"Error listing files: {str(e)}"}
@app.delete("/delete/{filename}")
async def delete_pdf(filename: str):
"""Delete a PDF file from S3 bucket"""
try:
# Decode URL encoded filename
import urllib.parse
decoded_filename = urllib.parse.unquote(filename)
success = delete_from_s3(S3_BUCKET_NAME, decoded_filename)
if success:
return {"message": f"Fichier {decoded_filename} supprimé avec succès"}
else:
return {"error": f"Erreur lors de la suppression du fichier {decoded_filename}"}
return {
"error": f"Erreur lors de la suppression du fichier {decoded_filename}"
}
except Exception as e:
return {"error": f"Error deleting file: {str(e)}"}
@app.post("/bulk-delete")
async def bulk_delete(filenames: str = Form(...)):
"""Delete multiple PDF files from S3 bucket"""
try:
import json
filename_list = json.loads(filenames)
deleted_files = []
errors = []
for filename in filename_list:
success = delete_from_s3(S3_BUCKET_NAME, filename)
if success:
deleted_files.append(filename)
else:
errors.append(filename)
if errors:
return {"message": f"Fichiers supprimés: {len(deleted_files)}", "errors": errors}
return {
"message": f"Fichiers supprimés: {len(deleted_files)}",
"errors": errors,
}
else:
return {"message": f"{len(deleted_files)} fichiers supprimés avec succès"}
except Exception as e:
return {"error": f"Error deleting files: {str(e)}"}
from fastapi import Form
@app.post("/bulk-download")
async def bulk_download(filenames: str = Form(...)):
"""Download multiple PDF files as a zip archive"""
try:
import zipfile
import tempfile
import json
# Parse the JSON string to get the list of filenames
filename_list = json.loads(filenames)
# Create a temporary zip file
with tempfile.NamedTemporaryFile(suffix='.zip', delete=False) as tmp_zip:
with zipfile.ZipFile(tmp_zip.name, 'w') as zipf:
with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp_zip:
with zipfile.ZipFile(tmp_zip.name, "w") as zipf:
for filename in filename_list:
file_data = download_from_s3(S3_BUCKET_NAME, filename)
if file_data:
zipf.writestr(filename, file_data)
# Read the zip file
with open(tmp_zip.name, 'rb') as f:
with open(tmp_zip.name, "rb") as f:
zip_data = f.read()
# Clean up temporary file
import os
os.unlink(tmp_zip.name)
# Return streaming response with zip data
zip_filename = f"math_exercises_{len(filename_list)}_files.zip"
return StreamingResponse(
io.BytesIO(zip_data),
media_type='application/zip',
headers={'Content-Disposition': f'attachment; filename="{zip_filename}"'}
media_type="application/zip",
headers={
"Content-Disposition": f'attachment; filename="{zip_filename}"'
},
)
except Exception as e:
return {"error": f"Error downloading files: {str(e)}"}
@app.get("/health")
async def health_check():
"""Health check endpoint that returns the status of the application"""
return {"status": "healthy"}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
uvicorn.run(app, host="0.0.0.0", port=8000)
+116
View File
@@ -0,0 +1,116 @@
#!/usr/bin/env python3
import os
import boto3
from botocore.exceptions import ClientError
from typing import List, Optional
# S3 Configuration
S3_BUCKET_NAME = os.environ.get("S3_BUCKET_NAME", "math-exercises")
def get_s3_client():
"""Create and return an S3 client using environment variables"""
s3_access_key = os.environ.get("S3_ACCESS_KEY")
s3_secret_key = os.environ.get("S3_SECRET_KEY")
s3_host_base = os.environ.get("S3_HOST_BASE")
if not all([s3_access_key, s3_secret_key, s3_host_base]):
raise ValueError("S3 environment variables not properly set")
s3 = boto3.client(
"s3",
aws_access_key_id=s3_access_key,
aws_secret_access_key=s3_secret_key,
endpoint_url=s3_host_base,
region_name="us-east-1", # Required but unused for Infomaniak
)
return s3
def create_bucket_if_not_exists(bucket_name: str):
"""Create S3 bucket if it doesn't exist"""
s3_client = get_s3_client()
try:
s3_client.head_bucket(Bucket=bucket_name)
except ClientError as e:
error_code = int(e.response["Error"]["Code"])
if error_code == 404:
# Bucket doesn't exist, create it
try:
s3_client.create_bucket(Bucket=bucket_name)
print(f"Bucket {bucket_name} created successfully")
except ClientError as create_error:
print(f"Error creating bucket: {create_error}")
raise
else:
# Some other error
print(f"Error checking bucket: {e}")
raise
def upload_to_s3(
file_data: bytes,
bucket_name: str,
object_name: str,
content_type: str = "application/pdf",
) -> bool:
"""Upload file data to S3 bucket"""
s3_client = get_s3_client()
try:
s3_client.put_object(
Bucket=bucket_name,
Key=object_name,
Body=file_data,
ContentType=content_type,
)
return True
except ClientError as e:
print(f"Error uploading to S3: {e}")
return False
def download_from_s3(bucket_name: str, object_name: str) -> Optional[bytes]:
"""Download file data from S3 bucket"""
s3_client = get_s3_client()
try:
response = s3_client.get_object(Bucket=bucket_name, Key=object_name)
return response["Body"].read()
except ClientError as e:
print(f"Error downloading from S3: {e}")
return None
def list_objects_in_s3(bucket_name: str) -> List[dict]:
"""List all objects in S3 bucket (sorted from newest to oldest)"""
s3_client = get_s3_client()
try:
response = s3_client.list_objects_v2(Bucket=bucket_name)
if "Contents" in response:
# Filter for PDF files only and sort by last modified (newest first)
pdf_files = [
obj for obj in response["Contents"] if obj["Key"].endswith(".pdf")
]
pdf_files.sort(key=lambda x: x["LastModified"], reverse=True)
return pdf_files
else:
return []
except ClientError as e:
print(f"Error listing objects in S3: {e}")
return []
def delete_from_s3(bucket_name: str, object_name: str) -> bool:
"""Delete file from S3 bucket"""
s3_client = get_s3_client()
try:
s3_client.delete_object(Bucket=bucket_name, Key=object_name)
return True
except ClientError as e:
print(f"Error deleting from S3: {e}")
return False
+406 -11
View File
@@ -5,17 +5,42 @@
<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">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css" rel="stylesheet">
<style>
:root {
--bs-body-bg: #ffffff;
--bs-body-color: #212529;
--bs-border-color: #dee2e6;
--bs-secondary-bg: #f8f9fa;
--bs-tertiary-bg: #ffffff;
}
[data-bs-theme="dark"] {
--bs-body-bg: #121212;
--bs-body-color: #e9ecef;
--bs-border-color: #495057;
--bs-secondary-bg: #212529;
--bs-tertiary-bg: #343a40;
}
body {
background-color: var(--bs-body-bg);
color: var(--bs-body-color);
transition: background-color 0.3s ease, color 0.3s ease;
}
.card {
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
transition: 0.3s;
margin-bottom: 20px;
background-color: var(--bs-tertiary-bg);
border-color: var(--bs-border-color);
}
.card:hover {
box-shadow: 0 8px 16px rgba(0,0,0,0.2);
}
.exercise-result {
background-color: #f8f9fa;
background-color: var(--bs-secondary-bg);
border-radius: 5px;
padding: 15px;
margin-top: 20px;
@@ -25,22 +50,95 @@
height: 3rem;
margin-right: 0.75rem;
}
.dark-mode-toggle {
position: fixed;
top: 20px;
right: 20px;
z-index: 1000;
border-radius: 50%;
width: 50px;
height: 50px;
border: none;
background-color: var(--bs-tertiary-bg);
color: var(--bs-body-color);
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
transition: all 0.3s ease;
}
.dark-mode-toggle:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
}
[data-bs-theme="dark"] .bg-light {
background-color: var(--bs-secondary-bg) !important;
}
[data-bs-theme="dark"] .text-muted {
color: var(--bs-body-color) !important;
opacity: 0.7;
}
[data-bs-theme="dark"] .table {
--bs-table-bg: var(--bs-tertiary-bg);
--bs-table-striped-bg: var(--bs-secondary-bg);
--bs-table-hover-bg: var(--bs-border-color);
}
[data-bs-theme="dark"] .alert {
border-color: var(--bs-border-color);
}
[data-bs-theme="dark"] .form-control,
[data-bs-theme="dark"] .form-select {
background-color: var(--bs-tertiary-bg);
border-color: var(--bs-border-color);
color: var(--bs-body-color);
}
[data-bs-theme="dark"] .form-control:focus,
[data-bs-theme="dark"] .form-select:focus {
background-color: var(--bs-tertiary-bg);
border-color: var(--bs-primary);
color: var(--bs-body-color);
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
}
[data-bs-theme="dark"] .form-check-input {
background-color: var(--bs-tertiary-bg);
border-color: var(--bs-border-color);
}
[data-bs-theme="dark"] .form-check-input:checked {
background-color: var(--bs-primary);
border-color: var(--bs-primary);
}
.transition-all {
transition: all 0.3s ease;
}
</style>
</head>
<body>
<!-- Dark Mode Toggle Button -->
<button class="dark-mode-toggle" id="darkModeToggle" title="Basculer le mode sombre" aria-label="Basculer le mode sombre">
<i class="bi bi-moon-fill" id="darkModeIcon"></i>
</button>
<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>
<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>
@@ -60,9 +158,35 @@
<div class="form-text">Nombre total d'exercices à générer (entre 1 et 100)</div>
</div>
<div class="mb-3">
<label class="form-label">Types d'exercices</label>
<div class="form-check">
<input class="form-check-input" type="radio" name="exerciseType" id="multiplicationOnly" value="multiplication" checked>
<label class="form-check-label" for="multiplicationOnly">
Multiplications uniquement
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="exerciseType" id="multiplicationAndDivision" value="both">
<label class="form-check-label" for="multiplicationAndDivision">
Multiplications ET divisions
</label>
</div>
</div>
<div class="mb-3">
<label for="maxFirstOperand" class="form-label">Maximum du premier opérande</label>
<select class="form-select" id="maxFirstOperand">
<option value="9">9 (Tables jusqu'à 9)</option>
<option value="10" selected>10 (Tables jusqu'à 10)</option>
<option value="12">12 (Tables jusqu'à 12)</option>
</select>
<div class="form-text">La valeur maximale pour le premier opérande dans les multiplications</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="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 +194,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 +306,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>
@@ -171,7 +348,7 @@
</div>
</div>
<footer class="bg-light text-center text-muted py-4 mt-5">
<footer class="bg-light text-center text-muted py-4 mt-5 transition-all">
<div class="container">
<p>Générateur d'Exercices de Mathématiques &copy; 2025</p>
</div>
@@ -179,9 +356,125 @@
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
<script>
// Dark Mode Theme Management
class ThemeManager {
constructor() {
this.init();
}
init() {
// Load saved theme or detect system preference
const savedTheme = localStorage.getItem('theme');
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
const theme = savedTheme || systemTheme;
this.setTheme(theme);
// Listen for system theme changes
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
if (!localStorage.getItem('theme')) {
this.setTheme(e.matches ? 'dark' : 'light');
}
});
// Setup toggle button
this.setupToggleButton();
}
setTheme(theme) {
document.documentElement.setAttribute('data-bs-theme', theme);
localStorage.setItem('theme', theme);
this.updateToggleIcon(theme);
}
toggleTheme() {
const currentTheme = document.documentElement.getAttribute('data-bs-theme');
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
this.setTheme(newTheme);
}
updateToggleIcon(theme) {
const icon = document.getElementById('darkModeIcon');
if (theme === 'dark') {
icon.className = 'bi bi-sun-fill';
} else {
icon.className = 'bi bi-moon-fill';
}
}
setupToggleButton() {
const toggleButton = document.getElementById('darkModeToggle');
toggleButton.addEventListener('click', () => {
this.toggleTheme();
this.animateToggle();
});
}
animateToggle() {
const button = document.getElementById('darkModeToggle');
button.style.transform = 'scale(0.9)';
setTimeout(() => {
button.style.transform = '';
}, 150);
}
}
// Initialize theme manager
const themeManager = new ThemeManager();
// Current page for pagination
let currentPage = 1;
const pageSize = 10;
// Save form values to localStorage
function saveFormValues() {
const exerciseFormValues = {
minTable: document.getElementById('minTable').value,
maxTable: document.getElementById('maxTable').value,
numExercises: document.getElementById('numExercises').value,
exerciseType: document.querySelector('input[name="exerciseType"]:checked').value,
maxFirstOperand: document.getElementById('maxFirstOperand').value
};
const operationFormValues = {
numOperationExercises: document.getElementById('numOperationExercises').value
};
localStorage.setItem('exerciseFormValues', JSON.stringify(exerciseFormValues));
localStorage.setItem('operationFormValues', JSON.stringify(operationFormValues));
}
// Restore form values from localStorage
function restoreFormValues() {
// Restore exercise form values
const savedExerciseValues = localStorage.getItem('exerciseFormValues');
if (savedExerciseValues) {
const exerciseValues = JSON.parse(savedExerciseValues);
document.getElementById('minTable').value = exerciseValues.minTable || 1;
document.getElementById('maxTable').value = exerciseValues.maxTable || 10;
document.getElementById('numExercises').value = exerciseValues.numExercises || 15;
// Restore radio button selection
if (exerciseValues.exerciseType) {
const radio = document.querySelector(`input[name="exerciseType"][value="${exerciseValues.exerciseType}"]`);
if (radio) {
radio.checked = true;
}
} else if (exerciseValues.multiplicationOnly !== undefined) {
// Handle legacy storage format
document.getElementById('multiplicationOnly').checked = exerciseValues.multiplicationOnly;
}
document.getElementById('maxFirstOperand').value = exerciseValues.maxFirstOperand || 10;
}
// Restore operation form values
const savedOperationValues = localStorage.getItem('operationFormValues');
if (savedOperationValues) {
const operationValues = JSON.parse(savedOperationValues);
document.getElementById('numOperationExercises').value = operationValues.numOperationExercises || 20;
}
}
// Function to load and display existing PDFs with pagination (sorted from newest to oldest)
async function loadPdfList(page = 1) {
@@ -355,7 +648,23 @@
// Load PDF list on page load
document.addEventListener('DOMContentLoaded', function() {
loadPdfList(currentPage);
// Ensure theme is initialized first
setTimeout(() => {
restoreFormValues();
loadPdfList(currentPage);
}, 100);
});
// Add event listeners to save form values on change
document.getElementById('minTable').addEventListener('change', saveFormValues);
document.getElementById('maxTable').addEventListener('change', saveFormValues);
document.getElementById('numExercises').addEventListener('change', saveFormValues);
document.getElementById('maxFirstOperand').addEventListener('change', saveFormValues);
document.getElementById('numOperationExercises').addEventListener('change', saveFormValues);
// Add event listeners for radio buttons
document.querySelectorAll('input[name="exerciseType"]').forEach(radio => {
radio.addEventListener('change', saveFormValues);
});
// Event listener for select all button
@@ -454,9 +763,15 @@
document.getElementById('exerciseForm').addEventListener('submit', async function(e) {
e.preventDefault();
// Save form values to localStorage
saveFormValues();
const minTable = parseInt(document.getElementById('minTable').value);
const maxTable = parseInt(document.getElementById('maxTable').value);
const numExercises = parseInt(document.getElementById('numExercises').value);
const exerciseType = document.querySelector('input[name="exerciseType"]:checked').value;
const multiplicationOnly = (exerciseType === 'multiplication');
const maxFirstOperand = parseInt(document.getElementById('maxFirstOperand').value);
const generateBtn = document.getElementById('generateBtn');
const buttonText = document.getElementById('buttonText');
@@ -476,7 +791,9 @@
body: JSON.stringify({
min_table: minTable,
max_table: maxTable,
num_exercises: numExercises
num_exercises: numExercises,
multiplication_only: multiplicationOnly,
max_first_operand: maxFirstOperand
})
});
@@ -491,13 +808,14 @@
// Show success message
const resultContainer = document.getElementById('resultContainer');
const resultMessage = document.getElementById('resultMessage');
const exerciseType = multiplicationOnly ? "multiplications uniquement" : "multiplications et divisions";
resultMessage.innerHTML = `
<div class="alert alert-success">
<h6>PDF généré avec succès!</h6>
<p>Le téléchargement devrait commencer automatiquement.</p>
<ul class="mb-0">
<li>Tables de ${minTable} à ${maxTable}</li>
<li>${numExercises} exercices générés</li>
<li>${numExercises} exercices générés (${exerciseType})</li>
</ul>
</div>
`;
@@ -522,7 +840,84 @@
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();
// Save form values to localStorage
saveFormValues();
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;
}
+12 -5
View File
@@ -46,22 +46,29 @@ spec:
# Liveness probe
livenessProbe:
httpGet:
path: /
path: /health
port: 8000
initialDelaySeconds: 30
initialDelaySeconds: 90
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
# Readiness probe
readinessProbe:
httpGet:
path: /
path: /health
port: 8000
initialDelaySeconds: 5
periodSeconds: 10
initialDelaySeconds: 2
periodSeconds: 3
timeoutSeconds: 3
failureThreshold: 3
# Environment variables from ConfigMap
envFrom:
- configMapRef:
name: math-exercises-config
# Volume mount for temporary files
volumeMounts:
- name: tmp-volume
mountPath: /tmp
volumes:
- name: tmp-volume
emptyDir: {}
+9 -7
View File
@@ -10,10 +10,12 @@ resources:
- configmap.yaml
# Common labels to apply to all resources
commonLabels:
app.kubernetes.io/name: math-exercises
app.kubernetes.io/instance: math-exercises-instance
app.kubernetes.io/version: "1.0"
app.kubernetes.io/component: web
app.kubernetes.io/part-of: math-suite
app.kubernetes.io/managed-by: kustomize
labels:
- includeSelectors: true
pairs:
app.kubernetes.io/component: web
app.kubernetes.io/instance: math-exercises-instance
app.kubernetes.io/managed-by: kustomize
app.kubernetes.io/name: math-exercises
app.kubernetes.io/part-of: math-suite
app.kubernetes.io/version: "1.0"
+5 -10
View File
@@ -10,26 +10,21 @@ spec:
- Ingress
- Egress
ingress:
# Allow inbound traffic from the ingress controller only
# Allow inbound traffic to container port
- from:
- namespaceSelector:
matchLabels:
name: ingress-nginx
ports:
- protocol: TCP
port: 8000
egress:
# Allow outbound DNS resolution
- to:
- namespaceSelector:
matchLabels:
name: kube-system
ports:
- protocol: TCP
port: 53
- protocol: UDP
port: 53
# Allow outbound HTTPS for package updates or external APIs
- ports:
# Allow outbound HTTPS
- to:
ports:
- protocol: TCP
port: 443
port: 443
@@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
creationTimestamp: null
labels:
app: keda-add-ons-http-interceptor-proxy
name: keda-add-ons-http-interceptor-proxy
spec:
externalName: keda-add-ons-http-interceptor-proxy.keda
selector:
app: keda-add-ons-http-interceptor-proxy
type: ExternalName
status:
loadBalancer: {}
@@ -0,0 +1,24 @@
kind: HTTPScaledObject
apiVersion: http.keda.sh/v1alpha1
metadata:
name: math-exercises
spec:
hosts:
- "math-tables.cl1.parano.ch"
pathPrefixes:
- /
scaleTargetRef:
name: math-exercises-app
kind: Deployment
apiVersion: apps/v1
service: math-exercises-service
portName: http
replicas:
min: 0
max: 3
scaledownPeriod: 60
scalingMetric:
requestRate:
granularity: 1s
targetValue: 100
window: 1m
@@ -0,0 +1,51 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: math-tables
resources:
- ../production
- external-service.yaml
- http-scaled-object.yaml
patches:
- patch: |
[
{
"op": "remove",
"path": "/spec/replicas"
}
]
target:
group: apps
version: v1
kind: Deployment
name: math-exercises-app
- path: remove-pdb.yaml
- patch: |
[
{
"op": "replace",
"path": "/spec/rules/0/http/paths/0/backend/service/port/number",
"value": 8080
},
{
"op": "replace",
"path": "/spec/rules/0/http/paths/0/backend/service/name",
"value": "keda-add-ons-http-interceptor-proxy"
}
]
target:
group: networking.k8s.io
version: v1
kind: Ingress
name: math-exercises-ingress
- patch: |
[
{
"op": "replace",
"path": "/spec/internalTrafficPolicy",
"value": "Cluster"
}
]
target:
version: v1
kind: Service
name: math-exercises-service
@@ -0,0 +1,5 @@
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: math-exercises-pdb
$patch: delete
@@ -32,5 +32,5 @@ spec:
memory: "64Mi"
cpu: "250m"
limits:
memory: "128Mi"
memory: "256Mi"
cpu: "1"
+13 -11
View File
@@ -8,23 +8,25 @@ resources:
- namespace.yaml
# Production-specific patches
patchesStrategicMerge:
- deployment-patch.yaml
- security-patch.yaml
- ingress-patch.yaml
# Production-specific configurations
images:
- name: math-exercises
newName: harbor.cl1.parano.ch/library/math-exercice
newTag: 1.0.2
newTag: 1.4.0
# Production-specific labels
commonLabels:
environment: production
security-level: high
secretGenerator:
- name: s3-credentials
envs:
- s3-credentials.env
- envs:
- s3-credentials.env
name: s3-credentials
labels:
- includeSelectors: true
pairs:
environment: production
security-level: high
patches:
- path: deployment-patch.yaml
- path: security-patch.yaml
- path: ingress-patch.yaml
@@ -0,0 +1,15 @@
S3_ACCESS_KEY=ENC[AES256_GCM,data:wZb+QNC7JoDEEOERSONuq6nE7jTBi4t4NmfIWJKxItA=,iv:kbNjsgN3udI43PM7+xBZxtqzIpRItnWl+/oWheMWR+Y=,tag:/95+YFrzHzUyn6kVUkAWvA==,type:str]
S3_SECRET_KEY=ENC[AES256_GCM,data:UES0Qs0InnZutHZujcFqdEouJwYEQhXqPdHP/01qt9o=,iv:MBtiGmqd3kcNsD/Oi4jOgFKcN/7XyMEpmOyn2EcFT7o=,tag:gthAM9kLrpybchQgIyN/GQ==,type:str]
S3_HOST_BUCKET=ENC[AES256_GCM,data:6EKH68PpP1nAMPNqmQlkqvEpdW8ZQfXitC1+imMn278=,iv:ZgVo8TuZC/IlhB8E+WZhn5yoxzQUq7OWDun4WJWe0v8=,tag:QyDOV6Du6wOUPRRleZAMFw==,type:str]
S3_HOST_BASE=ENC[AES256_GCM,data:cpZPWgCklHGHKGicriXVGRpKuHtYQl2N1fqMx5+Snfg=,iv:wm4ncY+x0M2FHZNpT69F6KaYCeyuC9FnvROQuxZsn9M=,tag:bHCUuHhsVygRdkLmNynbZQ==,type:str]
S3_BUCKET_NAME=ENC[AES256_GCM,data:i+tP6X8weH9MomZSHsY=,iv:4HJo7fG9CwE/y+miLZiCpkt9c2frLSomswcErxWvAK8=,tag:65gsYZArFarUFsrb5nAgng==,type:str]
sops_lastmodified=2025-09-04T06:42:15Z
sops_mac=ENC[AES256_GCM,data:misqBU7bfv4f/hUNqMIeFkRls3iuHA3XwjWetVzkLdG2eV2WhjiGL+TuwpVah4PN4BUrqTHmOMGyf4fumGJqOt9DdGnH+MMexFr4aT54TfaPmUpskEuwZGysuyasA6LHZn7a7M6BzxLXiXfSZY9EjahY8l7ZaaV+2pFygJKFfmk=,iv:RlqWkbKQtvvnIRd/g3Eu6CvMduLa1i4omL6RY/HSsag=,tag:n3C16u3x0XznKUpu8cR2tg==,type:str]
sops_pgp__list_0__map_created_at=2025-09-04T06:42:09Z
sops_pgp__list_0__map_enc=-----BEGIN PGP MESSAGE-----\n\nhQIMA4pH315BwPYDAQ//VN3t9yRhfuQ5HIXGXJ67BFLMVL/MvmrWlWLOlyx0APJR\nYnOAA41LecthvvcPlvvx1gQlFjnAt62CN93Y/xrCJKLaRywmuqGnIVVrHgbg0SYG\nqaZbjQdjJ4NxRdeWGQ1ZSpmZAu8aEeFxvoZQBvHV7SByq7IGxbkbbcl1Ur/ZOu5r\n5OzT3xtqUdkmmF4bfjz3GcyZDTAPbZQhhzsK4FajuAjHZAAs1k6ASM69+LTedFzC\n/pyZpieAAZ/NEw6EUhT0cFwO40NTw+gDWT1TS3bXiXeeHcp/9a+eUb/TOOgt3g48\n3O8uw9UOv9/xGkeLYjMfInRcrCJScyekYw4OURo+cqwv2NgYAA+kwJ/I6QTXUJQt\nldJzr92m6tEkD6gLjfjjgKB+frrw/hWMmX40SOau9jaefmQw/Zkjb7e7/sMwwp1G\n5/NFcPhV9K00OyHUVAZg00UdRefT9ifsDOHqDTvZMmvVoGInGL/wY/Q8Ry78xGLR\n+Q4xIdjdHaybwtc7qrxZBG4lQGVYWpz0SiacaAd3dsh0/ds+yuM5csGpNAHtsCfm\nY1vYKnJKKHGaZT47zPt27gRzi6MuMqPAKrSbPA0D7R25xkRsH+2ZsWfu7vk/6ENL\nk7qVyWEZ5rNyXYnukeuw4kF8uKw9FpJpsd3ofu/cnRjbYHyNIfJKjE8Od+qnSTfS\nXAF2/0aiCmBXqX9+rADh6miKUxG6TUei6jSDRkBbfCnVRMWmf3cinpRWskayU0Fa\nsTvHEgBNT4zB5I+/ryuqpW3vTvKimqZkm0yIOgZ+n7xq2OiiFBu3ob0cfnJu\n=cbAs\n-----END PGP MESSAGE-----
sops_pgp__list_0__map_fp=7120085E6161EB9ABC2FD0117C26F0446A4D9603
sops_pgp__list_1__map_created_at=2025-09-04T06:42:09Z
sops_pgp__list_1__map_enc=-----BEGIN PGP MESSAGE-----\n\nhQIMA1Xegr5/Of3mAQ/9FGr7Wmqw8/7yJK+hBzrzVYP+kaM20dDdlmNYfjwhGn6x\nHnqMSAnfb12F9r9Pu4NRSISG25QhmSYhxeOtRPii39koKSQZwzEjzMJb9f0XGocR\nCIvN9LQFYNEeUcOCG/V89RJDjqeoxbD8zZyvVvZzF1seff8sN15YJFzb4bZv2Bdb\ngWsJgbIabBKccHUVu93VQyw9KoZbL4omRcZDHQDMSwum/BdxF1mGr+OPhqwJ0lri\nXDcOurO9VojNKfP8i501KP9DMzT7Zw3oj3b+AcsYUDdfWCXRM3MmLg6YCEJS/bMf\nH5DUjew/mV7hNrQvBN1DGaqXLHyel5Q1gGMBrpxGsXFfCKOGZSMZCF4VEkpRKQLg\nN0lkZ5bTBIS+EF45HmPXg/gYWKxiP4i7wEdn85hNBNAxktcsPcnCmCza2nPRv3Kp\n+WrVRRzkG0lDECFeyXuf9VqfNem2YWYmmJ1XtQGn2zqyOXlTC6wyg4EAd4vItUld\nfBMYBBzXRM+ME1FOXAtZes8s/uDqQYHmdxB2HIFhTqZqsPMo5MgT3kNYxWr4hNm0\nB3YUWuXv3GMaaUNYYCqezo/EcaakM8PMwOR6Gn+f77k1t4Tj8FkjJb6YeEKn5hl2\noeg18UHLk6miJTGmKLm4zuA0V8IOviAmJq2kFWNSa7f4yz+45RDGJToiuJoSK77S\nXAF9EOajbHfdy4M+PV0XYvSUOmESTWVw7TXKcQf6iTtyNayr2TLCFWKqMhPHYoHs\nmt5Hv25bqs9SaXHPjbwqHIPzOtMUD5Qfp/gvVU1zTZWM8JsQ0+z5p9b50cQO\n=Erct\n-----END PGP MESSAGE-----
sops_pgp__list_1__map_fp=E21007D7E97694AC900FF3434C6659F378169546
sops_unencrypted_suffix=_unencrypted
sops_version=3.10.1
+82 -56
View File
@@ -5,38 +5,44 @@ import subprocess
import os
from fpdf import FPDF
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.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")
def generate_exercises(min_table, max_table, num_exercises=15):
"""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':
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:
@@ -51,7 +57,7 @@ def generate_exercises(min_table, max_table, num_exercises=15):
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:
@@ -61,49 +67,52 @@ def generate_exercises(min_table, max_table, num_exercises=15):
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_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, max_table, num_exercises=15):
"""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)
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}'
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")
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:]
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):
@@ -111,34 +120,37 @@ def create_math_exercises_pdf(min_table, max_table, num_exercises=15):
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")
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'exercices_mathematiques_table_{min_table}_{num_exercises}_exercices.pdf'
filename = (
f"exercices_mathematiques_table_{min_table}_{num_exercises}_exercices.pdf"
)
else:
filename = f'exercices_mathematiques_tables_{min_table}_a_{max_table}_{num_exercises}_exercices.pdf'
filename = f"exercices_mathematiques_tables_{min_table}_a_{max_table}_{num_exercises}_exercices.pdf"
pdf.output(filename)
return filename
def open_pdf(filename):
"""Ouvre le fichier PDF avec la commande système appropriée"""
try:
@@ -152,32 +164,41 @@ def open_pdf(filename):
except (subprocess.CalledProcessError, FileNotFoundError, OSError):
return False
def main():
"""Fonction principale"""
# Vérifier les arguments de ligne de commande
if len(sys.argv) < 3:
print("Usage: python generate_math_exercises.py <min_table> <max_table> [num_exercises] [--open]")
print(
"Usage: python generate_math_exercises.py <min_table> <max_table> [num_exercises] [--open]"
)
print("Exemple: python generate_math_exercises.py 4 7 15")
print(" python generate_math_exercises.py 4 7 15 --open (pour ouvrir automatiquement le PDF)")
print(" python generate_math_exercises.py 5 5 10 (pour seulement la table de 5)")
print(
" python generate_math_exercises.py 4 7 15 --open (pour ouvrir automatiquement le PDF)"
)
print(
" python generate_math_exercises.py 5 5 10 (pour seulement la table de 5)"
)
sys.exit(1)
# Vérifier si l'option --open est présente
open_after_creation = "--open" in sys.argv
if open_after_creation:
sys.argv.remove("--open") # Retirer l'option de la liste des arguments
try:
min_table = int(sys.argv[1])
max_table = int(sys.argv[2])
if min_table < 1 or max_table < 1:
raise ValueError("Les tables doivent être supérieures à 0")
if min_table > max_table:
raise ValueError("La table minimale doit être inférieure ou égale à la table maximale")
raise ValueError(
"La table minimale doit être inférieure ou égale à la table maximale"
)
except ValueError as e:
print(f"Erreur: {e}")
sys.exit(1)
# Nombre d'exercices (par défaut 15)
num_exercises = 15
if len(sys.argv) > 3:
@@ -188,7 +209,7 @@ def main():
except ValueError as e:
print(f"Erreur: {e}")
sys.exit(1)
# Créer le PDF
try:
filename = create_math_exercises_pdf(min_table, max_table, num_exercises)
@@ -197,21 +218,26 @@ def main():
print(f"- Exercices pour la table de {min_table}")
else:
print(f"- Exercices pour les tables de {min_table} à {max_table}")
print(f"- {num_exercises} exercices MÉLANGÉS générés aléatoirement et numérotés de 1 à {num_exercises}")
print(
f"- {num_exercises} exercices MÉLANGÉS générés aléatoirement et numérotés de 1 à {num_exercises}"
)
print("- Types d'exercices : multiplications (·) et divisions (:) uniquement")
print("- Présentés en 3 colonnes pour économiser l'espace")
print("- Aucune opération en double (quand c'est possible)")
# Ouvrir le PDF si demandé
if open_after_creation:
if open_pdf(filename):
print(f"\nPDF ouvert automatiquement avec succès !")
print("\nPDF ouvert automatiquement avec succès !")
else:
print(f"\nImpossible d'ouvrir automatiquement le PDF. Vous pouvez l'ouvrir manuellement.")
print(
"\nImpossible d'ouvrir automatiquement le PDF. Vous pouvez l'ouvrir manuellement."
)
except Exception as e:
print(f"Erreur lors de la création du PDF: {e}")
sys.exit(1)
if __name__ == "__main__":
main()
main()
+31 -8
View File
@@ -1,9 +1,32 @@
annotated-doc==0.0.4
annotated-types==0.7.0
anyio==4.12.0
boto3==1.42.6
boto3-stubs==1.42.6
botocore==1.42.6
botocore-stubs==1.42.6
click==8.3.1
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
boto3==1.34.0
fastapi==0.124.0
fonttools==4.61.0
fpdf2==2.8.5
h11==0.16.0
idna==3.11
Jinja2==3.1.6
jmespath==1.0.1
MarkupSafe==3.0.3
pillow==12.0.0
pydantic==2.12.5
pydantic_core==2.41.5
python-dateutil==2.9.0.post0
python-multipart==0.0.20
s3transfer==0.16.0
six==1.17.0
sniffio==1.3.1
starlette==0.50.0
types-awscrt==0.29.2
types-s3transfer==0.16.0
typing-inspection==0.4.2
typing_extensions==4.15.0
urllib3==2.6.1
uvicorn==0.38.0