feat: add pagination to file listing and improve UI
This commit is contained in:
+144
-1
@@ -3,9 +3,11 @@ import random
|
|||||||
import os
|
import os
|
||||||
import io
|
import io
|
||||||
import boto3
|
import boto3
|
||||||
|
import zipfile
|
||||||
|
import tempfile
|
||||||
from botocore.exceptions import ClientError
|
from botocore.exceptions import ClientError
|
||||||
from typing import List
|
from typing import List
|
||||||
from fastapi import FastAPI, Request, Response
|
from fastapi import FastAPI, Request, Response, Form
|
||||||
from fastapi.responses import HTMLResponse, StreamingResponse
|
from fastapi.responses import HTMLResponse, StreamingResponse
|
||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
@@ -91,6 +93,34 @@ def download_from_s3(bucket_name, object_name):
|
|||||||
print(f"Error downloading from S3: {e}")
|
print(f"Error downloading from S3: {e}")
|
||||||
return None
|
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):
|
class MathExercisesPDF(FPDF):
|
||||||
def header(self):
|
def header(self):
|
||||||
self.set_font('Helvetica', 'B', 16)
|
self.set_font('Helvetica', 'B', 16)
|
||||||
@@ -283,6 +313,119 @@ async def download_pdf(filename: str):
|
|||||||
headers={'Content-Disposition': f'attachment; filename="{filename}"'}
|
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:
|
||||||
|
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}
|
||||||
|
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:
|
||||||
|
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)}"}
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import uvicorn
|
import uvicorn
|
||||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||||
+308
-1
@@ -36,6 +36,8 @@
|
|||||||
<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 et division personnalisés en PDF</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title">Paramètres des Exercices</h5>
|
<h5 class="card-title">Paramètres des Exercices</h5>
|
||||||
@@ -68,6 +70,36 @@
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<div id="resultContainer" class="exercise-result d-none">
|
||||||
<h5>Résultat</h5>
|
<h5>Résultat</h5>
|
||||||
<div id="resultMessage"></div>
|
<div id="resultMessage"></div>
|
||||||
@@ -145,8 +177,280 @@
|
|||||||
</div>
|
</div>
|
||||||
</footer>
|
</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>
|
<script>
|
||||||
|
// Current page for pagination
|
||||||
|
let currentPage = 1;
|
||||||
|
const pageSize = 10;
|
||||||
|
|
||||||
|
// 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() {
|
||||||
|
loadPdfList(currentPage);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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) {
|
document.getElementById('exerciseForm').addEventListener('submit', async function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
@@ -181,6 +485,9 @@
|
|||||||
// Automatically trigger download
|
// Automatically trigger download
|
||||||
window.location.href = response.url;
|
window.location.href = response.url;
|
||||||
|
|
||||||
|
// Refresh the PDF list (reset to first page)
|
||||||
|
setTimeout(() => loadPdfList(1), 1000);
|
||||||
|
|
||||||
// Show success message
|
// Show success message
|
||||||
const resultContainer = document.getElementById('resultContainer');
|
const resultContainer = document.getElementById('resultContainer');
|
||||||
const resultMessage = document.getElementById('resultMessage');
|
const resultMessage = document.getElementById('resultMessage');
|
||||||
|
|||||||
@@ -33,4 +33,4 @@ spec:
|
|||||||
cpu: "250m"
|
cpu: "250m"
|
||||||
limits:
|
limits:
|
||||||
memory: "128Mi"
|
memory: "128Mi"
|
||||||
cpu: "500m"
|
cpu: "1"
|
||||||
@@ -17,7 +17,7 @@ patchesStrategicMerge:
|
|||||||
images:
|
images:
|
||||||
- name: math-exercises
|
- name: math-exercises
|
||||||
newName: harbor.cl1.parano.ch/library/math-exercice
|
newName: harbor.cl1.parano.ch/library/math-exercice
|
||||||
newTag: 1.0.0
|
newTag: 1.0.1
|
||||||
|
|
||||||
# Production-specific labels
|
# Production-specific labels
|
||||||
commonLabels:
|
commonLabels:
|
||||||
|
|||||||
Reference in New Issue
Block a user