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:
nmemmert committed 2025-11-06 12:35:38 -05:00
1 parent d28cccc957
commit 36f1722fbe
1 file changed
+206 -21
+206 -21
View File
@@ -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)