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 zipfile
|
||||||
import threading
|
import threading
|
||||||
import uuid
|
import uuid
|
||||||
|
from datetime import datetime
|
||||||
from functools import wraps
|
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 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
|
from werkzeug.utils import secure_filename
|
||||||
@@ -666,7 +667,8 @@ def upload_files():
|
|||||||
'zip_path': None,
|
'zip_path': None,
|
||||||
'zip_name': None,
|
'zip_name': None,
|
||||||
'processed_files': [],
|
'processed_files': [],
|
||||||
'skipped': []
|
'skipped': [],
|
||||||
|
'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||||
}
|
}
|
||||||
|
|
||||||
file_list = []
|
file_list = []
|
||||||
@@ -756,7 +758,17 @@ def jobs():
|
|||||||
.flash.error { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
|
.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 { 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; }
|
.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-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; }
|
ul { list-style-type: none; padding: 0; }
|
||||||
li { background-color: #ffffff; margin: 2px 0; padding: 5px; border-radius: 3px; }
|
li { background-color: #ffffff; margin: 2px 0; padding: 5px; border-radius: 3px; }
|
||||||
</style>
|
</style>
|
||||||
@@ -765,6 +777,10 @@ def jobs():
|
|||||||
<a href="/logout" class="logout">Logout</a>
|
<a href="/logout" class="logout">Logout</a>
|
||||||
<img src="/static/trbc-logo-full.png" alt="Logo" width="200">
|
<img src="/static/trbc-logo-full.png" alt="Logo" width="200">
|
||||||
<h1>Processing Jobs</h1>
|
<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) %}
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
{% if messages %}
|
{% if messages %}
|
||||||
{% for category, message in messages %}
|
{% for category, message in messages %}
|
||||||
@@ -784,12 +800,16 @@ def jobs():
|
|||||||
<p><strong>ZIP Contents:</strong></p>
|
<p><strong>ZIP Contents:</strong></p>
|
||||||
{% for zip_name, pdf_files in job.zip_contents.items() %}
|
{% for zip_name, pdf_files in job.zip_contents.items() %}
|
||||||
<div class="zip-content">
|
<div class="zip-content">
|
||||||
<strong>{{ zip_name }}:</strong>
|
<div class="zip-toggle" onclick="toggleZipDetails(this)">
|
||||||
<ul>
|
<strong>{{ zip_name }}</strong> ({{ pdf_files|length }} files) ▶
|
||||||
{% for pdf_file in pdf_files %}
|
</div>
|
||||||
<li>{{ pdf_file }}</li>
|
<div class="zip-details">
|
||||||
{% endfor %}
|
<ul>
|
||||||
</ul>
|
{% for pdf_file in pdf_files %}
|
||||||
|
<li>{{ pdf_file }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -806,12 +826,18 @@ def jobs():
|
|||||||
</form></p>
|
</form></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if job.skipped %}
|
{% if job.skipped %}
|
||||||
<p><strong>Skipped Files:</strong></p>
|
<div class="skipped-section">
|
||||||
<ul>
|
<div class="skipped-toggle" onclick="toggleSkippedDetails(this)">
|
||||||
{% for skip_info in job.skipped %}
|
⚠️ Skipped Files ({{ job.skipped|length }}) ▶
|
||||||
<li><strong>{{ skip_info[0] }}</strong>: {{ skip_info[1] }}</li>
|
</div>
|
||||||
{% endfor %}
|
<div class="skipped-details">
|
||||||
</ul>
|
<ul>
|
||||||
|
{% for skip_info in job.skipped %}
|
||||||
|
<li><strong>{{ skip_info[0] }}</strong>: {{ skip_info[1] }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
@@ -853,10 +879,14 @@ def jobs():
|
|||||||
<p><strong>ZIP Contents:</strong></p>
|
<p><strong>ZIP Contents:</strong></p>
|
||||||
${Object.entries(job.zip_contents).map(([zipName, pdfFiles]) => `
|
${Object.entries(job.zip_contents).map(([zipName, pdfFiles]) => `
|
||||||
<div class="zip-content">
|
<div class="zip-content">
|
||||||
<strong>${zipName}:</strong>
|
<div class="zip-toggle" onclick="toggleZipDetails(this)">
|
||||||
<ul>
|
<strong>${zipName}</strong> (${pdfFiles.length} files) ▶
|
||||||
${pdfFiles.map(pdfFile => `<li>${pdfFile}</li>`).join('')}
|
</div>
|
||||||
</ul>
|
<div class="zip-details">
|
||||||
|
<ul>
|
||||||
|
${pdfFiles.map(pdfFile => `<li>${pdfFile}</li>`).join('')}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`).join('')}
|
`).join('')}
|
||||||
` : ''}
|
` : ''}
|
||||||
@@ -873,12 +903,19 @@ def jobs():
|
|||||||
</form></p>
|
</form></p>
|
||||||
`}
|
`}
|
||||||
${job.skipped ? `
|
${job.skipped ? `
|
||||||
<p><strong>Skipped Files:</strong></p>
|
<div class="skipped-section">
|
||||||
<ul>
|
<div class="skipped-toggle" onclick="toggleSkippedDetails(this)">
|
||||||
${job.skipped.map(skipInfo => `<li><strong>${skipInfo[0]}</strong>: ${skipInfo[1]}</li>`).join('')}
|
⚠️ Skipped Files (${job.skipped.length}) ▶
|
||||||
</ul>
|
</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);
|
jobsContainer.insertBefore(jobDiv, jobsContainer.querySelector('script') || jobsContainer.lastElementChild);
|
||||||
});
|
});
|
||||||
@@ -893,6 +930,21 @@ def jobs():
|
|||||||
|
|
||||||
// Update jobs every 2 seconds
|
// Update jobs every 2 seconds
|
||||||
setInterval(updateJobs, 2000);
|
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>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -935,5 +987,138 @@ def delete_job(job_id):
|
|||||||
flash('Job not found.', 'error')
|
flash('Job not found.', 'error')
|
||||||
return redirect('/jobs')
|
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__':
|
if __name__ == '__main__':
|
||||||
app.run(debug=True, port=3000)
|
app.run(debug=True, port=3000)
|
||||||
Reference in new issue
Block a user