mirror of
https://github.com/tmdinosaurcenter/kiosk-guestbook.git
synced 2026-06-04 00:28:21 -06:00
feat: add paginated admin interface for viewing and deleting entries
This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
from flask import Flask, render_template, request, redirect, url_for, jsonify, abort
|
from flask import Flask, render_template, request, redirect, url_for, jsonify, abort, Response
|
||||||
from flask_limiter import Limiter
|
from flask_limiter import Limiter
|
||||||
from flask_limiter.util import get_remote_address
|
from flask_limiter.util import get_remote_address
|
||||||
from email_validator import validate_email, EmailNotValidError
|
from email_validator import validate_email, EmailNotValidError
|
||||||
|
from functools import wraps
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
@@ -177,9 +178,58 @@ def index():
|
|||||||
logger.info("Rendering index with %d guests.", len(guests))
|
logger.info("Rendering index with %d guests.", len(guests))
|
||||||
return render_template('index.html', error=error, guests=guests)
|
return render_template('index.html', error=error, guests=guests)
|
||||||
|
|
||||||
# TODO: Add an admin interface for reviewing and deleting guest entries.
|
def require_admin_auth(f):
|
||||||
# Should include: paginated entry list, per-entry delete, and authentication
|
@wraps(f)
|
||||||
# (e.g. HTTP Basic Auth or a simple token) to restrict access.
|
def decorated(*args, **kwargs):
|
||||||
|
auth = request.authorization
|
||||||
|
admin_user = os.environ.get('ADMIN_USER', '')
|
||||||
|
admin_password = os.environ.get('ADMIN_PASSWORD', '')
|
||||||
|
if not auth or auth.username != admin_user or auth.password != admin_password:
|
||||||
|
return Response(
|
||||||
|
'Authentication required.',
|
||||||
|
401,
|
||||||
|
{'WWW-Authenticate': 'Basic realm="Admin"'}
|
||||||
|
)
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
return decorated
|
||||||
|
|
||||||
|
@app.route('/admin')
|
||||||
|
@require_admin_auth
|
||||||
|
def admin():
|
||||||
|
page = request.args.get('page', 1, type=int)
|
||||||
|
per_page = 25
|
||||||
|
offset = (page - 1) * per_page
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(DATABASE)
|
||||||
|
c = conn.cursor()
|
||||||
|
total = c.execute('SELECT COUNT(*) FROM guests').fetchone()[0]
|
||||||
|
c.execute('''
|
||||||
|
SELECT id, first_name, last_name, email, location, comment, newsletter_opt_in, timestamp
|
||||||
|
FROM guests ORDER BY id DESC LIMIT ? OFFSET ?
|
||||||
|
''', (per_page, offset))
|
||||||
|
guests = c.fetchall()
|
||||||
|
conn.close()
|
||||||
|
except sqlite3.Error as e:
|
||||||
|
logger.error("Database error in admin: %s", e)
|
||||||
|
guests = []
|
||||||
|
total = 0
|
||||||
|
total_pages = (total + per_page - 1) // per_page
|
||||||
|
return render_template('admin.html', guests=guests, page=page, total_pages=total_pages, total=total)
|
||||||
|
|
||||||
|
@app.route('/admin/delete/<int:entry_id>', methods=['POST'])
|
||||||
|
@require_admin_auth
|
||||||
|
def admin_delete(entry_id):
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(DATABASE)
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute('DELETE FROM guests WHERE id = ?', (entry_id,))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
logger.info("Admin deleted guest entry id=%d", entry_id)
|
||||||
|
except sqlite3.Error as e:
|
||||||
|
logger.error("Database error deleting guest %d: %s", entry_id, e)
|
||||||
|
return redirect(url_for('admin', page=request.args.get('page', 1)))
|
||||||
|
|
||||||
@app.route('/api/guests', methods=['GET'])
|
@app.route('/api/guests', methods=['GET'])
|
||||||
def api_guests():
|
def api_guests():
|
||||||
api_key = request.headers.get('X-API-Key')
|
api_key = request.headers.get('X-API-Key')
|
||||||
|
|||||||
@@ -10,3 +10,5 @@ PID=1000
|
|||||||
GID=1000
|
GID=1000
|
||||||
SITE_TITLE="The Montana Dinosaur Center Visitor Log"
|
SITE_TITLE="The Montana Dinosaur Center Visitor Log"
|
||||||
LOGO_URL="/static/images/logo.png"
|
LOGO_URL="/static/images/logo.png"
|
||||||
|
ADMIN_USER=admin
|
||||||
|
ADMIN_PASSWORD=changeme
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Guestbook Admin</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" />
|
||||||
|
</head>
|
||||||
|
<body class="bg-light">
|
||||||
|
<div class="container py-4">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h1 class="h3 mb-0">Guestbook Admin</h1>
|
||||||
|
<span class="text-muted">{{ total }} total entries</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-bordered table-hover bg-white">
|
||||||
|
<thead class="table-dark">
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>Location</th>
|
||||||
|
<th>Comment</th>
|
||||||
|
<th>Newsletter</th>
|
||||||
|
<th>Timestamp</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for g in guests %}
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">{{ g[0] }}</td>
|
||||||
|
<td>{{ g[1] }} {{ g[2] }}</td>
|
||||||
|
<td>{{ g[3] or '—' }}</td>
|
||||||
|
<td>{{ g[4] }}</td>
|
||||||
|
<td>{{ g[5] or '—' }}</td>
|
||||||
|
<td>{{ 'Yes' if g[6] else 'No' }}</td>
|
||||||
|
<td class="text-nowrap">{{ g[7] }}</td>
|
||||||
|
<td>
|
||||||
|
<form method="POST" action="{{ url_for('admin_delete', entry_id=g[0]) }}?page={{ page }}"
|
||||||
|
onsubmit="return confirm('Delete entry for {{ g[1] }} {{ g[2] }}?')">
|
||||||
|
<button type="submit" class="btn btn-danger btn-sm">Delete</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% else %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="8" class="text-center text-muted">No entries found.</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if total_pages > 1 %}
|
||||||
|
<nav>
|
||||||
|
<ul class="pagination">
|
||||||
|
<li class="page-item {% if page == 1 %}disabled{% endif %}">
|
||||||
|
<a class="page-link" href="{{ url_for('admin', page=page-1) }}">Previous</a>
|
||||||
|
</li>
|
||||||
|
{% for p in range(1, total_pages + 1) %}
|
||||||
|
<li class="page-item {% if p == page %}active{% endif %}">
|
||||||
|
<a class="page-link" href="{{ url_for('admin', page=p) }}">{{ p }}</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
<li class="page-item {% if page == total_pages %}disabled{% endif %}">
|
||||||
|
<a class="page-link" href="{{ url_for('admin', page=page+1) }}">Next</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user