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
|
||||
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 SCHEMA, get_requests_by_email
|
||||
from models import SCHEMA, get_requests_by_email, log_revision, list_revision_history
|
||||
|
||||
# Audio metadata import
|
||||
from mutagen.mp3 import MP3
|
||||
|
|
@ -630,6 +630,8 @@ def revise(token):
|
|||
# Increment revision counter and archive current files before new versions are uploaded.
|
||||
new_count = current_count + 1
|
||||
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():
|
||||
for field, version in (('song_a_path', 'A'), ('song_b_path', 'B')):
|
||||
path = req.get(field)
|
||||
|
|
@ -638,9 +640,14 @@ def revise(token):
|
|||
archived = upload_dir / f"Rev{new_count}-{old.name}"
|
||||
try:
|
||||
old.rename(archived)
|
||||
if field == 'song_a_path':
|
||||
new_a = str(archived)
|
||||
else:
|
||||
new_b = str(archived)
|
||||
req[field] = str(archived)
|
||||
except OSError:
|
||||
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',
|
||||
song_a_path=req.get('song_a_path'), song_b_path=req.get('song_b_path'),
|
||||
customer_approved='none', revision_count=new_count)
|
||||
|
|
@ -963,7 +970,7 @@ def admin_request(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'])
|
||||
|
|
@ -1088,19 +1095,20 @@ def admin_settings():
|
|||
action = request.form.get('action')
|
||||
|
||||
if action == 'fix_db':
|
||||
# Attempt to add missing columns via ALTER TABLE.
|
||||
if not health['ok'] and health['missing_columns']:
|
||||
try:
|
||||
db = get_db()
|
||||
# Attempt to create missing tables and add missing columns via ALTER TABLE.
|
||||
try:
|
||||
db = get_db()
|
||||
db.executescript(SCHEMA)
|
||||
if not health['ok'] and health['missing_columns']:
|
||||
for col in health['missing_columns']:
|
||||
# Default to TEXT columns; adequate for current schema.
|
||||
db.execute(f'ALTER TABLE requests ADD COLUMN {col} TEXT')
|
||||
db.commit()
|
||||
flash(f'Added missing columns: {", ".join(health["missing_columns"])}. Please refresh the page.', 'success')
|
||||
except Exception as e:
|
||||
flash(f'Failed to fix database: {e}', 'error')
|
||||
else:
|
||||
flash('No columns need fixing.', 'success')
|
||||
flash(f'Created missing tables and added columns: {", ".join(health["missing_columns"])}. Please refresh the page.', 'success')
|
||||
else:
|
||||
flash('Database schema is up to date.', 'success')
|
||||
db.commit()
|
||||
except Exception as e:
|
||||
flash(f'Failed to fix database: {e}', 'error')
|
||||
return redirect(url_for('admin_settings'))
|
||||
|
||||
elif action == 'reset_system':
|
||||
|
|
@ -1199,6 +1207,24 @@ def admin_settings():
|
|||
flash('Database file not found.', 'error')
|
||||
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':
|
||||
# Replace the current database file with an uploaded SQLite 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_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()
|
||||
|
||||
|
||||
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():
|
||||
"""Delete every row in the requests table and reset id auto-increment."""
|
||||
db = get_db()
|
||||
|
|
|
|||
|
|
@ -110,6 +110,23 @@
|
|||
margin-left:1.8rem;
|
||||
}
|
||||
.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-col{
|
||||
|
|
@ -291,7 +308,7 @@
|
|||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Payment & Delivery -->
|
||||
<!-- Payment & Delivery -->
|
||||
<div class="section">
|
||||
<h2>4. Payment & Delivery</h2>
|
||||
<p>
|
||||
|
|
@ -344,13 +361,36 @@
|
|||
<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 %}>
|
||||
<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>
|
||||
{% 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>
|
||||
{% endif %}
|
||||
</form>
|
||||
</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>
|
||||
|
||||
|
|
|
|||
|
|
@ -364,6 +364,16 @@
|
|||
</form>
|
||||
</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 -->
|
||||
<div class="danger-zone">
|
||||
<h2>⚠️ Reset System</h2>
|
||||
|
|
|
|||
Reference in a new issue