Spaces:
Running
Running
t commited on
Commit ·
2b07d6b
1
Parent(s): fb7e5e9
fix: consolidate pending fixes for PDF manager UI, dashboard filtering, and SQL syntax
Browse files- Removed corrupted 'arial.ttf'.
- Updated 'dashboard.py' to filter out 'final_pdf' sessions.
- Applied migrations in 'database.py' for session metadata.
- Fixed SQL syntax error in 'routes/library.py'.
- Set session type to 'final_pdf' in 'routes/upload.py'.
- Refined 'templates/pdf_manager.html' with search, dropdowns, and individual delete buttons.
- dashboard.py +1 -1
- database.py +15 -0
- routes/library.py +5 -3
- routes/upload.py +3 -1
- templates/pdf_manager.html +106 -4
dashboard.py
CHANGED
|
@@ -140,7 +140,7 @@ def dashboard():
|
|
| 140 |
COUNT(CASE WHEN i.image_type = 'cropped' THEN 1 END) as question_count
|
| 141 |
FROM sessions s
|
| 142 |
LEFT JOIN images i ON s.id = i.session_id
|
| 143 |
-
WHERE s.user_id = ?
|
| 144 |
GROUP BY s.id, s.created_at, s.original_filename, s.persist, s.name, s.session_type
|
| 145 |
ORDER BY s.created_at DESC
|
| 146 |
""", (current_user.id,)).fetchall()
|
|
|
|
| 140 |
COUNT(CASE WHEN i.image_type = 'cropped' THEN 1 END) as question_count
|
| 141 |
FROM sessions s
|
| 142 |
LEFT JOIN images i ON s.id = i.session_id
|
| 143 |
+
WHERE s.user_id = ? AND (s.session_type IS NULL OR s.session_type != 'final_pdf')
|
| 144 |
GROUP BY s.id, s.created_at, s.original_filename, s.persist, s.name, s.session_type
|
| 145 |
ORDER BY s.created_at DESC
|
| 146 |
""", (current_user.id,)).fetchall()
|
database.py
CHANGED
|
@@ -263,6 +263,21 @@ def setup_database():
|
|
| 263 |
except sqlite3.OperationalError:
|
| 264 |
cursor.execute("ALTER TABLE questions ADD COLUMN chapter TEXT")
|
| 265 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 266 |
# --- Multi-user Migrations ---
|
| 267 |
try:
|
| 268 |
cursor.execute("SELECT user_id FROM sessions LIMIT 1")
|
|
|
|
| 263 |
except sqlite3.OperationalError:
|
| 264 |
cursor.execute("ALTER TABLE questions ADD COLUMN chapter TEXT")
|
| 265 |
|
| 266 |
+
try:
|
| 267 |
+
cursor.execute("SELECT subject FROM sessions LIMIT 1")
|
| 268 |
+
except sqlite3.OperationalError:
|
| 269 |
+
cursor.execute("ALTER TABLE sessions ADD COLUMN subject TEXT")
|
| 270 |
+
|
| 271 |
+
try:
|
| 272 |
+
cursor.execute("SELECT tags FROM sessions LIMIT 1")
|
| 273 |
+
except sqlite3.OperationalError:
|
| 274 |
+
cursor.execute("ALTER TABLE sessions ADD COLUMN tags TEXT")
|
| 275 |
+
|
| 276 |
+
try:
|
| 277 |
+
cursor.execute("SELECT notes FROM sessions LIMIT 1")
|
| 278 |
+
except sqlite3.OperationalError:
|
| 279 |
+
cursor.execute("ALTER TABLE sessions ADD COLUMN notes TEXT")
|
| 280 |
+
|
| 281 |
# --- Multi-user Migrations ---
|
| 282 |
try:
|
| 283 |
cursor.execute("SELECT user_id FROM sessions LIMIT 1")
|
routes/library.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
| 1 |
import os
|
|
|
|
|
|
|
| 2 |
from datetime import datetime
|
| 3 |
-
from flask import request, jsonify, render_template, redirect, url_for, send_file
|
| 4 |
from .common import main_bp, get_db_connection, login_required, current_user
|
| 5 |
from database import get_folder_tree, get_all_descendant_folder_ids
|
| 6 |
from strings import METHOD_POST, METHOD_DELETE
|
|
@@ -222,7 +224,7 @@ def bulk_move_pdfs():
|
|
| 222 |
def get_all_subjects_and_tags():
|
| 223 |
conn = get_db_connection()
|
| 224 |
subjects = [row['subject'] for row in conn.execute('SELECT DISTINCT subject FROM generated_pdfs WHERE subject IS NOT NULL AND user_id = ?', (current_user.id,)).fetchall()]
|
| 225 |
-
tags_query = conn.execute(
|
| 226 |
all_tags = set()
|
| 227 |
for row in tags_query:
|
| 228 |
for t in row['tags'].split(','): all_tags.add(t.strip())
|
|
@@ -233,7 +235,7 @@ def get_all_subjects_and_tags():
|
|
| 233 |
def get_metadata_suggestions():
|
| 234 |
conn = get_db_connection()
|
| 235 |
subjects = [row['subject'] for row in conn.execute('SELECT DISTINCT subject FROM generated_pdfs WHERE subject IS NOT NULL AND user_id = ?', (current_user.id,)).fetchall()]
|
| 236 |
-
tags_query = conn.execute(
|
| 237 |
all_tags = set()
|
| 238 |
for row in tags_query:
|
| 239 |
for t in row['tags'].split(','): all_tags.add(t.strip())
|
|
|
|
| 1 |
import os
|
| 2 |
+
import io
|
| 3 |
+
import zipfile
|
| 4 |
from datetime import datetime
|
| 5 |
+
from flask import request, jsonify, render_template, redirect, url_for, send_file, current_app
|
| 6 |
from .common import main_bp, get_db_connection, login_required, current_user
|
| 7 |
from database import get_folder_tree, get_all_descendant_folder_ids
|
| 8 |
from strings import METHOD_POST, METHOD_DELETE
|
|
|
|
| 224 |
def get_all_subjects_and_tags():
|
| 225 |
conn = get_db_connection()
|
| 226 |
subjects = [row['subject'] for row in conn.execute('SELECT DISTINCT subject FROM generated_pdfs WHERE subject IS NOT NULL AND user_id = ?', (current_user.id,)).fetchall()]
|
| 227 |
+
tags_query = conn.execute("SELECT DISTINCT tags FROM generated_pdfs WHERE tags IS NOT NULL AND tags != '' AND user_id = ?", (current_user.id,)).fetchall()
|
| 228 |
all_tags = set()
|
| 229 |
for row in tags_query:
|
| 230 |
for t in row['tags'].split(','): all_tags.add(t.strip())
|
|
|
|
| 235 |
def get_metadata_suggestions():
|
| 236 |
conn = get_db_connection()
|
| 237 |
subjects = [row['subject'] for row in conn.execute('SELECT DISTINCT subject FROM generated_pdfs WHERE subject IS NOT NULL AND user_id = ?', (current_user.id,)).fetchall()]
|
| 238 |
+
tags_query = conn.execute("SELECT DISTINCT tags FROM generated_pdfs WHERE tags IS NOT NULL AND tags != '' AND user_id = ?", (current_user.id,)).fetchall()
|
| 239 |
all_tags = set()
|
| 240 |
for row in tags_query:
|
| 241 |
for t in row['tags'].split(','): all_tags.add(t.strip())
|
routes/upload.py
CHANGED
|
@@ -193,7 +193,9 @@ def handle_final_pdf_upload():
|
|
| 193 |
conn = get_db_connection()
|
| 194 |
def process_and_save_pdf(file_content, original_filename):
|
| 195 |
sid = str(uuid.uuid4())
|
| 196 |
-
|
|
|
|
|
|
|
| 197 |
out_name = f"{sid}_{secure_filename(original_filename)}"
|
| 198 |
with open(os.path.join(current_app.config['OUTPUT_FOLDER'], out_name), 'wb') as f: f.write(file_content)
|
| 199 |
conn.execute('INSERT INTO generated_pdfs (session_id, filename, subject, tags, notes, source_filename, user_id) VALUES (?, ?, ?, ?, ?, ?, ?)', (sid, out_name, subject, tags, notes, original_filename, current_user.id))
|
|
|
|
| 193 |
conn = get_db_connection()
|
| 194 |
def process_and_save_pdf(file_content, original_filename):
|
| 195 |
sid = str(uuid.uuid4())
|
| 196 |
+
# Associate session with user and mark as final_pdf
|
| 197 |
+
conn.execute('INSERT INTO sessions (id, original_filename, user_id, session_type) VALUES (?, ?, ?, ?)',
|
| 198 |
+
(sid, original_filename, current_user.id, 'final_pdf'))
|
| 199 |
out_name = f"{sid}_{secure_filename(original_filename)}"
|
| 200 |
with open(os.path.join(current_app.config['OUTPUT_FOLDER'], out_name), 'wb') as f: f.write(file_content)
|
| 201 |
conn.execute('INSERT INTO generated_pdfs (session_id, filename, subject, tags, notes, source_filename, user_id) VALUES (?, ?, ?, ?, ?, ?, ?)', (sid, out_name, subject, tags, notes, original_filename, current_user.id))
|
templates/pdf_manager.html
CHANGED
|
@@ -43,6 +43,9 @@
|
|
| 43 |
.table-hover .highlighted-row {
|
| 44 |
background-color: #0d6efd !important;
|
| 45 |
}
|
|
|
|
|
|
|
|
|
|
| 46 |
|
| 47 |
/* Tom Select Dark Mode Fix */
|
| 48 |
.ts-control {
|
|
@@ -78,7 +81,14 @@
|
|
| 78 |
<!-- Header & Actions -->
|
| 79 |
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-4">
|
| 80 |
<h1 class="mb-0">PDF Manager</h1>
|
| 81 |
-
<div class="d-flex flex-wrap gap-2">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
{% if all_view %}
|
| 83 |
<a href="/pdf_manager" class="btn btn-info">Show Folders</a>
|
| 84 |
{% else %}
|
|
@@ -130,8 +140,18 @@
|
|
| 130 |
<h5 class="card-title mt-2">{{ folder.name }}</h5>
|
| 131 |
<p class="card-text text-muted">{{ folder.created_at.strftime('%Y-%m-%d %I:%M %p') }}</p>
|
| 132 |
</div>
|
| 133 |
-
<div class="card-footer
|
| 134 |
<input type="checkbox" class="form-check-input item-checkbox" data-item-type="folder" value="{{ folder.id }}">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
</div>
|
| 136 |
</div>
|
| 137 |
</div>
|
|
@@ -181,9 +201,20 @@
|
|
| 181 |
</div>
|
| 182 |
<div class="card-footer d-flex justify-content-between align-items-center">
|
| 183 |
<small class="text-muted">{{ pdf.created_at.strftime('%Y-%m-%d %I:%M %p') }}</small>
|
| 184 |
-
<
|
| 185 |
{% if pdf.persist %}<i class="bi bi-pin-angle-fill text-primary" title="Persisted"></i>{% endif %}
|
| 186 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
</div>
|
| 188 |
</div>
|
| 189 |
</div>
|
|
@@ -367,6 +398,77 @@
|
|
| 367 |
cb.addEventListener('change', updateBulkButtons);
|
| 368 |
});
|
| 369 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 370 |
document.querySelectorAll('.folder-card').forEach(card => {
|
| 371 |
card.addEventListener('click', e => {
|
| 372 |
// If the click target is the folder name link, let the link handle it
|
|
|
|
| 43 |
.table-hover .highlighted-row {
|
| 44 |
background-color: #0d6efd !important;
|
| 45 |
}
|
| 46 |
+
.no-caret::after {
|
| 47 |
+
display: none !important;
|
| 48 |
+
}
|
| 49 |
|
| 50 |
/* Tom Select Dark Mode Fix */
|
| 51 |
.ts-control {
|
|
|
|
| 81 |
<!-- Header & Actions -->
|
| 82 |
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-4">
|
| 83 |
<h1 class="mb-0">PDF Manager</h1>
|
| 84 |
+
<div class="d-flex flex-wrap gap-2 align-items-center">
|
| 85 |
+
<form action="/pdf_manager" method="get" class="d-flex gap-2">
|
| 86 |
+
<input type="text" name="search" class="form-control bg-dark text-white border-secondary" placeholder="Search..." value="{{ request.args.get('search', '') }}">
|
| 87 |
+
<button type="submit" class="btn btn-outline-secondary"><i class="bi bi-search"></i></button>
|
| 88 |
+
{% if request.args.get('search') %}
|
| 89 |
+
<a href="/pdf_manager" class="btn btn-outline-danger"><i class="bi bi-x-lg"></i></a>
|
| 90 |
+
{% endif %}
|
| 91 |
+
</form>
|
| 92 |
{% if all_view %}
|
| 93 |
<a href="/pdf_manager" class="btn btn-info">Show Folders</a>
|
| 94 |
{% else %}
|
|
|
|
| 140 |
<h5 class="card-title mt-2">{{ folder.name }}</h5>
|
| 141 |
<p class="card-text text-muted">{{ folder.created_at.strftime('%Y-%m-%d %I:%M %p') }}</p>
|
| 142 |
</div>
|
| 143 |
+
<div class="card-footer d-flex justify-content-between align-items-center">
|
| 144 |
<input type="checkbox" class="form-check-input item-checkbox" data-item-type="folder" value="{{ folder.id }}">
|
| 145 |
+
<div class="dropdown">
|
| 146 |
+
<button class="btn btn-sm btn-outline-secondary dropdown-toggle no-caret" type="button" data-bs-toggle="dropdown">
|
| 147 |
+
<i class="bi bi-three-dots-vertical"></i>
|
| 148 |
+
</button>
|
| 149 |
+
<ul class="dropdown-menu dropdown-menu-dark dropdown-menu-end">
|
| 150 |
+
<li><a class="dropdown-item rename-single-btn" href="#" data-id="{{ folder.id }}" data-type="folder"><i class="bi bi-pencil-square me-2"></i> Rename</a></li>
|
| 151 |
+
<li><hr class="dropdown-divider"></li>
|
| 152 |
+
<li><a class="dropdown-item text-danger delete-item-btn" href="#" data-item-type="folder" data-id="{{ folder.id }}"><i class="bi bi-trash me-2"></i> Delete</a></li>
|
| 153 |
+
</ul>
|
| 154 |
+
</div>
|
| 155 |
</div>
|
| 156 |
</div>
|
| 157 |
</div>
|
|
|
|
| 201 |
</div>
|
| 202 |
<div class="card-footer d-flex justify-content-between align-items-center">
|
| 203 |
<small class="text-muted">{{ pdf.created_at.strftime('%Y-%m-%d %I:%M %p') }}</small>
|
| 204 |
+
<div class="d-flex align-items-center gap-2">
|
| 205 |
{% if pdf.persist %}<i class="bi bi-pin-angle-fill text-primary" title="Persisted"></i>{% endif %}
|
| 206 |
+
<div class="dropdown">
|
| 207 |
+
<button class="btn btn-sm btn-outline-secondary dropdown-toggle no-caret" type="button" data-bs-toggle="dropdown">
|
| 208 |
+
<i class="bi bi-three-dots-vertical"></i>
|
| 209 |
+
</button>
|
| 210 |
+
<ul class="dropdown-menu dropdown-menu-dark dropdown-menu-end">
|
| 211 |
+
<li><a class="dropdown-item edit-single-btn" href="#" data-id="{{ pdf.id }}"><i class="bi bi-info-circle me-2"></i> Details</a></li>
|
| 212 |
+
<li><a class="dropdown-item" href="{{ url_for('main.download_file', filename=pdf.filename) }}"><i class="bi bi-download me-2"></i> Download</a></li>
|
| 213 |
+
<li><hr class="dropdown-divider"></li>
|
| 214 |
+
<li><a class="dropdown-item text-danger delete-item-btn" href="#" data-item-type="pdf" data-id="{{ pdf.id }}"><i class="bi bi-trash me-2"></i> Delete</a></li>
|
| 215 |
+
</ul>
|
| 216 |
+
</div>
|
| 217 |
+
</div>
|
| 218 |
</div>
|
| 219 |
</div>
|
| 220 |
</div>
|
|
|
|
| 398 |
cb.addEventListener('change', updateBulkButtons);
|
| 399 |
});
|
| 400 |
|
| 401 |
+
// Single Deletion
|
| 402 |
+
document.querySelectorAll('.delete-item-btn').forEach(btn => {
|
| 403 |
+
btn.addEventListener('click', async (e) => {
|
| 404 |
+
e.preventDefault();
|
| 405 |
+
e.stopPropagation();
|
| 406 |
+
const itemType = btn.dataset.itemType;
|
| 407 |
+
const itemId = btn.dataset.id;
|
| 408 |
+
if (!confirm(`Are you sure you want to delete this ${itemType}?`)) return;
|
| 409 |
+
|
| 410 |
+
const url = itemType === 'folder' ? `/delete_folder/${itemId}` : `/delete_generated_pdf/${itemId}`;
|
| 411 |
+
const response = await fetch(url, { method: 'DELETE' });
|
| 412 |
+
if (response.ok) {
|
| 413 |
+
location.reload();
|
| 414 |
+
} else {
|
| 415 |
+
alert('Failed to delete item.');
|
| 416 |
+
}
|
| 417 |
+
});
|
| 418 |
+
});
|
| 419 |
+
|
| 420 |
+
// Single Rename
|
| 421 |
+
document.querySelectorAll('.rename-single-btn').forEach(btn => {
|
| 422 |
+
btn.addEventListener('click', (e) => {
|
| 423 |
+
e.preventDefault();
|
| 424 |
+
e.stopPropagation();
|
| 425 |
+
|
| 426 |
+
const card = btn.closest('.card');
|
| 427 |
+
const titleElement = card.querySelector('.card-title') || card.querySelector('.fw-bold');
|
| 428 |
+
const currentName = titleElement.textContent.trim();
|
| 429 |
+
const itemType = btn.dataset.type || 'pdf';
|
| 430 |
+
const itemId = btn.dataset.id;
|
| 431 |
+
|
| 432 |
+
titleElement.innerHTML = `<input type="text" class="editable-input" value="${currentName}" />`;
|
| 433 |
+
const input = titleElement.querySelector('input');
|
| 434 |
+
input.focus();
|
| 435 |
+
input.select();
|
| 436 |
+
|
| 437 |
+
const saveChanges = async () => {
|
| 438 |
+
const newName = input.value;
|
| 439 |
+
if (newName && newName !== currentName) {
|
| 440 |
+
const response = await fetch('/rename_item', {
|
| 441 |
+
method: 'POST',
|
| 442 |
+
headers: { 'Content-Type': 'application/json' },
|
| 443 |
+
body: JSON.stringify({
|
| 444 |
+
item_type: itemType,
|
| 445 |
+
item_id: itemId,
|
| 446 |
+
new_name: newName
|
| 447 |
+
})
|
| 448 |
+
});
|
| 449 |
+
if (response.ok) location.reload();
|
| 450 |
+
else {
|
| 451 |
+
alert('Failed to rename.');
|
| 452 |
+
titleElement.textContent = currentName;
|
| 453 |
+
}
|
| 454 |
+
} else {
|
| 455 |
+
titleElement.textContent = currentName;
|
| 456 |
+
}
|
| 457 |
+
};
|
| 458 |
+
|
| 459 |
+
input.addEventListener('blur', saveChanges);
|
| 460 |
+
input.addEventListener('keydown', ev => {
|
| 461 |
+
if (ev.key === 'Enter') {
|
| 462 |
+
ev.preventDefault();
|
| 463 |
+
saveChanges();
|
| 464 |
+
}
|
| 465 |
+
if (ev.key === 'Escape') titleElement.textContent = currentName;
|
| 466 |
+
});
|
| 467 |
+
});
|
| 468 |
+
});
|
| 469 |
+
|
| 470 |
+
// Single Edit
|
| 471 |
+
|
| 472 |
document.querySelectorAll('.folder-card').forEach(card => {
|
| 473 |
card.addEventListener('click', e => {
|
| 474 |
// If the click target is the folder name link, let the link handle it
|