Add configurable max revisions limit with customer counter and operator override
This commit is contained in:
parent
5b6012aa3d
commit
506d07179a
5 changed files with 99 additions and 22 deletions
100
app.py
100
app.py
|
|
@ -33,6 +33,8 @@ import ssl
|
||||||
from email.message import EmailMessage
|
from email.message import EmailMessage
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
# Flask and related imports
|
# Flask and related imports
|
||||||
from flask import Flask, request, render_template, redirect, url_for, flash, session, send_from_directory, abort, current_app
|
from flask import Flask, request, render_template, redirect, url_for, flash, session, send_from_directory, abort, current_app
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
|
|
@ -202,7 +204,19 @@ def play(token):
|
||||||
req = get_request_by_token(token)
|
req = get_request_by_token(token)
|
||||||
if not req:
|
if not req:
|
||||||
abort(404)
|
abort(404)
|
||||||
return render_template('player.html', req=req)
|
|
||||||
|
# Load runtime max revisions setting.
|
||||||
|
cfg_path = Path(current_app.config['UPLOAD_FOLDER']).parent / 'booth_settings.json'
|
||||||
|
runtime_settings = {}
|
||||||
|
if cfg_path.exists():
|
||||||
|
try:
|
||||||
|
runtime_settings = json.loads(cfg_path.read_text())
|
||||||
|
except (json.JSONDecodeError, OSError):
|
||||||
|
runtime_settings = {}
|
||||||
|
max_revisions = runtime_settings.get('max_revisions', current_app.config['MAX_REVISIONS'])
|
||||||
|
revisions_left = max(0, max_revisions - (req.get('revision_count') or 0))
|
||||||
|
|
||||||
|
return render_template('player.html', req=req, revisions_left=revisions_left)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/play/<token>/approve', methods=['POST'])
|
@app.route('/play/<token>/approve', methods=['POST'])
|
||||||
|
|
@ -236,29 +250,42 @@ def revise(token):
|
||||||
so the operator sees it in the dashboard queue.
|
so the operator sees it in the dashboard queue.
|
||||||
"""
|
"""
|
||||||
note = request.form.get('revision_note', '').strip()
|
note = request.form.get('revision_note', '').strip()
|
||||||
# Increment revision counter and archive current files before new versions are uploaded.
|
|
||||||
req = get_request_by_token(token)
|
req = get_request_by_token(token)
|
||||||
if req:
|
if not req:
|
||||||
new_count = (req.get('revision_count') or 0) + 1
|
|
||||||
upload_dir = Path(current_app.config['UPLOAD_FOLDER']) / str(req['id'])
|
|
||||||
if upload_dir.exists():
|
|
||||||
for field, version in (('song_a_path', 'A'), ('song_b_path', 'B')):
|
|
||||||
path = req.get(field)
|
|
||||||
if path and Path(path).exists():
|
|
||||||
old = Path(path)
|
|
||||||
archived = upload_dir / f"Rev{new_count}-{old.name}"
|
|
||||||
try:
|
|
||||||
old.rename(archived)
|
|
||||||
# Keep path pointing to the archived file; operator will upload new files.
|
|
||||||
req[field] = str(archived)
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
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)
|
|
||||||
else:
|
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
|
# Enforce max revisions limit for customer-submitted revisions.
|
||||||
|
cfg_path = Path(current_app.config['UPLOAD_FOLDER']).parent / 'booth_settings.json'
|
||||||
|
runtime_settings = {}
|
||||||
|
if cfg_path.exists():
|
||||||
|
try:
|
||||||
|
runtime_settings = json.loads(cfg_path.read_text())
|
||||||
|
except (json.JSONDecodeError, OSError):
|
||||||
|
runtime_settings = {}
|
||||||
|
max_revisions = runtime_settings.get('max_revisions', current_app.config['MAX_REVISIONS'])
|
||||||
|
current_count = req.get('revision_count') or 0
|
||||||
|
if current_count >= max_revisions:
|
||||||
|
flash('Revision limit reached. Please speak to the booth operator if you need further changes.', 'error')
|
||||||
|
return redirect(url_for('play', token=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'])
|
||||||
|
if upload_dir.exists():
|
||||||
|
for field, version in (('song_a_path', 'A'), ('song_b_path', 'B')):
|
||||||
|
path = req.get(field)
|
||||||
|
if path and Path(path).exists():
|
||||||
|
old = Path(path)
|
||||||
|
archived = upload_dir / f"Rev{new_count}-{old.name}"
|
||||||
|
try:
|
||||||
|
old.rename(archived)
|
||||||
|
req[field] = str(archived)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
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)
|
||||||
|
|
||||||
# NOTE: No operator email is sent; the dashboard is the single queue.
|
# NOTE: No operator email is sent; the dashboard is the single queue.
|
||||||
flash('Your feedback has been saved. We will regenerate and update you.', 'success')
|
flash('Your feedback has been saved. We will regenerate and update you.', 'success')
|
||||||
return redirect(url_for('play', token=token))
|
return redirect(url_for('play', token=token))
|
||||||
|
|
@ -479,6 +506,16 @@ def admin_settings():
|
||||||
for req in all_requests:
|
for req in all_requests:
|
||||||
status_counts[req['status']] = status_counts.get(req['status'], 0) + 1
|
status_counts[req['status']] = status_counts.get(req['status'], 0) + 1
|
||||||
|
|
||||||
|
# Load persistent runtime settings (max_revisions overrides env var if set).
|
||||||
|
cfg_path = Path(current_app.config['UPLOAD_FOLDER']).parent / 'booth_settings.json'
|
||||||
|
runtime_settings = {}
|
||||||
|
if cfg_path.exists():
|
||||||
|
try:
|
||||||
|
runtime_settings = json.loads(cfg_path.read_text())
|
||||||
|
except (json.JSONDecodeError, OSError):
|
||||||
|
runtime_settings = {}
|
||||||
|
current_max_revisions = runtime_settings.get('max_revisions', current_app.config['MAX_REVISIONS'])
|
||||||
|
|
||||||
# Compute upload folder stats.
|
# Compute upload folder stats.
|
||||||
total_upload_size = 0
|
total_upload_size = 0
|
||||||
upload_file_count = 0
|
upload_file_count = 0
|
||||||
|
|
@ -555,6 +592,24 @@ def admin_settings():
|
||||||
flash('System reset complete. All orders and files have been cleared.', 'success')
|
flash('System reset complete. All orders and files have been cleared.', 'success')
|
||||||
return redirect(url_for('admin_settings'))
|
return redirect(url_for('admin_settings'))
|
||||||
|
|
||||||
|
elif action == 'save_max_revisions':
|
||||||
|
# Update the MAX_REVISIONS config from the settings form.
|
||||||
|
try:
|
||||||
|
val = int(request.form.get('max_revisions', '2').strip())
|
||||||
|
if val < 0:
|
||||||
|
raise ValueError
|
||||||
|
# Store in a simple config file so it persists across restarts.
|
||||||
|
cfg_path = Path(current_app.config['UPLOAD_FOLDER']).parent / 'booth_settings.json'
|
||||||
|
data = {}
|
||||||
|
if cfg_path.exists():
|
||||||
|
data = json.loads(cfg_path.read_text())
|
||||||
|
data['max_revisions'] = val
|
||||||
|
cfg_path.write_text(json.dumps(data))
|
||||||
|
flash(f'Maximum revisions set to {val}.', 'success')
|
||||||
|
except ValueError:
|
||||||
|
flash('Invalid revision limit. Please enter a non-negative number.', 'error')
|
||||||
|
return redirect(url_for('admin_settings'))
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
'admin/settings.html',
|
'admin/settings.html',
|
||||||
health=health,
|
health=health,
|
||||||
|
|
@ -566,7 +621,8 @@ def admin_settings():
|
||||||
upload_dir_count=request_dir_count,
|
upload_dir_count=request_dir_count,
|
||||||
upload_size=format_bytes(total_upload_size),
|
upload_size=format_bytes(total_upload_size),
|
||||||
db_path=str(db_path),
|
db_path=str(db_path),
|
||||||
upload_path=str(upload_root)
|
upload_path=str(upload_root),
|
||||||
|
current_max_revisions=current_max_revisions,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
1
booth_settings.json
Normal file
1
booth_settings.json
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"max_revisions": 1}
|
||||||
|
|
@ -47,6 +47,9 @@ class Config:
|
||||||
# Optional operator alert email. Currently unused because the dashboard is the queue.
|
# Optional operator alert email. Currently unused because the dashboard is the queue.
|
||||||
ADMIN_ALERT_EMAIL = os.environ.get('ADMIN_ALERT_EMAIL', '')
|
ADMIN_ALERT_EMAIL = os.environ.get('ADMIN_ALERT_EMAIL', '')
|
||||||
|
|
||||||
|
# Maximum number of revision rounds a customer is allowed to request automatically.
|
||||||
|
MAX_REVISIONS = int(os.environ.get('MAX_REVISIONS', '2'))
|
||||||
|
|
||||||
# Public HTTPS URL used in customer emails and QR codes.
|
# Public HTTPS URL used in customer emails and QR codes.
|
||||||
PUBLIC_BASE_URL = os.environ.get('PUBLIC_BASE_URL', 'http://127.0.0.1:5000')
|
PUBLIC_BASE_URL = os.environ.get('PUBLIC_BASE_URL', 'http://127.0.0.1:5000')
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -194,6 +194,18 @@
|
||||||
<p><strong>Uploads:</strong><br><span class="path">{{ upload_path }}</span></p>
|
<p><strong>Uploads:</strong><br><span class="path">{{ upload_path }}</span></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Settings controls -->
|
||||||
|
<div class="section">
|
||||||
|
<h2>Booth Settings</h2>
|
||||||
|
<form method="POST">
|
||||||
|
<input type="hidden" name="action" value="save_max_revisions">
|
||||||
|
<label for="max_revisions">Maximum customer revisions allowed</label>
|
||||||
|
<input type="number" id="max_revisions" name="max_revisions" min="0" value="{{ current_max_revisions }}">
|
||||||
|
<p class="copy-hint">Set to 0 to disable customer-submitted revisions. Operators can still upload new versions manually.</p>
|
||||||
|
<button type="submit">Save Revision Limit</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- System reset -->
|
<!-- System reset -->
|
||||||
<div class="danger-zone">
|
<div class="danger-zone">
|
||||||
<h2>⚠️ Reset System</h2>
|
<h2>⚠️ Reset System</h2>
|
||||||
|
|
|
||||||
|
|
@ -130,14 +130,19 @@
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
{% if revisions_left > 0 %}
|
||||||
<!-- Revision form: customer asks for changes -->
|
<!-- Revision form: customer asks for changes -->
|
||||||
<form method="POST" action="{{ url_for('revise', token=req.player_token) }}">
|
<form method="POST" action="{{ url_for('revise', token=req.player_token) }}">
|
||||||
|
<p style="color:#93c5fd;font-weight:600">Revisions remaining: {{ revisions_left }}</p>
|
||||||
<label for="revision_note">Or ask for changes:</label>
|
<label for="revision_note">Or ask for changes:</label>
|
||||||
<textarea id="revision_note" name="revision_note" placeholder="e.g. make the chorus louder, swap a lyric..."></textarea>
|
<textarea id="revision_note" name="revision_note" placeholder="e.g. make the chorus louder, swap a lyric..."></textarea>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button type="submit" class="revision">Request Changes</button>
|
<button type="submit" class="revision">Request Changes</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<p style="margin-top:1rem;color:#f87171;font-weight:600">No revisions remaining. Please speak to the booth operator if you need further changes.</p>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Reference in a new issue