Michael Rabinovich commited on
Commit
58ce093
·
1 Parent(s): 0957a56

app: admin tab as a multi-select table with delete

Browse files

replaces the single-row dropdown with an editable table: one row per
submission, a leading select checkbox the only editable column, the
rest read-only context. promote, demote and delete now act on every
ticked row at once. promote applies the chosen validation_method to the
whole selection. delete sits in a danger-zone accordion behind a
confirm checkbox that arms the button, and clears companion artifacts
plus the row. a live count line reports how many rows are selected.

Files changed (1) hide show
  1. app.py +154 -76
app.py CHANGED
@@ -25,6 +25,8 @@ from gradio_leaderboard import Leaderboard
25
  from huggingface_hub import hf_hub_download
26
 
27
  from leaderboard import (
 
 
28
  HF_DATA_REPO,
29
  HF_SUBMISSIONS_REPO,
30
  LEADERBOARD_DATATYPES,
@@ -32,9 +34,16 @@ from leaderboard import (
32
  VALIDATED_LEADERBOARD_DATATYPES,
33
  _fmt_timestamp,
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,111 +165,138 @@ def _enable_submit_when_logged_in(
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
 
@@ -488,30 +524,40 @@ to publish the resulting row on the public leaderboard.
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():
@@ -519,22 +565,52 @@ to publish the resulting row on the public leaderboard.
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.
@@ -558,10 +634,12 @@ to publish the resulting row on the public leaderboard.
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
  )
 
25
  from huggingface_hub import hf_hub_download
26
 
27
  from leaderboard import (
28
+ ADMIN_COLUMNS,
29
+ ADMIN_SELECT_COL,
30
  HF_DATA_REPO,
31
  HF_SUBMISSIONS_REPO,
32
  LEADERBOARD_DATATYPES,
 
34
  VALIDATED_LEADERBOARD_DATATYPES,
35
  _fmt_timestamp,
36
  build_combined_csv,
37
+ load_admin_table,
38
  load_leaderboard_split,
39
  )
40
+ from admin import (
41
+ VALID_METHODS,
42
+ delete_rows,
43
+ demote_rows,
44
+ is_admin,
45
+ promote_rows,
46
+ )
47
  from submit import handle_submit
48
 
49
  logger = logging.getLogger(__name__)
 
165
  return gr.Button(interactive=profile is not None)
166
 
167
 
168
+ def _selected_ids(table_df: pd.DataFrame | None) -> list[str]:
169
+ """Submission ids of the rows whose ``select`` checkbox is ticked."""
170
+ if (
171
+ table_df is None
172
+ or len(table_df) == 0
173
+ or ADMIN_SELECT_COL not in table_df.columns
174
+ or "submission_id" not in table_df.columns
175
+ ):
176
+ return []
177
+ mask = table_df[ADMIN_SELECT_COL].apply(bool)
178
+ return [str(s) for s in table_df.loc[mask, "submission_id"].tolist() if s]
 
 
 
 
 
 
 
179
 
180
 
181
+ def _admin_selection_status(table_df: pd.DataFrame | None) -> str:
182
+ """Live count line under the admin table, updated as boxes are ticked."""
183
+ n = len(_selected_ids(table_df))
184
+ return f"**{n}** row(s) selected." if n else "_No rows selected._"
185
 
186
 
187
  def _gate_admin_controls(
188
  profile: gr.OAuthProfile | None,
189
+ ) -> tuple[gr.Dataframe, gr.Radio, gr.Button, gr.Button, gr.Checkbox, gr.Button, str]:
190
  """Enable the admin controls only for a logged-in user in the admin set.
191
 
192
  Runs on every page load and re-runs on LoginButton auth events.
193
+ Non-admins and logged-out visitors get the tab with the table
194
+ read-only and every control disabled, mirroring the server-side
195
+ re-check in each handler. The delete button always loads disarmed:
196
+ it only enables once the confirm checkbox is ticked.
197
  """
198
  admin = is_admin(profile)
199
  if profile is None:
200
  status = "Log in with an admin account to enable the controls below."
201
  elif admin:
202
+ status = f"Signed in as `{profile.username}`. Admin controls enabled."
203
  else:
204
  status = (
205
  f"Signed in as `{profile.username}`, which is not in the admin "
206
  "set. Controls are disabled."
207
  )
208
  return (
209
+ gr.Dataframe(interactive=admin),
210
  gr.Radio(interactive=admin),
211
  gr.Button(interactive=admin),
212
  gr.Button(interactive=admin),
213
+ gr.Checkbox(interactive=admin, value=False),
214
+ gr.Button(interactive=False),
215
  status,
216
  )
217
 
218
 
219
+ def _arm_delete(
220
+ confirm: bool, profile: gr.OAuthProfile | None,
221
+ ) -> gr.Button:
222
+ """Enable the delete button only when an admin has ticked the confirm box."""
223
+ return gr.Button(interactive=bool(confirm) and is_admin(profile))
224
+
225
+
226
  def _admin_promote(
227
+ table_df: pd.DataFrame | None,
228
  method: str | None,
229
  profile: gr.OAuthProfile | None,
230
+ ) -> tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]:
231
+ """Promote every ticked row, then refresh the admin table + both tiers.
232
 
233
  Re-checks :func:`admin.is_admin` server-side so a tampered client
234
+ that re-enables the button still can't write.
 
 
235
  """
236
  if not is_admin(profile):
237
  raise gr.Error("You are not in the admin set.")
238
+ ids = _selected_ids(table_df)
239
+ if not ids:
240
+ raise gr.Error("Tick at least one row first.")
241
  if not method:
242
+ raise gr.Error("Pick a validation_method first.")
243
  try:
244
+ promote_rows(ids, method)
245
  except (LookupError, ValueError) as e:
246
  raise gr.Error(str(e))
247
+ gr.Info(f"Promoted {len(ids)} row(s) to validated ({method}).")
248
  validated, unvalidated = load_leaderboard_split()
249
+ return load_admin_table(), validated, unvalidated
 
 
 
 
250
 
251
 
252
  def _admin_demote(
253
+ table_df: pd.DataFrame | None,
254
+ profile: gr.OAuthProfile | None,
255
+ ) -> tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]:
256
+ """Demote every ticked row, then refresh the admin table + both tiers."""
257
+ if not is_admin(profile):
258
+ raise gr.Error("You are not in the admin set.")
259
+ ids = _selected_ids(table_df)
260
+ if not ids:
261
+ raise gr.Error("Tick at least one row first.")
262
+ try:
263
+ demote_rows(ids)
264
+ except (LookupError, ValueError) as e:
265
+ raise gr.Error(str(e))
266
+ gr.Info(f"Demoted {len(ids)} row(s) to unvalidated.")
267
+ validated, unvalidated = load_leaderboard_split()
268
+ return load_admin_table(), validated, unvalidated
269
+
270
+
271
+ def _admin_delete(
272
+ table_df: pd.DataFrame | None,
273
+ confirm: bool,
274
  profile: gr.OAuthProfile | None,
275
+ ) -> tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame, gr.Checkbox, gr.Button]:
276
+ """Delete every ticked row (artifacts + row), then refresh and disarm.
277
+
278
+ Resets the confirm checkbox and re-disables the delete button on
279
+ the way out so the next deletion needs a fresh, deliberate confirm.
280
+ """
281
  if not is_admin(profile):
282
  raise gr.Error("You are not in the admin set.")
283
+ if not confirm:
284
+ raise gr.Error("Tick the confirmation box to enable delete.")
285
+ ids = _selected_ids(table_df)
286
+ if not ids:
287
+ raise gr.Error("Tick at least one row first.")
288
  try:
289
+ delete_rows(ids)
290
+ except ValueError as e:
291
  raise gr.Error(str(e))
292
+ gr.Info(f"Deleted {len(ids)} submission(s).")
293
  validated, unvalidated = load_leaderboard_split()
294
  return (
295
+ load_admin_table(),
296
  validated,
297
  unvalidated,
298
+ gr.Checkbox(value=False),
299
+ gr.Button(interactive=False),
300
  )
301
 
302
 
 
524
  gr.Markdown(ABOUT_MD)
525
 
526
  with gr.Tab("Admin"):
527
+ # Maintainer-only controls. The tab is visible to everyone (a
528
+ # hint the path exists); the table + buttons are gated to OAuth
529
+ # users in the CADGENBENCH_ADMINS set via the `blocks.load`
530
+ # handler below + a server-side re-check in every handler. See
531
+ # decisions/validation-policy.md.
532
  gr.Markdown(
533
  "## Admin\n"
534
+ "Tick rows in the **select** column, then promote them into the "
535
+ "**Validated** tier (recording an evidence type), demote them back "
536
+ "to **Unvalidated**, or delete them. Actions apply to every ticked "
537
+ "row at once. Limited to maintainers in the admin set; everyone "
538
+ "else sees the tab with the controls disabled."
539
  )
540
  admin_login_btn = gr.LoginButton()
541
  admin_status = gr.Markdown(
542
  "Log in with an admin account to enable the controls below."
543
  )
544
+ # Only the leading `select` column is editable; the rest is
545
+ # read-only context. Click-to-tick drives every action below.
546
+ admin_table = gr.Dataframe(
547
+ value=load_admin_table(),
548
+ datatype=[
549
+ "bool", "str", "str", "str", "str", "str", "str", "number",
550
+ "str",
551
+ ],
552
+ static_columns=list(range(1, len(ADMIN_COLUMNS))),
553
  interactive=False,
554
+ label="Submissions (tick select to choose rows)",
555
+ wrap=True,
556
  )
557
+ admin_selection_md = gr.Markdown("_No rows selected._")
558
  admin_method_radio = gr.Radio(
559
  choices=list(VALID_METHODS),
560
+ label="validation_method (applied to all rows on promote)",
561
  interactive=False,
562
  )
563
  with gr.Row():
 
565
  "Mark validated", variant="primary", interactive=False,
566
  )
567
  demote_btn = gr.Button("Mark unvalidated", interactive=False)
568
+ with gr.Accordion("Danger zone: delete", open=False):
569
+ gr.Markdown(
570
+ "Permanently deletes the ticked rows **and** their uploaded "
571
+ "zip + report files from the submissions dataset. This cannot "
572
+ "be undone (only a manual revert of the dataset commit)."
573
+ )
574
+ delete_confirm = gr.Checkbox(
575
+ label=(
576
+ "I understand this permanently deletes the selected "
577
+ "submissions and their files."
578
+ ),
579
+ value=False,
580
+ interactive=False,
581
+ )
582
+ delete_btn = gr.Button(
583
+ "Delete selected", variant="stop", interactive=False,
584
+ )
585
+ admin_refresh_btn = gr.Button("Refresh", size="sm")
586
 
587
+ admin_table.change(
588
+ fn=_admin_selection_status,
589
+ inputs=admin_table,
590
+ outputs=admin_selection_md,
591
+ )
592
  promote_btn.click(
593
  fn=_admin_promote,
594
+ inputs=[admin_table, admin_method_radio],
595
+ outputs=[admin_table, validated_view, unvalidated_view],
596
  )
597
  demote_btn.click(
598
  fn=_admin_demote,
599
+ inputs=[admin_table],
600
+ outputs=[admin_table, validated_view, unvalidated_view],
601
+ )
602
+ delete_confirm.change(
603
+ fn=_arm_delete, inputs=[delete_confirm], outputs=delete_btn,
604
  )
605
+ delete_btn.click(
606
+ fn=_admin_delete,
607
+ inputs=[admin_table, delete_confirm],
608
+ outputs=[
609
+ admin_table, validated_view, unvalidated_view,
610
+ delete_confirm, delete_btn,
611
+ ],
612
  )
613
+ admin_refresh_btn.click(fn=load_admin_table, outputs=admin_table)
614
 
615
  # gradio_leaderboard.Leaderboard handles its own update path
616
  # cleanly; bind a Timer to push fresh dataframes every 10 seconds.
 
634
  blocks.load(
635
  fn=_gate_admin_controls,
636
  outputs=[
637
+ admin_table,
638
  admin_method_radio,
639
  promote_btn,
640
  demote_btn,
641
+ delete_confirm,
642
+ delete_btn,
643
  admin_status,
644
  ],
645
  )