Michael Rabinovich commited on
Commit ·
47c86cf
1
Parent(s): 0bd3c0a
app: add admin tab for promoting and demoting rows
Browse filesnew admin tab with a login button, a submission dropdown, a
validation_method radio, and mark validated / mark unvalidated buttons.
the controls are gated on membership in the CADGENBENCH_ADMINS set via a
blocks.load handler, mirroring the submit button's login gate; the
promote and demote handlers re-check admin rights server-side and
refresh the dropdown plus both leaderboard tables after a write.
app.py
CHANGED
|
@@ -34,6 +34,7 @@ from leaderboard import (
|
|
| 34 |
build_combined_csv,
|
| 35 |
load_leaderboard_split,
|
| 36 |
)
|
|
|
|
| 37 |
from submit import handle_submit
|
| 38 |
|
| 39 |
logger = logging.getLogger(__name__)
|
|
@@ -155,6 +156,114 @@ def _enable_submit_when_logged_in(
|
|
| 155 |
return gr.Button(interactive=profile is not None)
|
| 156 |
|
| 157 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
def _format_detail_and_report(
|
| 159 |
df: pd.DataFrame | None, evt: gr.SelectData,
|
| 160 |
) -> tuple[str, str]:
|
|
@@ -378,6 +487,55 @@ to publish the resulting row on the public leaderboard.
|
|
| 378 |
with gr.Tab("About"):
|
| 379 |
gr.Markdown(ABOUT_MD)
|
| 380 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 381 |
# gradio_leaderboard.Leaderboard handles its own update path
|
| 382 |
# cleanly; bind a Timer to push fresh dataframes every 10 seconds.
|
| 383 |
# Single tick runs `load_leaderboard_split` once and pushes the
|
|
@@ -394,6 +552,20 @@ to publish the resulting row on the public leaderboard.
|
|
| 394 |
# Gradio's auth-event plumbing.
|
| 395 |
blocks.load(fn=_enable_submit_when_logged_in, outputs=submit_btn)
|
| 396 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 397 |
|
| 398 |
# Mount Gradio under a FastAPI parent so the custom proxy route
|
| 399 |
# above lives at the same origin as the UI. Direct routes on `app`
|
|
|
|
| 34 |
build_combined_csv,
|
| 35 |
load_leaderboard_split,
|
| 36 |
)
|
| 37 |
+
from admin import VALID_METHODS, demote_row, is_admin, promote_row
|
| 38 |
from submit import handle_submit
|
| 39 |
|
| 40 |
logger = logging.getLogger(__name__)
|
|
|
|
| 156 |
return gr.Button(interactive=profile is not None)
|
| 157 |
|
| 158 |
|
| 159 |
+
def _choices_from_split(
|
| 160 |
+
validated: pd.DataFrame | None,
|
| 161 |
+
unvalidated: pd.DataFrame | None,
|
| 162 |
+
) -> list[tuple[str, str]]:
|
| 163 |
+
"""Build the admin dropdown's ``(label, submission_id)`` choice list.
|
| 164 |
+
|
| 165 |
+
Lists unvalidated rows first (the common promote target), then
|
| 166 |
+
validated rows. Each label is tagged with its current tier so the
|
| 167 |
+
admin can tell which direction a button press moves the row.
|
| 168 |
+
"""
|
| 169 |
+
choices: list[tuple[str, str]] = []
|
| 170 |
+
for df, tier in ((unvalidated, "unvalidated"), (validated, "validated")):
|
| 171 |
+
if df is None or "submission_id" not in df.columns:
|
| 172 |
+
continue
|
| 173 |
+
for sid in df["submission_id"].tolist():
|
| 174 |
+
if sid:
|
| 175 |
+
choices.append((f"{sid} ({tier})", str(sid)))
|
| 176 |
+
return choices
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
def _admin_row_choices() -> list[tuple[str, str]]:
|
| 180 |
+
"""Fresh choice list pulled from the current results split."""
|
| 181 |
+
return _choices_from_split(*load_leaderboard_split())
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
def _gate_admin_controls(
|
| 185 |
+
profile: gr.OAuthProfile | None,
|
| 186 |
+
) -> tuple[gr.Dropdown, gr.Radio, gr.Button, gr.Button, str]:
|
| 187 |
+
"""Enable the admin controls only for a logged-in user in the admin set.
|
| 188 |
+
|
| 189 |
+
Runs on every page load and re-runs on LoginButton auth events.
|
| 190 |
+
Logged-out visitors and non-admins see the tab but every control
|
| 191 |
+
stays disabled, mirroring the server-side gate in the promote /
|
| 192 |
+
demote handlers (which raise ``gr.Error`` if invoked without admin
|
| 193 |
+
rights).
|
| 194 |
+
"""
|
| 195 |
+
admin = is_admin(profile)
|
| 196 |
+
if profile is None:
|
| 197 |
+
status = "Log in with an admin account to enable the controls below."
|
| 198 |
+
elif admin:
|
| 199 |
+
status = f"Signed in as `{profile.username}`. Promotion controls enabled."
|
| 200 |
+
else:
|
| 201 |
+
status = (
|
| 202 |
+
f"Signed in as `{profile.username}`, which is not in the admin "
|
| 203 |
+
"set. Controls are disabled."
|
| 204 |
+
)
|
| 205 |
+
return (
|
| 206 |
+
gr.Dropdown(interactive=admin),
|
| 207 |
+
gr.Radio(interactive=admin),
|
| 208 |
+
gr.Button(interactive=admin),
|
| 209 |
+
gr.Button(interactive=admin),
|
| 210 |
+
status,
|
| 211 |
+
)
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
def _admin_promote(
|
| 215 |
+
submission_id: str | None,
|
| 216 |
+
method: str | None,
|
| 217 |
+
profile: gr.OAuthProfile | None,
|
| 218 |
+
) -> tuple[gr.Dropdown, pd.DataFrame, pd.DataFrame]:
|
| 219 |
+
"""Promote the selected row, then refresh the dropdown + both tables.
|
| 220 |
+
|
| 221 |
+
Re-checks :func:`admin.is_admin` server-side so a tampered client
|
| 222 |
+
that re-enables the button still can't write. Surfaces the outcome
|
| 223 |
+
via a toast; maps the helper's ``LookupError`` / ``ValueError`` to
|
| 224 |
+
a clean ``gr.Error``.
|
| 225 |
+
"""
|
| 226 |
+
if not is_admin(profile):
|
| 227 |
+
raise gr.Error("You are not in the admin set.")
|
| 228 |
+
if not submission_id:
|
| 229 |
+
raise gr.Error("Select a submission first.")
|
| 230 |
+
if not method:
|
| 231 |
+
raise gr.Error("Select a validation method first.")
|
| 232 |
+
try:
|
| 233 |
+
promote_row(submission_id, method)
|
| 234 |
+
except (LookupError, ValueError) as e:
|
| 235 |
+
raise gr.Error(str(e))
|
| 236 |
+
gr.Info(f"Promoted {submission_id} to validated ({method}).")
|
| 237 |
+
validated, unvalidated = load_leaderboard_split()
|
| 238 |
+
return (
|
| 239 |
+
gr.Dropdown(choices=_choices_from_split(validated, unvalidated)),
|
| 240 |
+
validated,
|
| 241 |
+
unvalidated,
|
| 242 |
+
)
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
def _admin_demote(
|
| 246 |
+
submission_id: str | None,
|
| 247 |
+
profile: gr.OAuthProfile | None,
|
| 248 |
+
) -> tuple[gr.Dropdown, pd.DataFrame, pd.DataFrame]:
|
| 249 |
+
"""Demote the selected row, then refresh the dropdown + both tables."""
|
| 250 |
+
if not is_admin(profile):
|
| 251 |
+
raise gr.Error("You are not in the admin set.")
|
| 252 |
+
if not submission_id:
|
| 253 |
+
raise gr.Error("Select a submission first.")
|
| 254 |
+
try:
|
| 255 |
+
demote_row(submission_id)
|
| 256 |
+
except LookupError as e:
|
| 257 |
+
raise gr.Error(str(e))
|
| 258 |
+
gr.Info(f"Demoted {submission_id} to unvalidated.")
|
| 259 |
+
validated, unvalidated = load_leaderboard_split()
|
| 260 |
+
return (
|
| 261 |
+
gr.Dropdown(choices=_choices_from_split(validated, unvalidated)),
|
| 262 |
+
validated,
|
| 263 |
+
unvalidated,
|
| 264 |
+
)
|
| 265 |
+
|
| 266 |
+
|
| 267 |
def _format_detail_and_report(
|
| 268 |
df: pd.DataFrame | None, evt: gr.SelectData,
|
| 269 |
) -> tuple[str, str]:
|
|
|
|
| 487 |
with gr.Tab("About"):
|
| 488 |
gr.Markdown(ABOUT_MD)
|
| 489 |
|
| 490 |
+
with gr.Tab("Admin"):
|
| 491 |
+
# Maintainer-only promotion controls. The tab is visible to
|
| 492 |
+
# everyone (a hint that the path exists); the controls are
|
| 493 |
+
# gated to OAuth users in the CADGENBENCH_ADMINS set via the
|
| 494 |
+
# `blocks.load` handler below + a server-side re-check in the
|
| 495 |
+
# promote / demote handlers. See decisions/validation-policy.md.
|
| 496 |
+
gr.Markdown(
|
| 497 |
+
"## Admin\n"
|
| 498 |
+
"Promote a submission into the **Validated** table (recording the "
|
| 499 |
+
"evidence type) or demote it back to **Unvalidated**. Limited to "
|
| 500 |
+
"maintainers in the admin set; everyone else sees the tab with the "
|
| 501 |
+
"controls disabled."
|
| 502 |
+
)
|
| 503 |
+
admin_login_btn = gr.LoginButton()
|
| 504 |
+
admin_status = gr.Markdown(
|
| 505 |
+
"Log in with an admin account to enable the controls below."
|
| 506 |
+
)
|
| 507 |
+
admin_submission_dd = gr.Dropdown(
|
| 508 |
+
choices=_choices_from_split(initial_validated, initial_unvalidated),
|
| 509 |
+
label="Submission",
|
| 510 |
+
interactive=False,
|
| 511 |
+
)
|
| 512 |
+
admin_method_radio = gr.Radio(
|
| 513 |
+
choices=list(VALID_METHODS),
|
| 514 |
+
label="validation_method (required to promote)",
|
| 515 |
+
interactive=False,
|
| 516 |
+
)
|
| 517 |
+
with gr.Row():
|
| 518 |
+
promote_btn = gr.Button(
|
| 519 |
+
"Mark validated", variant="primary", interactive=False,
|
| 520 |
+
)
|
| 521 |
+
demote_btn = gr.Button("Mark unvalidated", interactive=False)
|
| 522 |
+
admin_refresh_btn = gr.Button("Refresh list", size="sm")
|
| 523 |
+
|
| 524 |
+
promote_btn.click(
|
| 525 |
+
fn=_admin_promote,
|
| 526 |
+
inputs=[admin_submission_dd, admin_method_radio],
|
| 527 |
+
outputs=[admin_submission_dd, validated_view, unvalidated_view],
|
| 528 |
+
)
|
| 529 |
+
demote_btn.click(
|
| 530 |
+
fn=_admin_demote,
|
| 531 |
+
inputs=[admin_submission_dd],
|
| 532 |
+
outputs=[admin_submission_dd, validated_view, unvalidated_view],
|
| 533 |
+
)
|
| 534 |
+
admin_refresh_btn.click(
|
| 535 |
+
fn=lambda: gr.Dropdown(choices=_admin_row_choices()),
|
| 536 |
+
outputs=admin_submission_dd,
|
| 537 |
+
)
|
| 538 |
+
|
| 539 |
# gradio_leaderboard.Leaderboard handles its own update path
|
| 540 |
# cleanly; bind a Timer to push fresh dataframes every 10 seconds.
|
| 541 |
# Single tick runs `load_leaderboard_split` once and pushes the
|
|
|
|
| 552 |
# Gradio's auth-event plumbing.
|
| 553 |
blocks.load(fn=_enable_submit_when_logged_in, outputs=submit_btn)
|
| 554 |
|
| 555 |
+
# Same per-load OAuth read, gating the Admin tab's controls on
|
| 556 |
+
# membership in the CADGENBENCH_ADMINS set. Logged-out / non-admin
|
| 557 |
+
# visitors get the tab with everything disabled.
|
| 558 |
+
blocks.load(
|
| 559 |
+
fn=_gate_admin_controls,
|
| 560 |
+
outputs=[
|
| 561 |
+
admin_submission_dd,
|
| 562 |
+
admin_method_radio,
|
| 563 |
+
promote_btn,
|
| 564 |
+
demote_btn,
|
| 565 |
+
admin_status,
|
| 566 |
+
],
|
| 567 |
+
)
|
| 568 |
+
|
| 569 |
|
| 570 |
# Mount Gradio under a FastAPI parent so the custom proxy route
|
| 571 |
# above lives at the same origin as the UI. Direct routes on `app`
|