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]

Reply via email to