#!/usr/bin/env python3 """Generate roster.json from a class-register XLSX. Reads the first column (student IDs) and emits a JSON file the quiz app loads at startup. Names from the second column, if present, are kept in the JSON for human auditability but are NOT used for the gate. Usage: python deploy/build_roster.py [-o roster.json] The XLSX is expected to have a header row, then one row per student. Column 1 = student ID, column 2 = name (optional). """ from __future__ import annotations import argparse import json import sys from pathlib import Path def build(xlsx_path: Path, out_path: Path) -> int: try: import openpyxl except ImportError: print("openpyxl is required: pip install openpyxl", file=sys.stderr) return 2 wb = openpyxl.load_workbook(xlsx_path) ws = wb.worksheets[0] students = [] seen: set[str] = set() for row in ws.iter_rows(values_only=True): if not row: continue sid_raw = row[0] if sid_raw is None: continue sid = str(sid_raw).strip() if not sid or sid in {"学号", "Student ID", "ID"}: continue if sid.upper() in seen: continue seen.add(sid.upper()) name = "" if len(row) > 1 and row[1] is not None: name = str(row[1]).strip() students.append({"id": sid, "name": name}) payload = { "source": str(xlsx_path), "count": len(students), "students": students, } out_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2)) print(f"Wrote {len(students)} students to {out_path}") return 0 def main() -> int: p = argparse.ArgumentParser(description="Build roster.json for the quiz app.") p.add_argument("xlsx", type=Path, help="Path to attendance.xlsx") p.add_argument("-o", "--out", type=Path, default=Path("roster.json")) args = p.parse_args() return build(args.xlsx, args.out) if __name__ == "__main__": sys.exit(main())