This is an automated email from the ASF dual-hosted git repository.
sbp pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tooling-atr-experiments.git
The following commit(s) were added to refs/heads/main by this push:
new 139199f Add a data browser for admins to inspect database records
139199f is described below
commit 139199fb640b0ecfd6d720d1f6885c9de94b0cad
Author: Sean B. Palmer <[email protected]>
AuthorDate: Thu Feb 13 16:57:49 2025 +0200
Add a data browser for admins to inspect database records
---
atr/routes.py | 65 +++++++++++++++++++++++++++++-
atr/templates/data-browser.html | 89 +++++++++++++++++++++++++++++++++++++++++
2 files changed, 153 insertions(+), 1 deletion(-)
diff --git a/atr/routes.py b/atr/routes.py
index aa5cadc..d9a482c 100644
--- a/atr/routes.py
+++ b/atr/routes.py
@@ -31,7 +31,18 @@ from sqlmodel import Session, select
from sqlalchemy.exc import IntegrityError
import httpx
-from .models import PMC, Release, ReleaseStage, ReleasePhase, Package
+from .models import (
+ DistributionChannel,
+ PMC,
+ PMCKeyLink,
+ Package,
+ ProductLine,
+ PublicSigningKey,
+ Release,
+ ReleasePhase,
+ ReleaseStage,
+ VotePolicy,
+)
if APP is ...:
raise ValueError("APP is not set")
@@ -167,6 +178,58 @@ async def add_release_candidate() -> str:
)
[email protected]("/admin/data-browser")
[email protected]("/admin/data-browser/<model>")
+@require(R.committer)
+async def admin_data_browser(model: str = "PMC") -> str:
+ "Browse all records in the database."
+ session = await session_read()
+ if session is None:
+ raise ASFQuartException("Not authenticated", errorcode=401)
+
+ if session.uid not in ALLOWED_USERS:
+ raise ASFQuartException("You are not authorized to browse data",
errorcode=403)
+
+ # Map of model names to their classes
+ models = {
+ "PMC": PMC,
+ "Release": Release,
+ "Package": Package,
+ "VotePolicy": VotePolicy,
+ "ProductLine": ProductLine,
+ "DistributionChannel": DistributionChannel,
+ "PublicSigningKey": PublicSigningKey,
+ "PMCKeyLink": PMCKeyLink,
+ }
+
+ if model not in models:
+ # Default to PMC if invalid model specified
+ model = "PMC"
+
+ with Session(current_app.config["engine"]) as db_session:
+ # Get all records for the selected model
+ statement = select(models[model])
+ records = db_session.exec(statement).all()
+
+ # Convert records to dictionaries for JSON serialization
+ records_dict = []
+ for record in records:
+ if hasattr(record, "dict"):
+ record_dict = record.dict()
+ else:
+ # Fallback for models without dict() method
+ record_dict = {
+ "id": getattr(record, "id", None),
+ "storage_key": getattr(record, "storage_key", None),
+ }
+ for key in record.__dict__:
+ if not key.startswith("_"):
+ record_dict[key] = getattr(record, key)
+ records_dict.append(record_dict)
+
+ return await render_template("data-browser.html",
models=list(models.keys()), model=model, records=records_dict)
+
+
@APP.route("/admin/update-pmcs", methods=["GET", "POST"])
async def admin_update_pmcs() -> str:
"Update PMCs from remote, authoritative committee-info.json."
diff --git a/atr/templates/data-browser.html b/atr/templates/data-browser.html
new file mode 100644
index 0000000..9f0a447
--- /dev/null
+++ b/atr/templates/data-browser.html
@@ -0,0 +1,89 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width,initial-scale=1.0" />
+ <meta name="description" content="Browse all records in the database." />
+ <title>ATR | Data Browser</title>
+ <link rel="stylesheet" href="{{ url_for('static', filename='root.css') }}"
/>
+ <style>
+ .model-nav {
+ margin: 1rem 0;
+ padding: 0.5rem;
+ background: #f5f5f5;
+ border-radius: 4px;
+ }
+
+ .model-nav a {
+ margin-right: 1rem;
+ padding: 0.25rem 0.5rem;
+ text-decoration: none;
+ color: #333;
+ }
+
+ .model-nav a.active {
+ background: #333;
+ color: white;
+ border-radius: 2px;
+ }
+
+ .record {
+ border: 1px solid #ddd;
+ padding: 1rem;
+ margin-bottom: 1rem;
+ border-radius: 4px;
+ }
+
+ .record pre {
+ background: #f5f5f5;
+ padding: 0.5rem;
+ border-radius: 2px;
+ overflow-x: auto;
+ }
+
+ .record-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 0.5rem;
+ }
+
+ .record-meta {
+ color: #666;
+ font-size: 0.9em;
+ }
+
+ .no-records {
+ color: #666;
+ font-style: italic;
+ }
+ </style>
+ </head>
+ <body>
+ <h1>Data Browser</h1>
+ <p class="intro">Browse all records in the database.</p>
+
+ <div class="model-nav">
+ {% for model_name in models %}
+ <a href="{{ url_for('admin_data_browser', model=model_name) }}"
+ {% if model == model_name %}class="active"{% endif %}>{{ model_name
}}</a>
+ {% endfor %}
+ </div>
+
+ {% if records %}
+ <div class="records">
+ {% for record in records %}
+ <div class="record">
+ <div class="record-header">
+ <h3>{{ record.get('id', record.get('storage_key', 'Unknown ID')
) }}</h3>
+ <span class="record-meta">{{ model }}</span>
+ </div>
+ <pre>{{ record | tojson(indent=2) }}</pre>
+ </div>
+ {% endfor %}
+ </div>
+ {% else %}
+ <p class="no-records">No records found for {{ model }}.</p>
+ {% endif %}
+ </body>
+</html>
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]