htranx commited on
Commit
5eb0340
·
1 Parent(s): 4e8b91e

size the invited tab to its content and line the tables up (#19)

Browse files

- size the invited tab to its content and line the tables up (aba62528278072637aea5c14935c772c57695ef9)

Files changed (3) hide show
  1. app.py +5 -1
  2. invited.py +96 -19
  3. tests/test_invited.py +165 -0
app.py CHANGED
@@ -964,8 +964,12 @@ def _invited_html():
964
  who is invited is worse than a slow page.
965
  """
966
  doc = invited.document(config.HF_TOKEN)
 
 
 
 
 
967
  return (f'<iframe title="invited teams" srcdoc="{html.escape(doc)}" '
968
- f'scrolling="no" '
969
  f'style="width:100%;border:0;display:block;min-height:400px"></iframe>')
970
 
971
 
 
964
  who is invited is worse than a slow page.
965
  """
966
  doc = invited.document(config.HF_TOKEN)
967
+ # No scrolling="no": the document self-sizes, but if that script ever fails
968
+ # to run the frame keeps its default height, and suppressing the scrollbar
969
+ # turns that into silent truncation. That is exactly how this shipped the
970
+ # first time -- the page was cut off partway down the second of six tables
971
+ # with no way to reach the rest. Degrade to a scrollbar instead.
972
  return (f'<iframe title="invited teams" srcdoc="{html.escape(doc)}" '
 
973
  f'style="width:100%;border:0;display:block;min-height:400px"></iframe>')
974
 
975
 
invited.py CHANGED
@@ -22,20 +22,75 @@ import config
22
  # shortlist cannot silently reorder what participants read.
23
  _ORDER = [(t, d) for t in config.TRACKS for d in config.DIVISIONS]
24
 
 
 
 
25
  CSS = """
26
- body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
27
- margin:0;color:#1c1e21;font-size:14px}
28
- h3{font-size:1rem;margin:1.4rem 0 .3rem}
29
- p.note{color:#65676b;font-size:.87rem;margin:.2rem 0 1rem}
30
- table{border-collapse:collapse;margin:.3rem 0 1rem;font-size:.87rem}
31
- th,td{border:1px solid #dcdee1;padding:.3rem .6rem;text-align:left;white-space:nowrap}
32
- th{background:#f2f3f5;font-weight:600}
33
- td.num{text-align:right;font-variant-numeric:tabular-nums}
34
- td.team{max-width:20rem;white-space:normal;overflow-wrap:anywhere;font-weight:600}
35
- .selfrep{color:#65676b;font-weight:400;font-size:.85em}
36
- .empty{color:#65676b;font-style:italic}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  """
38
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
 
40
  def load_shortlist_report(token: str | None) -> dict | None:
41
  """The raw shortlist file, or None if it is not there yet."""
@@ -60,15 +115,34 @@ def _rows(subtrack: dict) -> list[dict]:
60
  return sorted(rows, key=lambda t: t.get("rank") or 0)
61
 
62
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  def document(token: str | None) -> str:
64
- """The whole page, as a standalone HTML document for an iframe srcdoc."""
 
 
 
 
 
65
  rep = load_shortlist_report(token)
66
  esc = html.escape
67
- out = [f"<!doctype html><meta charset=utf-8><style>{CSS}</style>"]
68
 
69
  if not rep or not rep.get("subtracks"):
70
  out.append("<p class=empty>The invited teams have not been published "
71
  "yet. They appear here once the shortlist is final.</p>")
 
72
  return "".join(out)
73
 
74
  by_sub = {(st["track"], st["division"]): st for st in rep["subtracks"]}
@@ -99,8 +173,10 @@ def document(token: str | None) -> str:
99
  if not rows:
100
  out.append("<p class=empty>No teams invited in this sub-track.</p>")
101
  continue
102
- out.append("<table><tr><th>#</th><th>Team</th><th>HF account</th>"
103
- f"<th>Model</th><th>{metric}</th><th>Total params</th></tr>")
 
 
104
  for i, t in enumerate(rows, 1):
105
  score = t.get("primary_score")
106
  try:
@@ -111,15 +187,16 @@ def document(token: str | None) -> str:
111
  params = (f"{params/1e9:g}B"
112
  if isinstance(params, (int, float)) and params else "—")
113
  out.append(
114
- f"<tr><td class=num>{i}</td>"
115
  f"<td class=team>{esc(str(t.get('team_name') or ''))}</td>"
116
- f"<td>{esc(str(t.get('hf_user') or ''))}</td>"
117
- f"<td>{esc(str(t.get('model_name') or ''))}</td>"
118
  f"<td class=num>{score}</td>"
119
  f"<td class=num>{esc(params)}</td></tr>")
120
- out.append("</table>")
121
 
122
  out.append("<p class=note>Ranked by each sub-track's own validation metric, "
123
  "open-weight entries only, deduplicated by team. Scores are the "
124
  "validation results already shown on the Validation board.</p>")
 
125
  return "".join(out)
 
22
  # shortlist cannot silently reorder what participants read.
23
  _ORDER = [(t, d) for t in config.TRACKS for d in config.DIVISIONS]
24
 
25
+ # Deliberately the same variables, fonts and border colours as results._DOC_CSS,
26
+ # because this tab sits beside the two boards and a different table style reads
27
+ # as a different system rather than a third view of the same data.
28
  CSS = """
29
+ :root{--lb-border:#e4e1f0;--lb-text:#1b1830;--lb-sec:#6a6585;--lb-faint:#9b96b3;
30
+ --lb-head:#f6f5fc;--lb-hover:#faf9fe;
31
+ --lb-mono:'IBM Plex Mono',ui-monospace,SFMono-Regular,Menlo,monospace;
32
+ --lb-sans:'IBM Plex Sans',ui-sans-serif,system-ui,-apple-system,sans-serif}
33
+ *{box-sizing:border-box}
34
+ html,body{margin:0;padding:0}
35
+ body{font-family:var(--lb-sans);color:var(--lb-text);background:transparent;
36
+ font-size:14px;padding:2px 0 12px}
37
+ h3{font-size:1.05rem;font-weight:700;letter-spacing:-.01em;margin:1.5rem 0 .5rem;
38
+ padding-bottom:.4rem;border-bottom:2px solid var(--lb-border)}
39
+ p.note{color:var(--lb-sec);font-size:.87rem;margin:.2rem 0 1.1rem;line-height:1.5}
40
+ /* Fixed layout + a shared colgroup is what makes all six tables line up. With
41
+ auto layout each table sizes to its own longest cell, so the columns stepped
42
+ left and right between sub-tracks and it read as six unrelated tables. */
43
+ table{border-collapse:collapse;width:100%;table-layout:fixed;
44
+ margin:.3rem 0 1.2rem;font-size:.87rem}
45
+ th,td{border:1px solid var(--lb-border);padding:.4rem .6rem;text-align:left;
46
+ vertical-align:top}
47
+ th{background:var(--lb-head);font-weight:600;white-space:nowrap}
48
+ tbody tr:hover{background:var(--lb-hover)}
49
+ /* Team, account and model WRAP: model_name is free text from participants and
50
+ reaches 80+ chars, so nowrap here is what pushed the table off the page. */
51
+ td.team,td.acct,td.model{white-space:normal;overflow-wrap:anywhere}
52
+ td.team{font-weight:600}
53
+ td.acct,td.model{color:var(--lb-sec)}
54
+ td.num,th.num{text-align:right;white-space:nowrap;
55
+ font-variant-numeric:tabular-nums;font-family:var(--lb-mono)}
56
+ td.rank{text-align:right;color:var(--lb-faint);font-family:var(--lb-mono)}
57
+ .selfrep{color:var(--lb-sec);font-weight:400;font-size:.85em}
58
+ .empty{color:var(--lb-sec);font-style:italic}
59
  """
60
 
61
+ # One colgroup for every table, so a column is in the same place in all six.
62
+ COLS = ("<colgroup><col style='width:2.8rem'><col style='width:20%'>"
63
+ "<col style='width:15%'><col><col style='width:8.5rem'>"
64
+ "<col style='width:7rem'></colgroup>")
65
+
66
+ # Same self-sizing contract as results.board_document(). Two things it must get
67
+ # right, both learned there: measure the CONTENT wrapper for height, never
68
+ # documentElement.scrollHeight, which is bounded below by the iframe viewport and
69
+ # so feeds back into an infinite grow loop; and set width from the parent's
70
+ # #lb-tab, because an iframe is a replaced element with a 300px intrinsic width
71
+ # that Gradio's flex wrappers shrink-wrap regardless of CSS width:100%.
72
+ FIT_JS = (
73
+ "<script>"
74
+ "function fitW(){try{var fe=window.frameElement;if(!fe||!window.parent)return;"
75
+ "var host=window.parent.document.getElementById('lb-tab');if(!host)return;"
76
+ "var cs=window.parent.getComputedStyle(host);"
77
+ "var w=host.clientWidth-parseFloat(cs.paddingLeft||0)"
78
+ "-parseFloat(cs.paddingRight||0)-2;"
79
+ "if(w>40)fe.style.width=w+'px';}catch(e){}}"
80
+ "function fitH(){try{var r=document.getElementById('invroot');"
81
+ "if(!r||!window.frameElement)return;"
82
+ "var h=Math.ceil(r.getBoundingClientRect().height)+16;"
83
+ "var cur=parseInt(window.frameElement.style.height||'0',10)||0;"
84
+ "if(h>40&&Math.abs(h-cur)>2)window.frameElement.style.height=h+'px';}catch(e){}}"
85
+ "function fit(){fitW();fitH();}"
86
+ "window.addEventListener('load',fit);"
87
+ "window.addEventListener('resize',fit);"
88
+ "if(window.parent)window.parent.addEventListener('resize',fit);"
89
+ "if(window.ResizeObserver){new ResizeObserver(fit).observe(document.body);}"
90
+ "setTimeout(fit,60);setTimeout(fit,400);setTimeout(fit,1200);"
91
+ "</script>"
92
+ )
93
+
94
 
95
  def load_shortlist_report(token: str | None) -> dict | None:
96
  """The raw shortlist file, or None if it is not there yet."""
 
115
  return sorted(rows, key=lambda t: t.get("rank") or 0)
116
 
117
 
118
+ def _head() -> str:
119
+ """Document head, with the same webfont the two boards load."""
120
+ return (
121
+ "<!DOCTYPE html><html lang='en'><head><meta charset='utf-8'>"
122
+ "<meta name='viewport' content='width=device-width, initial-scale=1'>"
123
+ "<link rel='preconnect' href='https://fonts.googleapis.com'>"
124
+ "<link rel='preconnect' href='https://fonts.gstatic.com' crossorigin>"
125
+ "<link href='https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@500;600"
126
+ "&family=IBM+Plex+Sans:wght@400;600;700&display=swap' rel='stylesheet'>"
127
+ f"<style>{CSS}</style></head><body>"
128
+ )
129
+
130
+
131
  def document(token: str | None) -> str:
132
+ """The whole page, as a standalone HTML document for an iframe srcdoc.
133
+
134
+ Everything visible sits inside #invroot, which is what the sizing script
135
+ measures. Without a content wrapper the iframe stays at its default height
136
+ and the page is simply cut off partway down the second table.
137
+ """
138
  rep = load_shortlist_report(token)
139
  esc = html.escape
140
+ out = [_head(), "<div id='invroot'>"]
141
 
142
  if not rep or not rep.get("subtracks"):
143
  out.append("<p class=empty>The invited teams have not been published "
144
  "yet. They appear here once the shortlist is final.</p>")
145
+ out.append(f"</div>{FIT_JS}</body></html>")
146
  return "".join(out)
147
 
148
  by_sub = {(st["track"], st["division"]): st for st in rep["subtracks"]}
 
173
  if not rows:
174
  out.append("<p class=empty>No teams invited in this sub-track.</p>")
175
  continue
176
+ out.append(f"<table>{COLS}<thead><tr><th class=num>#</th><th>Team</th>"
177
+ "<th>HF account</th>"
178
+ f"<th>Model</th><th class=num>{metric}</th>"
179
+ "<th class=num>Total params</th></tr></thead><tbody>")
180
  for i, t in enumerate(rows, 1):
181
  score = t.get("primary_score")
182
  try:
 
187
  params = (f"{params/1e9:g}B"
188
  if isinstance(params, (int, float)) and params else "—")
189
  out.append(
190
+ f"<tr><td class=rank>{i}</td>"
191
  f"<td class=team>{esc(str(t.get('team_name') or ''))}</td>"
192
+ f"<td class=acct>{esc(str(t.get('hf_user') or ''))}</td>"
193
+ f"<td class=model>{esc(str(t.get('model_name') or ''))}</td>"
194
  f"<td class=num>{score}</td>"
195
  f"<td class=num>{esc(params)}</td></tr>")
196
+ out.append("</tbody></table>")
197
 
198
  out.append("<p class=note>Ranked by each sub-track's own validation metric, "
199
  "open-weight entries only, deduplicated by team. Scores are the "
200
  "validation results already shown on the Validation board.</p>")
201
+ out.append(f"</div>{FIT_JS}</body></html>")
202
  return "".join(out)
tests/test_invited.py ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for the Invited to Test Phase tab.
2
+
3
+ Written 2026-08-08 after the first version shipped with no tests and three
4
+ visible defects: the iframe never grew past its default height so the page was
5
+ cut off partway down the second of six tables, the intro line ran off the right
6
+ edge, and each table sized to its own content so the columns stepped left and
7
+ right between sub-tracks. Each of those is pinned below.
8
+ """
9
+
10
+ import os
11
+ import re
12
+ import sys
13
+ import unittest
14
+ from unittest import mock
15
+
16
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
17
+
18
+ import invited # noqa: E402
19
+
20
+
21
+ def _report(n_subtracks=2, rows_per=3, note_rows=0):
22
+ subs = []
23
+ for i in range(n_subtracks):
24
+ shortlist = [
25
+ {
26
+ "rank": r + 1,
27
+ "team_name": f"Team {i}{r}",
28
+ "hf_user": f"user{i}{r}",
29
+ "model_name": "a-very-long-model-name-" + "x" * 60,
30
+ "primary_score": 0.5 - r / 100,
31
+ "total_params": 7_000_000_000,
32
+ }
33
+ for r in range(rows_per)
34
+ ]
35
+ for k in range(note_rows):
36
+ shortlist.append({
37
+ "rank": rows_per + k + 1, "team_name": "htranx",
38
+ "hf_user": "htranx", "model_name": "smoke",
39
+ "primary_score": 1.0, "total_params": 1,
40
+ "note": "organizer smoke test",
41
+ })
42
+ subs.append({
43
+ "track": ["convqa", "longqa"][i % 2],
44
+ "division": ["small", "large"][i % 2],
45
+ "track_label": f"Track{i}",
46
+ "primary_metric_label": "Accuracy",
47
+ "primary_is_self_reported": False,
48
+ "shortlist": shortlist,
49
+ })
50
+ return {"distinct_teams": 5, "total_seats": n_subtracks * rows_per,
51
+ "subtracks": subs}
52
+
53
+
54
+ def _doc(rep):
55
+ with mock.patch.object(invited, "load_shortlist_report", lambda t: rep):
56
+ return invited.document(None)
57
+
58
+
59
+ class DocumentIsWellFormedTest(unittest.TestCase):
60
+ def test_it_is_a_complete_html_document(self):
61
+ d = _doc(_report())
62
+ self.assertTrue(d.startswith("<!DOCTYPE html>"), d[:40])
63
+ self.assertTrue(d.rstrip().endswith("</html>"), d[-40:])
64
+
65
+ def test_the_empty_state_is_also_a_complete_document(self):
66
+ """The no-shortlist branch returned early and closed nothing."""
67
+ d = _doc(None)
68
+ self.assertTrue(d.startswith("<!DOCTYPE html>"))
69
+ self.assertTrue(d.rstrip().endswith("</html>"))
70
+ self.assertIn("have not been published", d)
71
+
72
+ def test_every_container_is_balanced(self):
73
+ d = _doc(_report(n_subtracks=2))
74
+ for tag in ("div", "table", "thead", "tbody", "tr", "th", "td"):
75
+ # `<th` also prefixes `<thead`, so match the tag name followed by a
76
+ # delimiter rather than by str.count on the opening bracket.
77
+ opens = len(re.findall(rf"<{tag}[\s>]", d))
78
+ self.assertEqual(opens, d.count(f"</{tag}>"), tag)
79
+
80
+
81
+ class SelfSizingTest(unittest.TestCase):
82
+ """The page was cut off partway down. This is why."""
83
+
84
+ def test_the_content_is_wrapped_in_the_element_the_script_measures(self):
85
+ d = _doc(_report())
86
+ self.assertEqual(1, d.count("id='invroot'"))
87
+ # the wrapper must close before the script, or the script is inside it
88
+ self.assertIn("</div><script>", d)
89
+
90
+ def test_it_measures_the_wrapper_not_the_document(self):
91
+ """documentElement.scrollHeight is bounded below by the iframe's own
92
+ viewport, so feeding it back into the iframe height grows without
93
+ bound. results.board_document() hit this first; do not reintroduce it.
94
+ """
95
+ d = _doc(_report())
96
+ self.assertIn("getElementById('invroot')", d)
97
+ self.assertNotIn("documentElement.scrollHeight", d)
98
+
99
+ def test_it_sets_width_from_the_host_tab(self):
100
+ """An iframe has a 300px intrinsic width and Gradio shrink-wraps it."""
101
+ d = _doc(_report())
102
+ self.assertIn("getElementById('lb-tab')", d)
103
+
104
+ def test_it_refits_on_load_and_resize(self):
105
+ d = _doc(_report())
106
+ for hook in ("load", "resize"):
107
+ self.assertIn(f"addEventListener('{hook}',fit)", d)
108
+
109
+
110
+ class ColumnsLineUpTest(unittest.TestCase):
111
+ def test_every_table_carries_the_same_colgroup(self):
112
+ d = _doc(_report(n_subtracks=2))
113
+ self.assertEqual(2, d.count("<table>"))
114
+ self.assertEqual(d.count("<table>"), d.count(invited.COLS))
115
+
116
+ def test_the_layout_is_fixed_so_widths_cannot_be_content_driven(self):
117
+ d = _doc(_report())
118
+ self.assertIn("table-layout:fixed", d)
119
+
120
+ def test_long_free_text_wraps_rather_than_widening_the_table(self):
121
+ """model_name is participant free text and reaches 80+ chars."""
122
+ d = _doc(_report())
123
+ self.assertIn("td.team,td.acct,td.model{white-space:normal", d)
124
+ self.assertIn("overflow-wrap:anywhere", d)
125
+
126
+
127
+ class ContentTest(unittest.TestCase):
128
+ def test_organizer_rows_are_filtered_by_their_note_field(self):
129
+ d = _doc(_report(n_subtracks=1, rows_per=2, note_rows=1))
130
+ self.assertNotIn("htranx", d)
131
+ self.assertIn("<b>2 seats</b>", d)
132
+
133
+ def test_the_team_count_is_read_from_the_file_not_recounted(self):
134
+ """Neither team names nor accounts reproduce the transitive merge."""
135
+ d = _doc(_report(n_subtracks=1, rows_per=2))
136
+ self.assertIn("5 teams", d)
137
+
138
+ def test_rows_are_ordered_by_rank(self):
139
+ rep = _report(n_subtracks=1, rows_per=3)
140
+ rep["subtracks"][0]["shortlist"].reverse()
141
+ d = _doc(rep)
142
+ pos = [d.index(f"Team 0{r}") for r in range(3)]
143
+ self.assertEqual(pos, sorted(pos))
144
+
145
+ def test_a_missing_score_or_param_count_renders_a_dash(self):
146
+ rep = _report(n_subtracks=1, rows_per=1)
147
+ rep["subtracks"][0]["shortlist"][0]["primary_score"] = None
148
+ rep["subtracks"][0]["shortlist"][0]["total_params"] = None
149
+ d = _doc(rep)
150
+ self.assertEqual(2, d.count("—"))
151
+
152
+ def test_a_sub_track_with_no_invited_teams_says_so(self):
153
+ rep = _report(n_subtracks=1, rows_per=0)
154
+ self.assertIn("No teams invited", _doc(rep))
155
+
156
+ def test_team_names_are_escaped(self):
157
+ rep = _report(n_subtracks=1, rows_per=1)
158
+ rep["subtracks"][0]["shortlist"][0]["team_name"] = "<script>x</script>"
159
+ d = _doc(rep)
160
+ self.assertNotIn("<script>x</script>", d)
161
+ self.assertIn("&lt;script&gt;", d)
162
+
163
+
164
+ if __name__ == "__main__":
165
+ unittest.main()