Add collapsible ZIP contents and skipped files sections
- Make ZIP contents collapsible with toggle functionality - Make skipped files section collapsible with warning styling - Add JavaScript functions for collapsible sections - Create dedicated /completed-jobs route for archive purposes - Add navigation links between jobs pages - Include job creation timestamps for archive display - Improve job status display with file counts and statistics - Add datetime import for timestamp functionality
This commit is contained in:
1 parent
d28cccc957
commit
36f1722fbe
1 file changed
+206
-21
+206
-21
@@ -3,6 +3,7 @@ import tempfile
|
||||
import zipfile
|
||||
import threading
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from functools import wraps
|
||||
from flask import Flask, request, render_template_string, redirect, url_for, session, send_from_directory, flash, get_flashed_messages, jsonify
|
||||
from werkzeug.utils import secure_filename
|
||||
@@ -666,7 +667,8 @@ def upload_files():
|
||||
'zip_path': None,
|
||||
'zip_name': None,
|
||||
'processed_files': [],
|
||||
'skipped': []
|
||||
'skipped': [],
|
||||
'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
}
|
||||
|
||||
file_list = []
|
||||
@@ -756,7 +758,17 @@ def jobs():
|
||||
.flash.error { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
|
||||
.logout { position: absolute; top: 20px; right: 20px; background-color: #dc3545; color: white; text-decoration: none; padding: 10px 20px; border-radius: 5px; }
|
||||
.logout:hover { background-color: #c82333; }
|
||||
.nav-links { margin-bottom: 20px; }
|
||||
.nav-links a { color: #007bff; text-decoration: none; padding: 10px 20px; background-color: #f8f9fa; border-radius: 5px; margin: 0 10px; }
|
||||
.nav-links a:hover { background-color: #e9ecef; }
|
||||
.zip-content { background-color: #f8f9fa; padding: 10px; margin: 5px 0; border-radius: 5px; border-left: 4px solid #007bff; }
|
||||
.zip-toggle { cursor: pointer; background-color: #e9ecef; padding: 8px; margin: 5px 0; border-radius: 5px; border: 1px solid #dee2e6; }
|
||||
.zip-toggle:hover { background-color: #dee2e6; }
|
||||
.zip-details { display: none; margin-top: 10px; }
|
||||
.skipped-section { background-color: #fff3cd; border: 1px solid #ffeaa7; border-radius: 5px; padding: 10px; margin: 10px 0; }
|
||||
.skipped-toggle { cursor: pointer; background-color: #ffeaa7; padding: 8px; margin: 5px 0; border-radius: 5px; border: 1px solid #ffc107; font-weight: bold; color: #856404; }
|
||||
.skipped-toggle:hover { background-color: #ffc107; }
|
||||
.skipped-details { display: none; margin-top: 10px; }
|
||||
ul { list-style-type: none; padding: 0; }
|
||||
li { background-color: #ffffff; margin: 2px 0; padding: 5px; border-radius: 3px; }
|
||||
</style>
|
||||
@@ -765,6 +777,10 @@ def jobs():
|
||||
<a href="/logout" class="logout">Logout</a>
|
||||
<img src="/static/trbc-logo-full.png" alt="Logo" width="200">
|
||||
<h1>Processing Jobs</h1>
|
||||
<div class="nav-links" style="margin-bottom: 20px;">
|
||||
<a href="/">← Upload Files</a>
|
||||
<a href="/completed-jobs">View Completed Jobs Archive →</a>
|
||||
</div>
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
@@ -784,12 +800,16 @@ def jobs():
|
||||
<p><strong>ZIP Contents:</strong></p>
|
||||
{% for zip_name, pdf_files in job.zip_contents.items() %}
|
||||
<div class="zip-content">
|
||||
<strong>{{ zip_name }}:</strong>
|
||||
<ul>
|
||||
{% for pdf_file in pdf_files %}
|
||||
<li>{{ pdf_file }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div class="zip-toggle" onclick="toggleZipDetails(this)">
|
||||
<strong>{{ zip_name }}</strong> ({{ pdf_files|length }} files) ▶
|
||||
</div>
|
||||
<div class="zip-details">
|
||||
<ul>
|
||||
{% for pdf_file in pdf_files %}
|
||||
<li>{{ pdf_file }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
@@ -806,12 +826,18 @@ def jobs():
|
||||
</form></p>
|
||||
{% endif %}
|
||||
{% if job.skipped %}
|
||||
<p><strong>Skipped Files:</strong></p>
|
||||
<ul>
|
||||
{% for skip_info in job.skipped %}
|
||||
<li><strong>{{ skip_info[0] }}</strong>: {{ skip_info[1] }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div class="skipped-section">
|
||||
<div class="skipped-toggle" onclick="toggleSkippedDetails(this)">
|
||||
⚠️ Skipped Files ({{ job.skipped|length }}) ▶
|
||||
</div>
|
||||
<div class="skipped-details">
|
||||
<ul>
|
||||
{% for skip_info in job.skipped %}
|
||||
<li><strong>{{ skip_info[0] }}</strong>: {{ skip_info[1] }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -853,10 +879,14 @@ def jobs():
|
||||
<p><strong>ZIP Contents:</strong></p>
|
||||
${Object.entries(job.zip_contents).map(([zipName, pdfFiles]) => `
|
||||
<div class="zip-content">
|
||||
<strong>${zipName}:</strong>
|
||||
<ul>
|
||||
${pdfFiles.map(pdfFile => `<li>${pdfFile}</li>`).join('')}
|
||||
</ul>
|
||||
<div class="zip-toggle" onclick="toggleZipDetails(this)">
|
||||
<strong>${zipName}</strong> (${pdfFiles.length} files) ▶
|
||||
</div>
|
||||
<div class="zip-details">
|
||||
<ul>
|
||||
${pdfFiles.map(pdfFile => `<li>${pdfFile}</li>`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
` : ''}
|
||||
@@ -873,12 +903,19 @@ def jobs():
|
||||
</form></p>
|
||||
`}
|
||||
${job.skipped ? `
|
||||
<p><strong>Skipped Files:</strong></p>
|
||||
<ul>
|
||||
${job.skipped.map(skipInfo => `<li><strong>${skipInfo[0]}</strong>: ${skipInfo[1]}</li>`).join('')}
|
||||
</ul>
|
||||
<div class="skipped-section">
|
||||
<div class="skipped-toggle" onclick="toggleSkippedDetails(this)">
|
||||
⚠️ Skipped Files (${job.skipped.length}) ▶
|
||||
</div>
|
||||
<div class="skipped-details">
|
||||
<ul>
|
||||
${job.skipped.map(skipInfo => `<li><strong>${skipInfo[0]}</strong>: ${skipInfo[1]}</li>`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
` : ''}
|
||||
` : ''}
|
||||
`;
|
||||
jobsContainer.insertBefore(jobDiv, jobsContainer.querySelector('script') || jobsContainer.lastElementChild);
|
||||
});
|
||||
@@ -893,6 +930,21 @@ def jobs():
|
||||
|
||||
// Update jobs every 2 seconds
|
||||
setInterval(updateJobs, 2000);
|
||||
|
||||
// Collapsible section functions
|
||||
function toggleZipDetails(element) {
|
||||
const details = element.nextElementSibling;
|
||||
const isVisible = details.style.display === 'block';
|
||||
details.style.display = isVisible ? 'none' : 'block';
|
||||
element.innerHTML = element.innerHTML.replace(isVisible ? ' ▼' : ' ▶', isVisible ? ' ▶' : ' ▼');
|
||||
}
|
||||
|
||||
function toggleSkippedDetails(element) {
|
||||
const details = element.nextElementSibling;
|
||||
const isVisible = details.style.display === 'block';
|
||||
details.style.display = isVisible ? 'none' : 'block';
|
||||
element.innerHTML = element.innerHTML.replace(isVisible ? ' ▼' : ' ▶', isVisible ? ' ▶' : ' ▼');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -935,5 +987,138 @@ def delete_job(job_id):
|
||||
flash('Job not found.', 'error')
|
||||
return redirect('/jobs')
|
||||
|
||||
@app.route('/completed-jobs')
|
||||
@login_required
|
||||
def completed_jobs():
|
||||
# Filter only completed jobs
|
||||
completed_jobs = {job_id: job for job_id, job in job_queue.items() if job.get('status') == 'completed'}
|
||||
return render_template_string('''
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Completed Jobs Archive</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; background-color: #f4f4f4; margin: 0; padding: 20px; text-align: center; }
|
||||
img { margin-bottom: 20px; }
|
||||
h1 { color: #333; }
|
||||
.job { background-color: #fff; margin: 10px auto; padding: 15px; border-radius: 10px; box-shadow: 0 0 10px rgba(0,0,0,0.1); max-width: 1000px; text-align: left; }
|
||||
.status { padding: 5px 10px; border-radius: 5px; color: white; font-weight: bold; background-color: #28a745; }
|
||||
.download-btn { background-color: #007bff; color: white; text-decoration: none; padding: 8px 16px; border-radius: 5px; margin: 5px; display: inline-block; }
|
||||
.download-btn:hover { background-color: #0056b3; }
|
||||
.delete-btn { background-color: #dc3545; color: white; border: none; padding: 8px 16px; border-radius: 5px; margin: 5px; cursor: pointer; }
|
||||
.delete-btn:hover { background-color: #c82333; }
|
||||
.flash { padding: 10px; margin: 10px 0; border-radius: 5px; text-align: center; }
|
||||
.flash.success { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
|
||||
.flash.error { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
|
||||
.logout { position: absolute; top: 20px; right: 20px; background-color: #dc3545; color: white; text-decoration: none; padding: 10px 20px; border-radius: 5px; }
|
||||
.logout:hover { background-color: #c82333; }
|
||||
.nav-links { margin-bottom: 20px; }
|
||||
.nav-links a { color: #007bff; text-decoration: none; padding: 10px 20px; background-color: #f8f9fa; border-radius: 5px; margin: 0 10px; }
|
||||
.nav-links a:hover { background-color: #e9ecef; }
|
||||
.job-stats { background-color: #e9ecef; padding: 10px; border-radius: 5px; margin-bottom: 15px; }
|
||||
.file-count { color: #17a2b8; font-weight: bold; }
|
||||
.zip-content { background-color: #f8f9fa; padding: 10px; margin: 5px 0; border-radius: 5px; border-left: 4px solid #007bff; }
|
||||
.zip-toggle { cursor: pointer; background-color: #e9ecef; padding: 8px; margin: 5px 0; border-radius: 5px; border: 1px solid #dee2e6; }
|
||||
.zip-toggle:hover { background-color: #dee2e6; }
|
||||
.zip-details { display: none; margin-top: 10px; }
|
||||
.skipped-section { background-color: #fff3cd; border: 1px solid #ffeaa7; border-radius: 5px; padding: 10px; margin: 10px 0; }
|
||||
.skipped-toggle { cursor: pointer; background-color: #ffeaa7; padding: 8px; margin: 5px 0; border-radius: 5px; border: 1px solid #ffc107; font-weight: bold; color: #856404; }
|
||||
.skipped-toggle:hover { background-color: #ffc107; }
|
||||
.skipped-details { display: none; margin-top: 10px; }
|
||||
ul { list-style-type: none; padding: 0; }
|
||||
li { background-color: #ffffff; margin: 2px 0; padding: 5px; border-radius: 3px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="/logout" class="logout">Logout</a>
|
||||
<img src="/static/trbc-logo-full.png" alt="Logo" width="200">
|
||||
<h1>Completed Jobs Archive</h1>
|
||||
<div class="nav-links">
|
||||
<a href="/">← Upload Files</a>
|
||||
<a href="/jobs">View All Jobs →</a>
|
||||
</div>
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="flash {{ category }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% for job_id, job in jobs.items() %}
|
||||
<div class="job">
|
||||
<h3>Job {{ job_id[:8] }} - {{ job.created_at if job.created_at else 'Unknown Date' }}</h3>
|
||||
<div class="job-stats">
|
||||
<span class="status">Completed</span>
|
||||
<span class="file-count">{{ job.processed_files|selectattr('0', 'equalto', 'success')|list|length }} files processed</span>
|
||||
{% if job.skipped %}
|
||||
<span style="color: #856404; font-weight: bold;">{{ job.skipped|length }} files skipped</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if job.zip_contents %}
|
||||
<p><strong>Original ZIP Contents:</strong></p>
|
||||
{% for zip_name, pdf_files in job.zip_contents.items() %}
|
||||
<div class="zip-content">
|
||||
<div class="zip-toggle" onclick="toggleZipDetails(this)">
|
||||
<strong>{{ zip_name }}</strong> ({{ pdf_files|length }} files) ▶
|
||||
</div>
|
||||
<div class="zip-details">
|
||||
<ul>
|
||||
{% for pdf_file in pdf_files %}
|
||||
<li>{{ pdf_file }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% if job.zip_name %}
|
||||
<p><strong>Download:</strong> <a href="/download/{{ job.zip_name }}" class="download-btn">📁 Download Organized ZIP ({{ job.zip_name }})</a></p>
|
||||
{% endif %}
|
||||
|
||||
{% if job.skipped %}
|
||||
<div class="skipped-section">
|
||||
<div class="skipped-toggle" onclick="toggleSkippedDetails(this)">
|
||||
⚠️ Skipped Files ({{ job.skipped|length }}) ▶
|
||||
</div>
|
||||
<div class="skipped-details">
|
||||
<ul>
|
||||
{% for skip_info in job.skipped %}
|
||||
<li><strong>{{ skip_info[0] }}</strong>: {{ skip_info[1] }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="/delete/{{ job_id }}" style="display: inline;">
|
||||
<button type="submit" class="delete-btn" onclick="return confirm('Are you sure you want to permanently delete this completed job and its files?')">🗑️ Delete Job</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if not jobs %}
|
||||
<p>No completed jobs yet. <a href="/">Upload some files</a> to get started.</p>
|
||||
{% endif %}
|
||||
<script>
|
||||
// Collapsible section functions
|
||||
function toggleZipDetails(element) {
|
||||
const details = element.nextElementSibling;
|
||||
const isVisible = details.style.display === 'block';
|
||||
details.style.display = isVisible ? 'none' : 'block';
|
||||
element.innerHTML = element.innerHTML.replace(isVisible ? ' ▼' : ' ▶', isVisible ? ' ▶' : ' ▼');
|
||||
}
|
||||
|
||||
function toggleSkippedDetails(element) {
|
||||
const details = element.nextElementSibling;
|
||||
const isVisible = details.style.display === 'block';
|
||||
details.style.display = isVisible ? 'none' : 'block';
|
||||
element.innerHTML = element.innerHTML.replace(isVisible ? ' ▼' : ' ▶', isVisible ? ' ▶' : ' ▼');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
''', jobs=completed_jobs)
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True, port=3000)
|
||||
Reference in new issue
Block a user