Michael Rabinovich commited on
Commit
47c86cf
·
1 Parent(s): 0bd3c0a

app: add admin tab for promoting and demoting rows

Browse files

new 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.

Files changed (1) hide show
  1. app.py +172 -0
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`