Skip to content

feat: timetable import (one-off, by hand), optional subjects and the timetable endpoints - #42

Merged
ThorSPB merged 2 commits into
mainfrom
feat/5-timetable
Oct 14, 2026
Merged

ThorSPB merged 2 commits into
mainfrom
feat/5-timetable

Conversation

@ThorSPB

@ThorSPB ThorSPB commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Closes #5

What

The import: CampusPulse.Api import-timetable --overrides FILE [--parse-only | --dry-run] [--force] WORKBOOK.xlsx…, a CLI mode of the API, run by hand; on the Pi through deploy/pi/campuspulse-import-timetable (sandboxed, like the migrations; installed by hand, not run). Guide: docs/timetable-import.md.

  • Workbooks are recognised by their sheets (the overrides' sources), never by file name: a re-downloaded "Timetable (1).xlsx" replaces what the first download brought. It refuses (exit 1) a workbook of no known source or of two, two workbooks of one source in a run, and a source whose sheets were already imported under another id.
  • A re-import keeps study groups, subjects and offered slots while they're the same (natural keys), and retires groups and subjects the workbook no longer has, never deleting them. Students keep them, shown as noLongerOffered.
  • Impacts are counted before writing: groups no longer offered, chosen subjects no longer offered, picks cleared. Any of them, or a sheet the source lists that's missing, refuses the run (exit 2, nothing written) until --force.
  • --dry-run runs the same write in a transaction and rolls it back, so it validates exactly what a real run would. --parse-only needs no database.
  • A source is skipped when its file's SHA-256, the overrides' SHA-256 and SheetParser.Version match its last import. A test pins the version to what the parser reads from the test workbook.

The parser:

  • Day blocks are the days column A names. With unreadable names, exactly five blocks are read in order; any other count is an error.
  • Merged cells become slots for every subgroup they cover, and two-row cells are joined.
  • From the cell text: marks, teacher, room, and about 15 week phrasings including parity. Lists with commas or &, and 2h/12sapt, stay whole when offers are split.
  • Weeks that come to none skip the activity (warned).
  • Two marks on separate lines are two activities; a cell with marks it can't split warns.
  • Group-column cells without a mark become notes (warned). The overrides can give one a kind.

One-hour cells (his decision):

  • Partnered cells: a one-hour cell whose block's other row has a one-hour partner, in the same column or the same room, is read as 2 h in the row's parity. Top is odd by default; topRowWeeks: even flips a sheet, alternate: false turns it off, and cells changes any one cell. Each such cell is warned.
  • Stated parity: a cell that states its own parity keeps it.
  • Lone cells: one-hour cells with no partner are read as written (warned).

Abbreviations resolve to the sheet's courses, in this order:

  1. overrides aliases;
  2. a course's own - XY;
  3. initials (acronyms whole, a trailing number optional);
  4. a prefix;
  5. a warned guess.

Typos merge, but never when only a number or numeral differs.

Offers:

  • Side columns become offered slots per subject. Optional and facultative columns give optional subjects; master labs give pickSlot. [X] tags pick the programmes.
  • Grid subjects: a sheet's optionalSubjects turns a subject's grid classes into offers for the whole year (his decision for the optional courses whose groups aren't the timetable's).
  • Becoming optional: a subject that has grid classes and becomes optional warns. A sheet's requiredSubjects keeps one required when an optional column offers it (a required course whose labs are listed with the optional ones): its offers there are times to pick, as with pickSlot.

Overrides: a per-cell text guard (on a mismatch: warn, ignore), plus kind. Unused cells, aliases and optional subjects are warned, and so are sheets no source lists. Unknown keys are refused.

Model, migration AddTimetable (generated on ThorSPB):

  • semesters, weeks and non-teaching days (a break covering part of a week makes those days non-teaching);
  • programmes, study groups and subjects (both with source and retired_at), rooms;
  • slots and their groups, offered slots, notes;
  • import runs (source, file name, sheets).

Students' student_settings and student_subjects cascade from users; the cascade walk (EF and pg_constraint) asserts both.

Endpoints (all signed in):

  • GET /semesters/current, GET /timetable/programmes (offered groups only);
  • GET/PUT /me/settings (noLongerOffered, fromAnotherSemester);
  • GET /timetable/groups/{id}/choices, GET /me/subjects (noLongerOffered), PUT/DELETE /me/subjects/{id};
  • GET /me/timetable, GET /timetable/groups/{id}, GET /rooms, GET /rooms/{code}/timetable.

Retired groups and subjects can't be picked. Dates are bounded to 2000-2100. The time-zone database is required at startup, and today is Bucharest's date.

Testing

  • Synthetic workbooks built in the tests, every name, subject and room invented (TimetableWorkbook). Unit tests cover the parser rules above, the overrides validation, the calendar, the CLI and the version pin.
  • Postgres tests:
    • same-file skip; a renamed file not doubling;
    • a changed source id refused; changed overrides or --force re-importing; a dry run writing nothing;
    • a missing sheet refused, then retired with --force;
    • a renamed subject and a vanished group with students: counted, refused, then kept as no longer offered with --force, and offered again later;
    • two workbooks in one run sharing a group and a subject;
    • a group from an older semester flagged;
    • every endpoint, including Bucharest's date when UTC's is still the previous day, and a far-future date → 400.
  • CI on the final commit: see the checks.
  • Real 2026-27 files: --parse-only on ThorSPB with the private overrides, copies deleted after. Counts are in Import this semester's AC timetable and exam schedule (one-off) #5; no real text is in this repo or this PR.

Not done here

  • The exam import: the 2026-27 exam schedules aren't published yet.
  • Running the import on stage or prod: not done.

🤖 Generated with Claude Code

@ThorSPB
ThorSPB marked this pull request as ready for review October 14, 2026 09:39
…timetable endpoints

The faculty's semester timetables (workbooks shared with UPT accounts only) are read by
`CampusPulse.Api import-timetable`, run by hand with a private overrides file. Workbooks are
recognised by their sheets, not their file names. Re-imports keep study groups, subjects
and offers (retiring what disappears) and refuse, unless forced, when students would lose
something. One-hour cells alternating in a two-hour block are two hours every other week
(top row odd by default). Optional, facultative and pick-a-time subjects come with their
offered slots, which students choose in their settings.

Endpoints: the current semester, programmes and groups, a student's group and subjects,
their timetable and a group's (expanded into dates), rooms and a room's day.

Closes #5

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nal column offers it

A required course whose labs a faculty lists with the optional ones read as optional, so its
classes showed only to students who chose it. A sheet's requiredSubjects reads those offers as
times to pick (pickSlot); a name in both lists is refused, one that matches nothing is warned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ThorSPB
ThorSPB merged commit 5f57bc1 into main Oct 14, 2026
3 checks passed
@ThorSPB
ThorSPB deleted the feat/5-timetable branch October 14, 2026 10:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Import this semester's AC timetable and exam schedule (one-off)

1 participant