feat: editable customer email in admin + email validation on public form
This commit is contained in:
parent
4af9322c9d
commit
bf9f9e09fd
4 changed files with 48 additions and 13 deletions
|
|
@ -6,7 +6,7 @@ A Flask web app for a convention booth where visitors request a custom AI-genera
|
||||||
|
|
||||||
### Customer-facing
|
### Customer-facing
|
||||||
|
|
||||||
- **Request form** (`/request`) — visitors enter name, email, hobbies, notable facts, preferred style/genre, vocal gender preference, and extra requests. A branded banner image is shown.
|
- **Request form** (`/request`) — visitors enter name, email, hobbies, notable facts, preferred style/genre, vocal gender preference, and extra requests. A branded banner image is shown. The email field is validated to reduce delivery problems.
|
||||||
- **Confirmation page** (`/thanks/<id>`) — shows the request number after submission.
|
- **Confirmation page** (`/thanks/<id>`) — shows the request number after submission.
|
||||||
- **Private player page** (`/play/<token>`) — customer receives an email with a unique link. They can stream Version A and Version B, pick one (or both), or request a limited number of revisions.
|
- **Private player page** (`/play/<token>`) — customer receives an email with a unique link. They can stream Version A and Version B, pick one (or both), or request a limited number of revisions.
|
||||||
- **Revision workflow** — when a customer asks for changes, the current MP3s are archived and the operator sees the request as "Revisions Requested" in the dashboard.
|
- **Revision workflow** — when a customer asks for changes, the current MP3s are archived and the operator sees the request as "Revisions Requested" in the dashboard.
|
||||||
|
|
@ -18,6 +18,7 @@ A Flask web app for a convention booth where visitors request a custom AI-genera
|
||||||
- **Dashboard queue** (`/admin`) — filter by status (All, Pending, Needs Upload, Awaiting Payment, Delivered) and auto-refresh at a configurable interval.
|
- **Dashboard queue** (`/admin`) — filter by status (All, Pending, Needs Upload, Awaiting Payment, Delivered) and auto-refresh at a configurable interval.
|
||||||
- **Per-request detail page** (`/admin/request/<id>`):
|
- **Per-request detail page** (`/admin/request/<id>`):
|
||||||
- Generate and save a Suno prompt from customer info.
|
- Generate and save a Suno prompt from customer info.
|
||||||
|
- Edit the customer's email address if they mistyped it.
|
||||||
- Upload Version A and Version B MP3s (with automatic ID3 metadata tagging).
|
- Upload Version A and Version B MP3s (with automatic ID3 metadata tagging).
|
||||||
- Send a preview email with a private player link.
|
- Send a preview email with a private player link.
|
||||||
- Mark paid, enter a Square payment reference, and deliver selected MP3 attachments.
|
- Mark paid, enter a Square payment reference, and deliver selected MP3 attachments.
|
||||||
|
|
@ -62,7 +63,7 @@ pending → prompt_ready → songs_uploaded → awaiting_payment → paid → de
|
||||||
| `templates/player.html` | Customer audio player, approval, and revision form. |
|
| `templates/player.html` | Customer audio player, approval, and revision form. |
|
||||||
| `templates/admin/login.html` | Admin login page. |
|
| `templates/admin/login.html` | Admin login page. |
|
||||||
| `templates/admin/dashboard.html` | Operator queue with filters and auto-refresh. |
|
| `templates/admin/dashboard.html` | Operator queue with filters and auto-refresh. |
|
||||||
| `templates/admin/request.html` | Single-request detail / prompt / upload / delivery page. |
|
| `templates/admin/request.html` | Single-request detail / prompt / upload / delivery page. Customer email is editable here. |
|
||||||
| `templates/admin/settings.html` | Maintenance, settings, backup/restore, and reset page. |
|
| `templates/admin/settings.html` | Maintenance, settings, backup/restore, and reset page. |
|
||||||
| `static/Trollgorithm_booth.jpg` | Banner image on the request page. |
|
| `static/Trollgorithm_booth.jpg` | Banner image on the request page. |
|
||||||
| `static/DM-Logo_email.png` | Inline Dionysis Media logo attached to emails. |
|
| `static/DM-Logo_email.png` | Inline Dionysis Media logo attached to emails. |
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ pending → prompt_ready → songs_uploaded → awaiting_payment → paid → de
|
||||||
|
|
||||||
1. Customer fills `/request`.
|
1. Customer fills `/request`.
|
||||||
2. Open `/admin`, click request row (or filter by status).
|
2. Open `/admin`, click request row (or filter by status).
|
||||||
3. On `/admin/request/<id>`, click **Copy customer info for Hermes**, paste result to Hermes.
|
3. On `/admin/request/<id>`, fix the customer's email if needed, then click **Copy customer info for Hermes**, paste result to Hermes.
|
||||||
4. Paste Hermes response (Title/Style/Lyrics format) into the fields and click **Save Prompt**.
|
4. Paste Hermes response (Title/Style/Lyrics format) into the fields and click **Save Prompt**.
|
||||||
5. Copy Style/Lyrics into Suno Custom Mode, generate two versions.
|
5. Copy Style/Lyrics into Suno Custom Mode, generate two versions.
|
||||||
6. Upload Version A and B MP3s.
|
6. Upload Version A and B MP3s.
|
||||||
|
|
|
||||||
40
app.py
40
app.py
|
|
@ -30,6 +30,7 @@ Admin routes:
|
||||||
|
|
||||||
# Standard library imports
|
# Standard library imports
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import smtplib
|
import smtplib
|
||||||
import ssl
|
import ssl
|
||||||
|
|
@ -38,7 +39,6 @@ from email.message import EmailMessage
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import time
|
|
||||||
|
|
||||||
from cryptography.fernet import Fernet
|
from cryptography.fernet import Fernet
|
||||||
from cryptography.hazmat.primitives import hashes
|
from cryptography.hazmat.primitives import hashes
|
||||||
|
|
@ -318,14 +318,18 @@ def request_form():
|
||||||
"""
|
"""
|
||||||
Public request form.
|
Public request form.
|
||||||
GET -> shows the form with the banner image.
|
GET -> shows the form with the banner image.
|
||||||
POST -> creates a database record, sends a confirmation email,
|
POST -> validates the email, creates a database record, sends a
|
||||||
and redirects to the thanks page.
|
confirmation email, and redirects to the thanks page.
|
||||||
Rate limited to 5 submissions per minute per IP.
|
Rate limited to 5 submissions per minute per IP.
|
||||||
"""
|
"""
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
|
email = request.form.get('email', '').strip().lower()
|
||||||
|
if not is_valid_email(email):
|
||||||
|
flash('Please enter a valid email address.', 'error')
|
||||||
|
return render_template('request.html'), 400
|
||||||
rid = create_request(
|
rid = create_request(
|
||||||
name=request.form.get('name', '').strip(),
|
name=request.form.get('name', '').strip(),
|
||||||
email=request.form.get('email', '').strip(),
|
email=email,
|
||||||
hobbies=request.form.get('hobbies', '').strip(),
|
hobbies=request.form.get('hobbies', '').strip(),
|
||||||
notable_facts=request.form.get('notable_facts', '').strip(),
|
notable_facts=request.form.get('notable_facts', '').strip(),
|
||||||
style_genre=request.form.get('style_genre', '').strip(),
|
style_genre=request.form.get('style_genre', '').strip(),
|
||||||
|
|
@ -491,6 +495,15 @@ def admin_login():
|
||||||
return render_template('admin/login.html')
|
return render_template('admin/login.html')
|
||||||
|
|
||||||
|
|
||||||
|
def is_valid_email(email):
|
||||||
|
"""Return True if the given string looks like a valid email address."""
|
||||||
|
if not email:
|
||||||
|
return False
|
||||||
|
# Very loose regex: local@domain.tld, no spaces, with a real TLD part.
|
||||||
|
pattern = r"^[^\s@]+@[^\s@]+\.[^\s@]+$"
|
||||||
|
return re.match(pattern, email) is not None
|
||||||
|
|
||||||
|
|
||||||
@app.route('/admin/logout')
|
@app.route('/admin/logout')
|
||||||
def admin_logout():
|
def admin_logout():
|
||||||
"""Clear the admin session."""
|
"""Clear the admin session."""
|
||||||
|
|
@ -517,9 +530,11 @@ def admin_dashboard():
|
||||||
def admin_request(rid):
|
def admin_request(rid):
|
||||||
"""
|
"""
|
||||||
Detail/edit page for a single request.
|
Detail/edit page for a single request.
|
||||||
GET -> render customer info, prompt, upload status, email status, and delivery forms.
|
GET -> render customer info (email editable), prompt, upload status,
|
||||||
POST -> handle one of four actions:
|
email status, and delivery forms.
|
||||||
save_prompt, upload_songs, notify_customer, mark_paid_deliver
|
POST -> handle one of five actions:
|
||||||
|
update_customer_email, save_prompt, upload_songs,
|
||||||
|
notify_customer, mark_paid_deliver
|
||||||
Uploaded MP3s are tagged with metadata defaults from /admin/settings.
|
Uploaded MP3s are tagged with metadata defaults from /admin/settings.
|
||||||
"""
|
"""
|
||||||
redir = require_admin()
|
redir = require_admin()
|
||||||
|
|
@ -549,7 +564,16 @@ def admin_request(rid):
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
action = request.form.get('action')
|
action = request.form.get('action')
|
||||||
|
|
||||||
if action == 'save_prompt':
|
if action == 'update_customer_email':
|
||||||
|
new_email = request.form.get('email', '').strip().lower()
|
||||||
|
if not is_valid_email(new_email):
|
||||||
|
flash('Please enter a valid email address.', 'error')
|
||||||
|
return redirect(url_for('admin_request', rid=rid))
|
||||||
|
update_request(rid, email=new_email)
|
||||||
|
flash('Customer email updated.', 'success')
|
||||||
|
return redirect(url_for('admin_request', rid=rid))
|
||||||
|
|
||||||
|
elif action == 'save_prompt':
|
||||||
# Store the generated title/style/lyrics and mark prompt ready.
|
# Store the generated title/style/lyrics and mark prompt ready.
|
||||||
update_request(rid,
|
update_request(rid,
|
||||||
suno_title=request.form.get('suno_title', '').strip(),
|
suno_title=request.form.get('suno_title', '').strip(),
|
||||||
|
|
|
||||||
|
|
@ -133,8 +133,18 @@
|
||||||
<!-- Section 1: Customer info summary -->
|
<!-- Section 1: Customer info summary -->
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h2>Customer Info</h2>
|
<h2>Customer Info</h2>
|
||||||
<div class="info-grid">
|
<p class="copy-hint">Operators can correct the customer's email address here.</p>
|
||||||
<div><strong>Email:</strong> {{ req.email }}</div>
|
<form method="POST">
|
||||||
|
<input type="hidden" name="action" value="update_customer_email">
|
||||||
|
<label for="customer_email">Email address</label>
|
||||||
|
<input type="email" id="customer_email" name="email" value="{{ req.email }}" required>
|
||||||
|
<div class="actions">
|
||||||
|
<button type="submit" class="secondary">Update Email</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="info-grid" style="margin-top:1rem">
|
||||||
|
<div><strong>Request #:</strong> {{ req.id }}</div>
|
||||||
<div><strong>Status:</strong> {{ statuses[req.status] }}</div>
|
<div><strong>Status:</strong> {{ statuses[req.status] }}</div>
|
||||||
</div>
|
</div>
|
||||||
<p><strong>Hobbies:</strong><br>{{ req.hobbies or '-' }}</p>
|
<p><strong>Hobbies:</strong><br>{{ req.hobbies or '-' }}</p>
|
||||||
|
|
|
||||||
Reference in a new issue