Add revision history log and music files ZIP backup
This commit is contained in:
parent
8246f641a2
commit
cda4d57859
4 changed files with 127 additions and 14 deletions
50
app.py
50
app.py
|
|
@ -58,7 +58,7 @@ from werkzeug.utils import secure_filename
|
||||||
# Project imports
|
# Project imports
|
||||||
from config import Config
|
from config import Config
|
||||||
from models import init_db, close_db, create_request, get_request_by_id, get_request_by_token, list_requests, update_request, now_utc, delete_request, reset_all_requests, get_db
|
from models import init_db, close_db, create_request, get_request_by_id, get_request_by_token, list_requests, update_request, now_utc, delete_request, reset_all_requests, get_db
|
||||||
from models import SCHEMA, get_requests_by_email
|
from models import SCHEMA, get_requests_by_email, log_revision, list_revision_history
|
||||||
|
|
||||||
# Audio metadata import
|
# Audio metadata import
|
||||||
from mutagen.mp3 import MP3
|
from mutagen.mp3 import MP3
|
||||||
|
|
@ -630,6 +630,8 @@ def revise(token):
|
||||||
# Increment revision counter and archive current files before new versions are uploaded.
|
# Increment revision counter and archive current files before new versions are uploaded.
|
||||||
new_count = current_count + 1
|
new_count = current_count + 1
|
||||||
upload_dir = Path(current_app.config['UPLOAD_FOLDER']) / str(req['id'])
|
upload_dir = Path(current_app.config['UPLOAD_FOLDER']) / str(req['id'])
|
||||||
|
old_a, old_b = req.get('song_a_path'), req.get('song_b_path')
|
||||||
|
new_a, new_b = old_a, old_b
|
||||||
if upload_dir.exists():
|
if upload_dir.exists():
|
||||||
for field, version in (('song_a_path', 'A'), ('song_b_path', 'B')):
|
for field, version in (('song_a_path', 'A'), ('song_b_path', 'B')):
|
||||||
path = req.get(field)
|
path = req.get(field)
|
||||||
|
|
@ -638,9 +640,14 @@ def revise(token):
|
||||||
archived = upload_dir / f"Rev{new_count}-{old.name}"
|
archived = upload_dir / f"Rev{new_count}-{old.name}"
|
||||||
try:
|
try:
|
||||||
old.rename(archived)
|
old.rename(archived)
|
||||||
|
if field == 'song_a_path':
|
||||||
|
new_a = str(archived)
|
||||||
|
else:
|
||||||
|
new_b = str(archived)
|
||||||
req[field] = str(archived)
|
req[field] = str(archived)
|
||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
|
log_revision(req['id'], new_count, note, old_a=old_a, old_b=old_b, new_a=new_a, new_b=new_b)
|
||||||
update_request(req['id'], revision_note=note, status='revisions_requested',
|
update_request(req['id'], revision_note=note, status='revisions_requested',
|
||||||
song_a_path=req.get('song_a_path'), song_b_path=req.get('song_b_path'),
|
song_a_path=req.get('song_a_path'), song_b_path=req.get('song_b_path'),
|
||||||
customer_approved='none', revision_count=new_count)
|
customer_approved='none', revision_count=new_count)
|
||||||
|
|
@ -963,7 +970,7 @@ def admin_request(rid):
|
||||||
|
|
||||||
return redirect(url_for('admin_request', rid=rid))
|
return redirect(url_for('admin_request', rid=rid))
|
||||||
|
|
||||||
return render_template('admin/request.html', req=req, statuses=STATUS_LABELS, file_exists=file_exists, basename=basename, extra_files=extra_files, callback_url=build_prompt_callback_url(rid))
|
return render_template('admin/request.html', req=req, statuses=STATUS_LABELS, file_exists=file_exists, basename=basename, extra_files=extra_files, callback_url=build_prompt_callback_url(rid), revision_history=list_revision_history(rid))
|
||||||
|
|
||||||
|
|
||||||
@app.route('/admin/request/<int:rid>/delete', methods=['POST'])
|
@app.route('/admin/request/<int:rid>/delete', methods=['POST'])
|
||||||
|
|
@ -1088,19 +1095,20 @@ def admin_settings():
|
||||||
action = request.form.get('action')
|
action = request.form.get('action')
|
||||||
|
|
||||||
if action == 'fix_db':
|
if action == 'fix_db':
|
||||||
# Attempt to add missing columns via ALTER TABLE.
|
# Attempt to create missing tables and add missing columns via ALTER TABLE.
|
||||||
if not health['ok'] and health['missing_columns']:
|
try:
|
||||||
try:
|
db = get_db()
|
||||||
db = get_db()
|
db.executescript(SCHEMA)
|
||||||
|
if not health['ok'] and health['missing_columns']:
|
||||||
for col in health['missing_columns']:
|
for col in health['missing_columns']:
|
||||||
# Default to TEXT columns; adequate for current schema.
|
# Default to TEXT columns; adequate for current schema.
|
||||||
db.execute(f'ALTER TABLE requests ADD COLUMN {col} TEXT')
|
db.execute(f'ALTER TABLE requests ADD COLUMN {col} TEXT')
|
||||||
db.commit()
|
flash(f'Created missing tables and added columns: {", ".join(health["missing_columns"])}. Please refresh the page.', 'success')
|
||||||
flash(f'Added missing columns: {", ".join(health["missing_columns"])}. Please refresh the page.', 'success')
|
else:
|
||||||
except Exception as e:
|
flash('Database schema is up to date.', 'success')
|
||||||
flash(f'Failed to fix database: {e}', 'error')
|
db.commit()
|
||||||
else:
|
except Exception as e:
|
||||||
flash('No columns need fixing.', 'success')
|
flash(f'Failed to fix database: {e}', 'error')
|
||||||
return redirect(url_for('admin_settings'))
|
return redirect(url_for('admin_settings'))
|
||||||
|
|
||||||
elif action == 'reset_system':
|
elif action == 'reset_system':
|
||||||
|
|
@ -1199,6 +1207,24 @@ def admin_settings():
|
||||||
flash('Database file not found.', 'error')
|
flash('Database file not found.', 'error')
|
||||||
return redirect(url_for('admin_settings'))
|
return redirect(url_for('admin_settings'))
|
||||||
|
|
||||||
|
elif action == 'download_uploads_zip':
|
||||||
|
# Zip all files under UPLOAD_FOLDER and send as a download.
|
||||||
|
import zipfile
|
||||||
|
zip_path = db_path.with_suffix('.uploads-' + str(int(time.time())) + '.zip')
|
||||||
|
try:
|
||||||
|
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zf:
|
||||||
|
if upload_root.exists():
|
||||||
|
for entry in upload_root.rglob('*'):
|
||||||
|
if entry.is_file():
|
||||||
|
zf.write(str(entry), str(entry.relative_to(upload_root)))
|
||||||
|
return send_file(str(zip_path), as_attachment=True, download_name='theme-song-booth-uploads.zip')
|
||||||
|
except Exception as e:
|
||||||
|
flash(f'Failed to create uploads ZIP: {e}', 'error')
|
||||||
|
return redirect(url_for('admin_settings'))
|
||||||
|
finally:
|
||||||
|
if zip_path.exists():
|
||||||
|
zip_path.unlink()
|
||||||
|
|
||||||
elif action == 'restore_db':
|
elif action == 'restore_db':
|
||||||
# Replace the current database file with an uploaded SQLite backup.
|
# Replace the current database file with an uploaded SQLite backup.
|
||||||
file_obj = request.files.get('db_backup')
|
file_obj = request.files.get('db_backup')
|
||||||
|
|
|
||||||
37
models.py
37
models.py
|
|
@ -55,6 +55,21 @@ CREATE TABLE IF NOT EXISTS requests (
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_requests_status ON requests(status);
|
CREATE INDEX IF NOT EXISTS idx_requests_status ON requests(status);
|
||||||
CREATE INDEX IF NOT EXISTS idx_requests_token ON requests(player_token);
|
CREATE INDEX IF NOT EXISTS idx_requests_token ON requests(player_token);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS revision_history (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
request_id INTEGER NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
revision_count INTEGER NOT NULL,
|
||||||
|
note TEXT,
|
||||||
|
old_song_a_path TEXT,
|
||||||
|
old_song_b_path TEXT,
|
||||||
|
new_song_a_path TEXT,
|
||||||
|
new_song_b_path TEXT,
|
||||||
|
FOREIGN KEY (request_id) REFERENCES requests(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_revision_history_request ON revision_history(request_id);
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -168,6 +183,28 @@ def delete_request(request_id):
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def log_revision(request_id, revision_count, note, old_a=None, old_b=None, new_a=None, new_b=None):
|
||||||
|
"""Record a revision event in the revision_history table."""
|
||||||
|
db = get_db()
|
||||||
|
db.execute(
|
||||||
|
"""INSERT INTO revision_history
|
||||||
|
(request_id, revision_count, note, old_song_a_path, old_song_b_path, new_song_a_path, new_song_b_path)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?)""",
|
||||||
|
(request_id, revision_count, note, old_a, old_b, new_a, new_b)
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def list_revision_history(request_id):
|
||||||
|
"""Return all revision history rows for a request, oldest first."""
|
||||||
|
db = get_db()
|
||||||
|
rows = db.execute(
|
||||||
|
'SELECT * FROM revision_history WHERE request_id = ? ORDER BY created_at ASC',
|
||||||
|
(request_id,)
|
||||||
|
).fetchall()
|
||||||
|
return [dict(r) for r in rows]
|
||||||
|
|
||||||
|
|
||||||
def reset_all_requests():
|
def reset_all_requests():
|
||||||
"""Delete every row in the requests table and reset id auto-increment."""
|
"""Delete every row in the requests table and reset id auto-increment."""
|
||||||
db = get_db()
|
db = get_db()
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,23 @@
|
||||||
margin-left:1.8rem;
|
margin-left:1.8rem;
|
||||||
}
|
}
|
||||||
.approved-box{font-size:1.2rem;font-weight:bold;color:#fbbf24;}
|
.approved-box{font-size:1.2rem;font-weight:bold;color:#fbbf24;}
|
||||||
|
.history-list{
|
||||||
|
margin:0;
|
||||||
|
padding:0;
|
||||||
|
list-style:none;
|
||||||
|
}
|
||||||
|
.history-list li{
|
||||||
|
border-bottom:1px solid #374151;
|
||||||
|
padding:.6rem 0;
|
||||||
|
}
|
||||||
|
.history-list li:last-child{border-bottom:none;}
|
||||||
|
.history-list .meta{
|
||||||
|
font-size:.8rem;
|
||||||
|
color:#9ca3af;
|
||||||
|
}
|
||||||
|
.history-list .note{
|
||||||
|
margin:.3rem 0 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* Two-column layout */
|
/* Two-column layout */
|
||||||
.two-col{
|
.two-col{
|
||||||
|
|
@ -291,7 +308,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Payment & Delivery -->
|
<!-- Payment & Delivery -->
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h2>4. Payment & Delivery</h2>
|
<h2>4. Payment & Delivery</h2>
|
||||||
<p>
|
<p>
|
||||||
|
|
@ -344,13 +361,36 @@
|
||||||
<input type="text" id="square_payment_ref" name="square_payment_ref" placeholder="e.g. sq0idp-... or receipt number"
|
<input type="text" id="square_payment_ref" name="square_payment_ref" placeholder="e.g. sq0idp-... or receipt number"
|
||||||
{% if req.customer_approved == 'none' %}disabled{% endif %}>
|
{% if req.customer_approved == 'none' %}disabled{% endif %}>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button type="submit" class="success" {% if req.customer_approved == 'none' %}disabled{% endif %}>Mark Paid & Deliver</button>
|
<button type="submit" class="success" {% if req.customer_approved == 'none' %}disabled{% endif %}>Mark Paid & Deliver</button>
|
||||||
</div>
|
</div>
|
||||||
{% if req.customer_approved == 'none' %}
|
{% if req.customer_approved == 'none' %}
|
||||||
<p style="margin-top:.5rem;color:#f87171;font-weight:600">⚠️ Customer must approve a version before you can mark paid or deliver.</p>
|
<p style="margin-top:.5rem;color:#f87171;font-weight:600">⚠️ Customer must approve a version before you can mark paid or deliver.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Revision History -->
|
||||||
|
<div class="section">
|
||||||
|
<h2>Revision History</h2>
|
||||||
|
{% if revision_history %}
|
||||||
|
<ul class="history-list">
|
||||||
|
{% for h in revision_history %}
|
||||||
|
<li>
|
||||||
|
<div class="meta">Revision #{{ h.revision_count }} — {{ h.created_at }}
|
||||||
|
{% if h.old_song_a_path or h.old_song_b_path %}
|
||||||
|
<br>Archived:
|
||||||
|
{% if h.old_song_a_path %}<br>A: {{ basename(h.old_song_a_path) }}{% endif %}
|
||||||
|
{% if h.old_song_b_path %}<br>B: {{ basename(h.old_song_b_path) }}{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<p class="note">{{ h.note or 'No note provided.' }}</p>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<p class="copy-hint">No revisions recorded yet.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -364,6 +364,16 @@
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Uploads backup -->
|
||||||
|
<div class="section">
|
||||||
|
<h2>Music Files Backup</h2>
|
||||||
|
<p class="copy-hint">Download all uploaded MP3 and archived revision files as a single ZIP.</p>
|
||||||
|
<form method="POST">
|
||||||
|
<input type="hidden" name="action" value="download_uploads_zip">
|
||||||
|
<button type="submit">Download Music Files ZIP</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- System reset spans both columns -->
|
<!-- System reset spans both columns -->
|
||||||
<div class="danger-zone">
|
<div class="danger-zone">
|
||||||
<h2>⚠️ Reset System</h2>
|
<h2>⚠️ Reset System</h2>
|
||||||
|
|
|
||||||
Reference in a new issue