Compare commits
31 Commits
f94dd12216
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
e4db258bc5
|
|||
|
449adc60e0
|
|||
|
d68dc8411f
|
|||
|
60f776c56d
|
|||
|
7228dc4ec6
|
|||
|
e4989dcc8d
|
|||
|
3fdbfcf20e
|
|||
|
b17afbb5cd
|
|||
|
711a26c26c
|
|||
|
21c9bfd359
|
|||
|
3d720ff426
|
|||
|
d9dd1281c3
|
|||
|
edefce9703
|
|||
|
eb6da3d9cf
|
|||
|
bc55b95bda
|
|||
|
7284c60b93
|
|||
|
28b1c80b50
|
|||
|
8bedd8a97a
|
|||
|
e9db1b012d
|
|||
|
9e2d6eeb12
|
|||
|
3f1a3cf7a4
|
|||
|
b3426f7493
|
|||
|
cadc34f797
|
|||
|
cdaf3d9500
|
|||
|
1d47b5490e
|
|||
|
5d11fe9763
|
|||
|
e0dddb70ec
|
|||
|
fac44d2246
|
|||
|
3c07c89f24
|
|||
|
75548dab2b
|
|||
|
a67db405f7
|
@@ -17,3 +17,5 @@ app/static/*.pdf
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
.envrc
|
||||
s3cfg
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
creation_rules:
|
||||
- path_regex: .*
|
||||
pgp: >-
|
||||
7120085E6161EB9ABC2FD0117C26F0446A4D9603,
|
||||
E21007D7E97694AC900FF3434C6659F378169546
|
||||
+20
-24
@@ -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
@@ -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
|
||||
+371
-100
@@ -1,124 +1,224 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
import random
|
||||
import os
|
||||
import io
|
||||
import zipfile
|
||||
import tempfile
|
||||
from typing import List
|
||||
from fastapi import FastAPI, Request, Response
|
||||
from fastapi.responses import HTMLResponse, FileResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
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
|
||||
|
||||
import logging
|
||||
|
||||
# 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()
|
||||
|
||||
# Mount static files and templates
|
||||
app.mount("/static", StaticFiles(directory="app/static"), name="static")
|
||||
templates = Jinja2Templates(directory="app/templates")
|
||||
|
||||
|
||||
# Create bucket on startup
|
||||
try:
|
||||
create_bucket_if_not_exists(S3_BUCKET_NAME)
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not create/check S3 bucket: {e}")
|
||||
|
||||
|
||||
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:
|
||||
"""Crée un fichier PDF avec des exercices de mathématiques mélangés en 3 colonnes"""
|
||||
|
||||
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 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:
|
||||
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)
|
||||
|
||||
# Ajouter des informations sur la plage de tables
|
||||
if min_table == max_table:
|
||||
table_info = f'Tables de multiplication et division pour {min_table}'
|
||||
else:
|
||||
table_info = f'Tables de multiplication et division de {min_table} à {max_table}'
|
||||
|
||||
pdf.cell(0, 10, table_info, 0, 1, 'C', new_x="LMARGIN", new_y="NEXT")
|
||||
pdf.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):
|
||||
@@ -126,78 +226,249 @@ 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")
|
||||
|
||||
# Sauvegarder le PDF
|
||||
if min_table == max_table:
|
||||
filename = f'app/static/exercices_mathematiques_table_{min_table}_{num_exercises}_exercices.pdf'
|
||||
else:
|
||||
filename = f'app/static/exercices_mathematiques_tables_{min_table}_a_{max_table}_{num_exercises}_exercices.pdf'
|
||||
pdf.output(filename)
|
||||
return filename
|
||||
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})
|
||||
|
||||
|
||||
@app.post("/generate")
|
||||
async def generate_exercises_endpoint(request: ExerciseRequest):
|
||||
try:
|
||||
if request.min_table < 1 or request.max_table < 1:
|
||||
return {"error": "Les tables doivent être supérieures à 0"}
|
||||
if request.min_table > request.max_table:
|
||||
return {"error": "La table minimale doit être inférieure ou égale à la table maximale"}
|
||||
return {
|
||||
"error": "La table minimale doit être inférieure ou égale à la table maximale"
|
||||
}
|
||||
if request.num_exercises < 1:
|
||||
return {"error": "Le nombre d'exercices doit être supérieur à 0"}
|
||||
|
||||
filename = create_math_exercises_pdf(
|
||||
request.min_table,
|
||||
request.max_table,
|
||||
request.num_exercises
|
||||
|
||||
pdf_filename = create_math_exercises_pdf(
|
||||
request.min_table,
|
||||
request.max_table,
|
||||
request.num_exercises,
|
||||
request.multiplication_only,
|
||||
request.max_first_operand, # Pass the new parameter
|
||||
)
|
||||
|
||||
# Extract just the filename for the download link
|
||||
pdf_filename = os.path.basename(filename)
|
||||
|
||||
return {
|
||||
"message": "PDF généré avec succès!",
|
||||
"pdf_filename": pdf_filename,
|
||||
"min_table": request.min_table,
|
||||
"max_table": request.max_table,
|
||||
"num_exercises": request.num_exercises
|
||||
}
|
||||
|
||||
# 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):
|
||||
file_path = f"app/static/{filename}"
|
||||
if os.path.exists(file_path):
|
||||
return FileResponse(
|
||||
path=file_path,
|
||||
media_type='application/pdf',
|
||||
filename=filename
|
||||
)
|
||||
return {"error": "File not found"}
|
||||
# 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}"'},
|
||||
)
|
||||
|
||||
|
||||
@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)"""
|
||||
try:
|
||||
# Validate page and page_size parameters
|
||||
if page < 1:
|
||||
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": {
|
||||
"current_page": page,
|
||||
"page_size": page_size,
|
||||
"total_files": total_files,
|
||||
"total_pages": total_pages,
|
||||
"has_next": page < total_pages,
|
||||
"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}"
|
||||
}
|
||||
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:
|
||||
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,
|
||||
}
|
||||
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)}"}
|
||||
|
||||
|
||||
@app.post("/bulk-download")
|
||||
async def bulk_download(filenames: str = Form(...)):
|
||||
"""Download multiple PDF files as a zip archive"""
|
||||
try:
|
||||
# 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:
|
||||
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:
|
||||
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}"'
|
||||
},
|
||||
)
|
||||
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
@@ -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
|
||||
+736
-28
@@ -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,20 +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>
|
||||
@@ -58,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>
|
||||
@@ -68,6 +194,57 @@
|
||||
</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">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h5 class="card-title mb-0">Fichiers PDF Existants</h5>
|
||||
<button id="refreshListBtn" class="btn btn-outline-primary btn-sm">
|
||||
<span id="refreshText">Actualiser</span>
|
||||
<span id="refreshSpinner" class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="d-flex gap-2 mb-3">
|
||||
<button id="downloadSelectedBtn" class="btn btn-success btn-sm" disabled>
|
||||
Télécharger Sélectionnés
|
||||
</button>
|
||||
<button id="deleteSelectedBtn" class="btn btn-danger btn-sm" disabled>
|
||||
Supprimer Sélectionnés
|
||||
</button>
|
||||
<button id="selectAllBtn" class="btn btn-outline-secondary btn-sm">
|
||||
Tout Sélectionner
|
||||
</button>
|
||||
<button id="clearSelectionBtn" class="btn btn-outline-secondary btn-sm" disabled>
|
||||
Désélectionner
|
||||
</button>
|
||||
</div>
|
||||
<div id="pdfListContainer" class="mt-3">
|
||||
<p class="text-muted">Cliquez sur "Actualiser" pour charger la liste des fichiers existants.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="resultContainer" class="exercise-result d-none">
|
||||
<h5>Résultat</h5>
|
||||
<div id="resultMessage"></div>
|
||||
@@ -129,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>
|
||||
@@ -139,20 +348,430 @@
|
||||
</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 © 2025</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<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) {
|
||||
const refreshBtn = document.getElementById('refreshListBtn');
|
||||
const refreshText = document.getElementById('refreshText');
|
||||
const refreshSpinner = document.getElementById('refreshSpinner');
|
||||
const pdfListContainer = document.getElementById('pdfListContainer');
|
||||
|
||||
// Show loading state
|
||||
refreshText.textContent = 'Chargement...';
|
||||
refreshSpinner.classList.remove('d-none');
|
||||
refreshBtn.disabled = true;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/list?page=${page}&page_size=${pageSize}`);
|
||||
const result = await response.json();
|
||||
|
||||
if (result.error) {
|
||||
pdfListContainer.innerHTML = `<div class="alert alert-danger">${result.error}</div>`;
|
||||
} else {
|
||||
if (result.files.length === 0) {
|
||||
pdfListContainer.innerHTML = '<p class="text-muted">Aucun fichier PDF trouvé.</p>';
|
||||
} else {
|
||||
let html = '<div class="table-responsive"><table class="table table-striped table-hover">';
|
||||
html += '<thead><tr><th><input type="checkbox" id="selectAllHeader"></th><th>Nom du fichier</th><th>Date de création</th><th>Taille</th><th>Actions</th></tr></thead><tbody>';
|
||||
|
||||
result.files.forEach(file => {
|
||||
// Format date
|
||||
const date = new Date(file.LastModified);
|
||||
const formattedDate = date.toLocaleDateString('fr-FR', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
|
||||
// Format size
|
||||
const sizeInKB = Math.round(file.Size / 1024);
|
||||
|
||||
html += '<tr>';
|
||||
html += `<td><input type="checkbox" class="file-checkbox" data-filename="${file.Key}"></td>`;
|
||||
html += `<td>${file.Key}</td>`;
|
||||
html += `<td>${formattedDate}</td>`;
|
||||
html += `<td>${sizeInKB} KB</td>`;
|
||||
html += `<td>
|
||||
<a href="/download/${encodeURIComponent(file.Key)}" class="btn btn-success btn-sm me-2">Télécharger</a>
|
||||
<button class="btn btn-danger btn-sm delete-btn" data-filename="${file.Key}">Supprimer</button>
|
||||
</td>`;
|
||||
html += '</tr>';
|
||||
});
|
||||
|
||||
html += '</tbody></table></div>';
|
||||
|
||||
// Add pagination controls if needed
|
||||
if (result.pagination.total_pages > 1) {
|
||||
html += '<nav aria-label="Pagination des fichiers"><ul class="pagination justify-content-center">';
|
||||
|
||||
// Previous button
|
||||
if (result.pagination.has_prev) {
|
||||
html += `<li class="page-item"><a class="page-link" href="#" data-page="${result.pagination.current_page - 1}">Précédent</a></li>`;
|
||||
} else {
|
||||
html += '<li class="page-item disabled"><span class="page-link">Précédent</span></li>';
|
||||
}
|
||||
|
||||
// Page numbers
|
||||
for (let i = 1; i <= result.pagination.total_pages; i++) {
|
||||
if (i === result.pagination.current_page) {
|
||||
html += `<li class="page-item active"><span class="page-link">${i}</span></li>`;
|
||||
} else {
|
||||
html += `<li class="page-item"><a class="page-link" href="#" data-page="${i}">${i}</a></li>`;
|
||||
}
|
||||
}
|
||||
|
||||
// Next button
|
||||
if (result.pagination.has_next) {
|
||||
html += `<li class="page-item"><a class="page-link" href="#" data-page="${result.pagination.current_page + 1}">Suivant</a></li>`;
|
||||
} else {
|
||||
html += '<li class="page-item disabled"><span class="page-link">Suivant</span></li>';
|
||||
}
|
||||
|
||||
html += '</ul></nav>';
|
||||
|
||||
// Display total count
|
||||
html += `<div class="text-center text-muted small">
|
||||
Affichage ${(result.pagination.current_page - 1) * pageSize + 1}-${Math.min(result.pagination.current_page * pageSize, result.pagination.total_files)}
|
||||
sur ${result.pagination.total_files} fichiers
|
||||
</div>`;
|
||||
}
|
||||
|
||||
pdfListContainer.innerHTML = html;
|
||||
|
||||
// Add event listeners to pagination links
|
||||
document.querySelectorAll('.page-link[data-page]').forEach(link => {
|
||||
link.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
const page = parseInt(this.getAttribute('data-page'));
|
||||
loadPdfList(page);
|
||||
});
|
||||
});
|
||||
|
||||
// Add event listeners to delete buttons
|
||||
document.querySelectorAll('.delete-btn').forEach(button => {
|
||||
button.addEventListener('click', async function() {
|
||||
const filename = this.getAttribute('data-filename');
|
||||
if (confirm(`Êtes-vous sûr de vouloir supprimer le fichier "${filename}" ?`)) {
|
||||
try {
|
||||
const response = await fetch(`/delete/${encodeURIComponent(filename)}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (result.error) {
|
||||
alert(`Erreur: ${result.error}`);
|
||||
} else {
|
||||
// Refresh the list (stay on current page)
|
||||
loadPdfList(currentPage);
|
||||
alert(result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
alert(`Erreur lors de la suppression: ${error.message}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Add event listener for select all checkbox in header
|
||||
document.getElementById('selectAllHeader').addEventListener('change', function() {
|
||||
const checkboxes = document.querySelectorAll('.file-checkbox');
|
||||
checkboxes.forEach(checkbox => {
|
||||
checkbox.checked = this.checked;
|
||||
});
|
||||
updateBulkButtons();
|
||||
});
|
||||
|
||||
// Add event listeners to individual checkboxes
|
||||
document.querySelectorAll('.file-checkbox').forEach(checkbox => {
|
||||
checkbox.addEventListener('change', updateBulkButtons);
|
||||
});
|
||||
|
||||
// Update current page
|
||||
currentPage = page;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
pdfListContainer.innerHTML = `<div class="alert alert-danger">Erreur lors du chargement de la liste: ${error.message}</div>`;
|
||||
} finally {
|
||||
// Reset button state
|
||||
refreshText.textContent = 'Actualiser';
|
||||
refreshSpinner.classList.add('d-none');
|
||||
refreshBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Event listener for refresh button
|
||||
document.getElementById('refreshListBtn').addEventListener('click', function() {
|
||||
loadPdfList(1); // Reset to first page on refresh
|
||||
});
|
||||
|
||||
// Function to update bulk action buttons state
|
||||
function updateBulkButtons() {
|
||||
const selectedCount = document.querySelectorAll('.file-checkbox:checked').length;
|
||||
document.getElementById('downloadSelectedBtn').disabled = selectedCount === 0;
|
||||
document.getElementById('deleteSelectedBtn').disabled = selectedCount === 0;
|
||||
document.getElementById('clearSelectionBtn').disabled = selectedCount === 0;
|
||||
|
||||
// Update select all header checkbox
|
||||
const allChecked = selectedCount === document.querySelectorAll('.file-checkbox').length && selectedCount > 0;
|
||||
document.getElementById('selectAllHeader').checked = allChecked;
|
||||
}
|
||||
|
||||
// Load PDF list on page load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 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
|
||||
document.getElementById('selectAllBtn').addEventListener('click', function() {
|
||||
document.querySelectorAll('.file-checkbox').forEach(checkbox => {
|
||||
checkbox.checked = true;
|
||||
});
|
||||
updateBulkButtons();
|
||||
});
|
||||
|
||||
// Event listener for clear selection button
|
||||
document.getElementById('clearSelectionBtn').addEventListener('click', function() {
|
||||
document.querySelectorAll('.file-checkbox').forEach(checkbox => {
|
||||
checkbox.checked = false;
|
||||
});
|
||||
updateBulkButtons();
|
||||
});
|
||||
|
||||
// Event listener for download selected button
|
||||
document.getElementById('downloadSelectedBtn').addEventListener('click', async function() {
|
||||
const selectedFiles = Array.from(document.querySelectorAll('.file-checkbox:checked'))
|
||||
.map(checkbox => checkbox.getAttribute('data-filename'));
|
||||
|
||||
if (selectedFiles.length === 0) {
|
||||
alert('Veuillez sélectionner au moins un fichier à télécharger.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Create a form to submit the request (needed for file download)
|
||||
const formData = new FormData();
|
||||
formData.append('filenames', JSON.stringify(selectedFiles));
|
||||
|
||||
// Create a hidden form and submit it
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = '/bulk-download';
|
||||
form.style.display = 'none';
|
||||
|
||||
const input = document.createElement('input');
|
||||
input.type = 'hidden';
|
||||
input.name = 'filenames';
|
||||
input.value = JSON.stringify(selectedFiles);
|
||||
form.appendChild(input);
|
||||
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
document.body.removeChild(form);
|
||||
} catch (error) {
|
||||
alert(`Erreur lors du téléchargement: ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Event listener for delete selected button
|
||||
document.getElementById('deleteSelectedBtn').addEventListener('click', async function() {
|
||||
const selectedFiles = Array.from(document.querySelectorAll('.file-checkbox:checked'))
|
||||
.map(checkbox => checkbox.getAttribute('data-filename'));
|
||||
|
||||
if (selectedFiles.length === 0) {
|
||||
alert('Veuillez sélectionner au moins un fichier à supprimer.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm(`Êtes-vous sûr de vouloir supprimer ${selectedFiles.length} fichier(s) ?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Create form data
|
||||
const formData = new FormData();
|
||||
formData.append('filenames', JSON.stringify(selectedFiles));
|
||||
|
||||
const response = await fetch('/bulk-delete', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.error) {
|
||||
alert(`Erreur: ${result.error}`);
|
||||
} else {
|
||||
// Refresh the list (stay on current page)
|
||||
loadPdfList(currentPage);
|
||||
let message = result.message;
|
||||
if (result.errors && result.errors.length > 0) {
|
||||
message += `\nErreurs pour les fichiers: ${result.errors.join(', ')}`;
|
||||
}
|
||||
alert(message);
|
||||
}
|
||||
} catch (error) {
|
||||
alert(`Erreur lors de la suppression: ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
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');
|
||||
@@ -172,35 +791,47 @@
|
||||
body: JSON.stringify({
|
||||
min_table: minTable,
|
||||
max_table: maxTable,
|
||||
num_exercises: numExercises
|
||||
num_exercises: numExercises,
|
||||
multiplication_only: multiplicationOnly,
|
||||
max_first_operand: maxFirstOperand
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
const resultContainer = document.getElementById('resultContainer');
|
||||
const resultMessage = document.getElementById('resultMessage');
|
||||
const downloadLinkContainer = document.getElementById('downloadLinkContainer');
|
||||
const downloadLink = document.getElementById('downloadLink');
|
||||
|
||||
if (result.error) {
|
||||
resultMessage.innerHTML = `<div class="alert alert-danger">${result.error}</div>`;
|
||||
downloadLinkContainer.classList.add('d-none');
|
||||
} else {
|
||||
// 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');
|
||||
const exerciseType = multiplicationOnly ? "multiplications uniquement" : "multiplications et divisions";
|
||||
resultMessage.innerHTML = `
|
||||
<div class="alert alert-success">
|
||||
<h6>${result.message}</h6>
|
||||
<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 ${result.min_table} à ${result.max_table}</li>
|
||||
<li>${result.num_exercises} exercices générés</li>
|
||||
<li>Tables de ${minTable} à ${maxTable}</li>
|
||||
<li>${numExercises} exercices générés (${exerciseType})</li>
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
downloadLink.href = `/download/${result.pdf_filename}`;
|
||||
downloadLinkContainer.classList.remove('d-none');
|
||||
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');
|
||||
}
|
||||
|
||||
resultContainer.classList.remove('d-none');
|
||||
} catch (error) {
|
||||
const resultContainer = document.getElementById('resultContainer');
|
||||
const resultMessage = document.getElementById('resultMessage');
|
||||
@@ -209,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;
|
||||
}
|
||||
|
||||
@@ -7,8 +7,4 @@ data:
|
||||
PORT: "8000"
|
||||
LOG_LEVEL: "INFO"
|
||||
MAX_REQUEST_SIZE: "10mb"
|
||||
|
||||
# Security configuration
|
||||
SECURE_COOKIES: "true"
|
||||
CORS_ORIGINS: "math-exercises.local"
|
||||
REQUEST_TIMEOUT: "30s"
|
||||
+10
-11
@@ -46,30 +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 mounts for writable directories
|
||||
# Volume mount for temporary files
|
||||
volumeMounts:
|
||||
- name: static-volume
|
||||
mountPath: /app/app/static
|
||||
# Volumes
|
||||
- name: tmp-volume
|
||||
mountPath: /tmp
|
||||
volumes:
|
||||
- name: static-volume
|
||||
emptyDir: {}
|
||||
- name: tmp-volume
|
||||
emptyDir: {}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -16,6 +16,10 @@ spec:
|
||||
value: development
|
||||
- name: DEBUG
|
||||
value: "false"
|
||||
# Environment variables from S3 credentials secret
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: s3-credentials
|
||||
# Reduce resource consumption in development
|
||||
resources:
|
||||
requests:
|
||||
|
||||
@@ -17,4 +17,9 @@ images:
|
||||
# Development-specific labels
|
||||
commonLabels:
|
||||
environment: development
|
||||
security-level: standard
|
||||
security-level: standard
|
||||
|
||||
secretGenerator:
|
||||
- name: s3-credentials
|
||||
envs:
|
||||
- s3-credentials.env
|
||||
@@ -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
|
||||
@@ -8,7 +8,6 @@ metadata:
|
||||
# Security annotations
|
||||
seccomp.security.alpha.kubernetes.io/pod: docker/default
|
||||
spec:
|
||||
replicas: 1
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
@@ -33,5 +32,5 @@ spec:
|
||||
memory: "64Mi"
|
||||
cpu: "250m"
|
||||
limits:
|
||||
memory: "128Mi"
|
||||
cpu: "500m"
|
||||
memory: "256Mi"
|
||||
cpu: "1"
|
||||
@@ -8,18 +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.0
|
||||
newTag: 1.4.0
|
||||
|
||||
# Production-specific labels
|
||||
commonLabels:
|
||||
environment: production
|
||||
security-level: high
|
||||
|
||||
secretGenerator:
|
||||
- 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
|
||||
@@ -24,4 +24,8 @@ spec:
|
||||
drop:
|
||||
- ALL
|
||||
add:
|
||||
- NET_BIND_SERVICE
|
||||
- NET_BIND_SERVICE
|
||||
# Environment variables from S3 credentials secret
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: s3-credentials
|
||||
+82
-56
@@ -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
-7
@@ -1,8 +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
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user