giuseppecuccunm commited on
Commit
b0c3cd2
·
verified ·
1 Parent(s): d88243e

Skip old-schema grading templates so Stats cannot crash

Browse files
Files changed (4) hide show
  1. README.md +2 -2
  2. src/stats.py +20 -0
  3. src/streamlit_app.py +68 -53
  4. tests/test_stats.py +35 -0
README.md CHANGED
@@ -174,7 +174,7 @@ See [`WORKFLOW.md`](WORKFLOW.md) for the full round-setup flow.
174
  | What | Where | Safe? |
175
  |------|--------|-------|
176
  | **en-us human scores (old 55-Q schema)** | `definitions/gradings/{MK,ML,RS}/*.json` in git — 80 terms × 3 graders (~4.8 MB) | **Yes — in repo** |
177
- | **sv-se / hr-hr human scores** | Were on HF dataset `TransLegal/grading-answers` (`{jurisdiction}/users/{username}_answers.parquet`); reports in `hf-spaces/gradings-analysis/` prove they existed (Mar–Apr 2026) | **Verify HF access** — not mirrored in `definitions/gradings/` |
178
  | **Streamlit app answers (any jurisdiction)** | HF **bucket** `TransLegal/grading-answers` when deployed (`*_answers.csv` plus `user_progress.csv`); hourly copy on the HF **dataset**; local mode under `{GRADING_DATA_DIR}/{jurisdiction}/users/` | Local only if you graded locally |
179
  | **AI template rows** | `grading_template.parquet` — regeneratable from `structured_analysis` | Not precious |
180
 
@@ -227,7 +227,7 @@ Hourly dataset versioning runs from **this GitHub repo**, not from the Space. Ad
227
  1. **Jurisdiction Discovery:** Local mode scans `GRADING_DATA_DIR`; HF mode scans the dataset for `grading_template.parquet`
228
  2. **User Accounts:** Each jurisdiction has separate user accounts (same username can exist in different jurisdictions)
229
  3. **Data Storage:** Local mode writes parquet and `user_progress.csv` under `GRADING_DATA_DIR/{jurisdiction}/users/`; HF mode writes sparse CSV grades and the navigation table to the storage bucket and keeps templates / `users.json` on the dataset
230
- 4. **Stats:** After sign-in, **Stats** in the sidebar opens a read-only dashboard of Pass / fail / ungraded cards for you, everyone, and each other grader, across every jurisdiction that has a template. Opening Stats does not change last-viewed / furthest-viewed. Local demo data: `uv run python seed_stats_demo.py` from this directory (new accounts use password `1234`; existing nonempty verdicts are kept).
231
 
232
  ## Deployment
233
 
 
174
  | What | Where | Safe? |
175
  |------|--------|-------|
176
  | **en-us human scores (old 55-Q schema)** | `definitions/gradings/{MK,ML,RS}/*.json` in git — 80 terms × 3 graders (~4.8 MB) | **Yes — in repo** |
177
+ | **sv-se / hr-hr human scores** | Historical files on HF dataset `TransLegal/grading-answers` (old category/subcategory schema). Removed from the live tree; recover from dataset commit `4ba3d14f048c1b76aefb93e7051a42a9eeff0a32`. Reports in `hf-spaces/gradings-analysis/` | **Git history on the dataset** |
178
  | **Streamlit app answers (any jurisdiction)** | HF **bucket** `TransLegal/grading-answers` when deployed (`*_answers.csv` plus `user_progress.csv`); hourly copy on the HF **dataset**; local mode under `{GRADING_DATA_DIR}/{jurisdiction}/users/` | Local only if you graded locally |
179
  | **AI template rows** | `grading_template.parquet` — regeneratable from `structured_analysis` | Not precious |
180
 
 
227
  1. **Jurisdiction Discovery:** Local mode scans `GRADING_DATA_DIR`; HF mode scans the dataset for `grading_template.parquet`
228
  2. **User Accounts:** Each jurisdiction has separate user accounts (same username can exist in different jurisdictions)
229
  3. **Data Storage:** Local mode writes parquet and `user_progress.csv` under `GRADING_DATA_DIR/{jurisdiction}/users/`; HF mode writes sparse CSV grades and the navigation table to the storage bucket and keeps templates / `users.json` on the dataset
230
+ 4. **Stats:** After sign-in, **Stats** in the sidebar opens a read-only dashboard of Pass / fail / ungraded cards for you, everyone, and each other grader, across every jurisdiction whose template matches the current swipe-deck schema. Older leftover templates are skipped (they do not crash the page). Opening Stats does not change last-viewed / furthest-viewed. Local demo data: `uv run python seed_stats_demo.py` from this directory (new accounts use password `1234`; existing nonempty verdicts are kept).
231
 
232
  ## Deployment
233
 
src/stats.py CHANGED
@@ -17,6 +17,26 @@ DEFAULT_FAIL_REASONS: tuple[str, ...] = (
17
  )
18
  OTHER_REASON = "Other"
19
  _ANSWERS_SUFFIXES = ("_answers.parquet", "_answers.csv")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
 
21
 
22
  @dataclass(frozen=True)
 
17
  )
18
  OTHER_REASON = "Other"
19
  _ANSWERS_SUFFIXES = ("_answers.parquet", "_answers.csv")
20
+ SWIPE_TEMPLATE_COLUMNS: tuple[str, ...] = (
21
+ "term",
22
+ "field_of_law",
23
+ "concept",
24
+ "section",
25
+ "section_index",
26
+ "question_id",
27
+ "group",
28
+ "part",
29
+ "question",
30
+ "question_index",
31
+ "answer",
32
+ "source_fields",
33
+ )
34
+
35
+
36
+ def missing_template_columns(columns: object) -> tuple[str, ...]:
37
+ """Return swipe-deck columns absent from a template frame."""
38
+ present = {str(name) for name in columns}
39
+ return tuple(name for name in SWIPE_TEMPLATE_COLUMNS if name not in present)
40
 
41
 
42
  @dataclass(frozen=True)
src/streamlit_app.py CHANGED
@@ -40,6 +40,7 @@ from stats import (
40
  counts_from_verdicts,
41
  dashboard_html,
42
  grades_to_verdicts,
 
43
  overlay_verdicts,
44
  sum_counts,
45
  username_from_answers_filename,
@@ -290,65 +291,66 @@ def get_jurisdiction_display_mapping(jurisdiction_codes):
290
  # ---------------------------------------------------------------------------
291
  # Template + user data I/O
292
  # ---------------------------------------------------------------------------
293
- TEMPLATE_COLUMNS = (
294
- "term",
295
- "field_of_law",
296
- "concept",
297
- "section",
298
- "section_index",
299
- "question_id",
300
- "group",
301
- "part",
302
- "question",
303
- "question_index",
304
- "answer",
305
- "source_fields",
306
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
307
 
308
 
309
  @st.cache_data
310
  def load_grading_template(jurisdiction):
311
  """Load the PASS/FAIL question grading template (local path or HF)."""
312
- available = discover_available_jurisdictions()
313
- if not jurisdiction or jurisdiction not in available:
 
314
  st.error("❌ **Error**: Jurisdiction is not set or invalid.")
315
- if available:
316
- st.error(f"Available jurisdictions: {', '.join(available)}")
317
- st.stop()
318
-
319
- if LOCAL_MODE:
320
- template_path = _local_template_path(jurisdiction)
321
- try:
322
- if not template_path.is_file():
323
- raise FileNotFoundError(f"Template not found: {template_path}")
324
- df = pd.read_parquet(template_path)
325
- except Exception as e:
326
- st.error(f"❌ **Error loading local grading template**: {str(e)}")
327
- st.error(f"Expected file at `{template_path}`")
328
- st.stop()
329
- else:
330
- try:
331
- file_path = hf_hub_download(
332
- repo_id=HF_DATASET_REPO,
333
- filename=f"{jurisdiction}/grading_template.parquet",
334
- repo_type="dataset",
335
- token=HF_TOKEN,
336
- )
337
- df = pd.read_parquet(file_path)
338
- except Exception as e:
339
  st.error(
340
- f"❌ **Error loading grading template from Hugging Face**: {str(e)}"
 
341
  )
342
- st.error(
343
- f"Expected `{jurisdiction}/grading_template.parquet` in {HF_DATASET_REPO}"
344
- )
345
- st.stop()
 
 
346
 
347
- missing = [c for c in TEMPLATE_COLUMNS if c not in df.columns]
348
  if missing:
349
  st.error(
350
  "❌ **Error**: grading template is missing columns "
351
- f"{missing}. Rebuild it with `build_grading_template.py`."
352
  )
353
  st.stop()
354
  return df
@@ -593,8 +595,13 @@ def _build_stats_snapshot(
593
  ]:
594
  per_user: dict[str, GradeCounts] = {}
595
  per_jurisdiction: dict[str, GradeCounts] = {}
 
596
  for code in jurisdictions:
597
- cards = get_cards(load_grading_template(code))
 
 
 
 
598
  total_cards = len(cards)
599
  for grader in list_grader_usernames(code):
600
  counts = _counts_for_user(
@@ -620,10 +627,10 @@ def _build_stats_snapshot(
620
  ((name, counts) for name, counts in per_user.items() if name != username),
621
  key=lambda item: (-item[1].graded, item[0]),
622
  )
623
- names = get_jurisdiction_display_mapping(tuple(jurisdictions))
624
  jurisdiction_rows = [
625
  (code, names.get(code, code), per_jurisdiction.get(code, sum_counts()))
626
- for code in jurisdictions
627
  ]
628
  return everyone, you, others, jurisdiction_rows
629
 
@@ -657,6 +664,8 @@ def _render_stats_page(
657
  username: str, jurisdiction: str, jurisdictions: list[str]
658
  ) -> None:
659
  _render_stats_fabs()
 
 
660
  everyone, you, others, jurisdiction_rows = _build_stats_snapshot(
661
  username,
662
  jurisdiction,
@@ -674,6 +683,12 @@ def _render_stats_page(
674
  ),
675
  unsafe_allow_html=True,
676
  )
 
 
 
 
 
 
677
 
678
 
679
  def _progress_file_missing(exc: BaseException) -> bool:
@@ -1551,7 +1566,7 @@ def _try_register(username: str, password: str, jurisdiction: str) -> None:
1551
  # Login page
1552
  # ---------------------------------------------------------------------------
1553
  if not st.session_state.logged_in:
1554
- available_jurisdictions = discover_available_jurisdictions()
1555
  auto_user = os.getenv("GRADING_AUTO_LOGIN") if LOCAL_MODE else None
1556
  if auto_user and available_jurisdictions:
1557
  jurisdiction = st.session_state.jurisdiction or available_jurisdictions[0]
@@ -1621,7 +1636,7 @@ elif notice == "register":
1621
  username = st.session_state.username
1622
  jurisdiction = st.session_state.jurisdiction
1623
 
1624
- available_jurisdictions = discover_available_jurisdictions()
1625
  if not jurisdiction or jurisdiction not in available_jurisdictions:
1626
  st.error("❌ **Error**: Jurisdiction is not set. Please sign out and in again.")
1627
  if st.button("Sign out"):
@@ -1629,7 +1644,7 @@ if not jurisdiction or jurisdiction not in available_jurisdictions:
1629
  st.stop()
1630
 
1631
  if st.session_state.view == "stats":
1632
- _render_stats_page(username, jurisdiction, available_jurisdictions)
1633
  st.stop()
1634
 
1635
  st.markdown('<div class="grading-top-align"></div>', unsafe_allow_html=True)
 
40
  counts_from_verdicts,
41
  dashboard_html,
42
  grades_to_verdicts,
43
+ missing_template_columns,
44
  overlay_verdicts,
45
  sum_counts,
46
  username_from_answers_filename,
 
291
  # ---------------------------------------------------------------------------
292
  # Template + user data I/O
293
  # ---------------------------------------------------------------------------
294
+ @st.cache_data
295
+ def _read_template_df(jurisdiction: str) -> pd.DataFrame | None:
296
+ """Load a jurisdiction parquet with no schema check and no Streamlit stop."""
297
+ try:
298
+ if LOCAL_MODE:
299
+ template_path = _local_template_path(jurisdiction)
300
+ if not template_path.is_file():
301
+ return None
302
+ return pd.read_parquet(template_path)
303
+ file_path = hf_hub_download(
304
+ repo_id=HF_DATASET_REPO,
305
+ filename=f"{jurisdiction}/grading_template.parquet",
306
+ repo_type="dataset",
307
+ token=HF_TOKEN,
308
+ )
309
+ return pd.read_parquet(file_path)
310
+ except Exception:
311
+ return None
312
+
313
+
314
+ @st.cache_data
315
+ def discover_compatible_jurisdictions() -> tuple[str, ...]:
316
+ """Jurisdictions whose template matches the current swipe-deck columns."""
317
+ compatible: list[str] = []
318
+ for code in discover_available_jurisdictions():
319
+ frame = _read_template_df(code)
320
+ if frame is None:
321
+ continue
322
+ if missing_template_columns(frame.columns):
323
+ continue
324
+ compatible.append(code)
325
+ return tuple(compatible)
326
 
327
 
328
  @st.cache_data
329
  def load_grading_template(jurisdiction):
330
  """Load the PASS/FAIL question grading template (local path or HF)."""
331
+ compatible = discover_compatible_jurisdictions()
332
+ if not jurisdiction or jurisdiction not in compatible:
333
+ available = discover_available_jurisdictions()
334
  st.error("❌ **Error**: Jurisdiction is not set or invalid.")
335
+ if compatible:
336
+ st.error(f"Available jurisdictions: {', '.join(compatible)}")
337
+ elif available:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
338
  st.error(
339
+ "Found template files, but none match the current grading schema. "
340
+ "Rebuild them with `build_grading_template.py`."
341
  )
342
+ st.stop()
343
+
344
+ df = _read_template_df(jurisdiction)
345
+ if df is None:
346
+ st.error("❌ **Error**: Could not load the grading template.")
347
+ st.stop()
348
 
349
+ missing = missing_template_columns(df.columns)
350
  if missing:
351
  st.error(
352
  "❌ **Error**: grading template is missing columns "
353
+ f"{list(missing)}. Rebuild it with `build_grading_template.py`."
354
  )
355
  st.stop()
356
  return df
 
595
  ]:
596
  per_user: dict[str, GradeCounts] = {}
597
  per_jurisdiction: dict[str, GradeCounts] = {}
598
+ used: list[str] = []
599
  for code in jurisdictions:
600
+ frame = _read_template_df(code)
601
+ if frame is None or missing_template_columns(frame.columns):
602
+ continue
603
+ used.append(code)
604
+ cards = get_cards(frame)
605
  total_cards = len(cards)
606
  for grader in list_grader_usernames(code):
607
  counts = _counts_for_user(
 
627
  ((name, counts) for name, counts in per_user.items() if name != username),
628
  key=lambda item: (-item[1].graded, item[0]),
629
  )
630
+ names = get_jurisdiction_display_mapping(tuple(used))
631
  jurisdiction_rows = [
632
  (code, names.get(code, code), per_jurisdiction.get(code, sum_counts()))
633
+ for code in used
634
  ]
635
  return everyone, you, others, jurisdiction_rows
636
 
 
664
  username: str, jurisdiction: str, jurisdictions: list[str]
665
  ) -> None:
666
  _render_stats_fabs()
667
+ compatible = set(discover_compatible_jurisdictions())
668
+ skipped = [code for code in jurisdictions if code not in compatible]
669
  everyone, you, others, jurisdiction_rows = _build_stats_snapshot(
670
  username,
671
  jurisdiction,
 
683
  ),
684
  unsafe_allow_html=True,
685
  )
686
+ if skipped:
687
+ st.caption(
688
+ "Skipped templates that are not the current grading schema: "
689
+ + ", ".join(skipped)
690
+ + "."
691
+ )
692
 
693
 
694
  def _progress_file_missing(exc: BaseException) -> bool:
 
1566
  # Login page
1567
  # ---------------------------------------------------------------------------
1568
  if not st.session_state.logged_in:
1569
+ available_jurisdictions = list(discover_compatible_jurisdictions())
1570
  auto_user = os.getenv("GRADING_AUTO_LOGIN") if LOCAL_MODE else None
1571
  if auto_user and available_jurisdictions:
1572
  jurisdiction = st.session_state.jurisdiction or available_jurisdictions[0]
 
1636
  username = st.session_state.username
1637
  jurisdiction = st.session_state.jurisdiction
1638
 
1639
+ available_jurisdictions = list(discover_compatible_jurisdictions())
1640
  if not jurisdiction or jurisdiction not in available_jurisdictions:
1641
  st.error("❌ **Error**: Jurisdiction is not set. Please sign out and in again.")
1642
  if st.button("Sign out"):
 
1644
  st.stop()
1645
 
1646
  if st.session_state.view == "stats":
1647
+ _render_stats_page(username, jurisdiction, list(discover_available_jurisdictions()))
1648
  st.stop()
1649
 
1650
  st.markdown('<div class="grading-top-align"></div>', unsafe_allow_html=True)
tests/test_stats.py CHANGED
@@ -9,6 +9,7 @@ from stats import (
9
  dashboard_html,
10
  empty_counts,
11
  grades_to_verdicts,
 
12
  overlay_verdicts,
13
  percent,
14
  segment_widths,
@@ -17,6 +18,40 @@ from stats import (
17
  )
18
 
19
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  def test_empty_user_is_all_ungraded() -> None:
21
  counts = counts_from_verdicts({}, 10)
22
  assert counts.passed == 0
 
9
  dashboard_html,
10
  empty_counts,
11
  grades_to_verdicts,
12
+ missing_template_columns,
13
  overlay_verdicts,
14
  percent,
15
  segment_widths,
 
18
  )
19
 
20
 
21
+ def test_missing_template_columns_detects_old_schema() -> None:
22
+ old = pd.DataFrame(
23
+ columns=[
24
+ "term",
25
+ "category",
26
+ "category_index",
27
+ "subcategory",
28
+ "subcategory_index",
29
+ "question",
30
+ "answer",
31
+ ]
32
+ )
33
+ missing = missing_template_columns(old.columns)
34
+ assert "field_of_law" in missing
35
+ assert "question_id" in missing
36
+ current = pd.DataFrame(
37
+ columns=[
38
+ "term",
39
+ "field_of_law",
40
+ "concept",
41
+ "section",
42
+ "section_index",
43
+ "question_id",
44
+ "group",
45
+ "part",
46
+ "question",
47
+ "question_index",
48
+ "answer",
49
+ "source_fields",
50
+ ]
51
+ )
52
+ assert missing_template_columns(current.columns) == ()
53
+
54
+
55
  def test_empty_user_is_all_ungraded() -> None:
56
  counts = counts_from_verdicts({}, 10)
57
  assert counts.passed == 0