Skip to content

feat: link timetable subjects to their Campus Virtual courses (#45) - #48

Merged
ThorSPB merged 2 commits into
mainfrom
feat/45-campus-virtual-links
Oct 18, 2026
Merged

ThorSPB merged 2 commits into
mainfrom
feat/45-campus-virtual-links

Conversation

@ThorSPB

@ThorSPB ThorSPB commented Oct 18, 2026

Copy link
Copy Markdown
Contributor

Closes #45

What

Each timetable subject can carry its Campus Virtual (cv.upt.ro) course, and every class links to it.

  • scripts/campus-virtual-courses.py (Python, standard library only) builds the mapping once per semester:
    • it searches the catalog with the read-only core_course_search_courses, for the shortname prefixes in the overrides' campusVirtual.search;
    • it matches each subject among the courses of its programme and year (the shortname's <CODE><YEAR> part, with campusVirtual.codes for programmes whose courses sit under more than one code), by name, by the sheet's abbreviation or by initials. A subject it can't place there gets another programme's course only under exactly its name (a shared course);
    • it rewrites the campusVirtual section of the private overrides file and keeps every other line and comment;
    • it prints the counts and lists what it left out: ambiguous (two courses fit equally, e.g. one per series) and unmatched. An entry marked hand: true is kept on the next run, to pick a course or say "none".
    • The token comes from MOODLE_TOKEN in the environment or an --env-file. It goes only into the body of an https POST and is never printed or written; a test checks both.
  • import-timetable --subjects-json FILE writes the subjects the import read (plus the section's input) for the script. No database is needed: the script can't parse YAML or run the parser itself.
  • The overrides validate campusVirtual: known programmes, years 1-6, ids above 0, no name listed twice, and unknown keys refused as everywhere.
  • The import stores the id in Subject.CampusVirtualCourseId (new nullable column subjects.campus_virtual_course_id; migration AddSubjectCampusVirtualCourse, made on ThorSPB).
    • It prints -- Campus Virtual: N of M subjects have a course with the ones that have none, and adds the count to the run's summary.
    • It warns about entries that name no subject of their programme's year (a renamed subject).
    • The subject lines of the diff carry cv:<id>, so --dry-run shows a changed link.
  • Docs: docs/timetable-import.md ("Campus Virtual courses", the overrides example) and the README's timetable section.

Why

#45: a student goes from a class in the timetable to its Moodle course in one tap. The mapping is made with a token where one is available. The import itself can't reach cv.upt.ro, because the Pi's egress guard allows only sign-in and push.

How I tested it

  • The script: 16 unittest cases against invented catalogs, added to CI as the "Script tests" step. They cover paging, prefix filtering, every match kind, series ties, cross-programme courses, hand entries, the file rewrite (comments and other sections kept, markers replaced), the dry run, exit codes, and the token staying out of output and URLs.
  • .NET:
    • parser tests: linking ignoring case, hand "none", the unmatched-entry warning, the printout, 8 bad sections refused;
    • --subjects-json output and its usage error;
    • a Postgres import test: the id is stored, and a changed mapping re-imports and updates it;
    • an endpoint test: campusVirtualUrl on /me/timetable, choices, /me/subjects (GET and PUT) and the room timetable.
  • Locally on ThorSPB: build with -warnaserror, dotnet format --verify-no-changes, the non-database tests, and the script tests.
  • By hand, with the real 2026-27 files (private, never in the repo):
    • the export, then the script against the live catalog (276 courses);
    • the result: 231 matched (17 of them to a shared course of another programme), 4 ambiguous, 16 unmatched;
    • with 8 hand links for clear cases, the import's parse-only reports 239 of 251 subjects linked, with no new warnings.

API contract

A new nullable string field, campusVirtualUrl (https://cv.upt.ro/course/view.php?id=<id>, or null), on:

  • OccurrenceResponse (/me/timetable, /timetable/groups/{id});
  • ChoiceSubjectResponse (/timetable/groups/{id}/choices);
  • StudentSubjectResponse (/me/subjects, PUT /me/subjects/{id});
  • RoomOccurrenceResponse (/rooms/{code}/timetable).

Nothing is removed. The id itself isn't exposed, so clients never build the URL.

Checklist

  • tests cover the change
  • a schema change comes with its migration
  • contract changes are listed above

🤖 Generated with Claude Code

ThorSPB and others added 2 commits October 18, 2026 12:31
Each subject can now carry its Campus Virtual (cv.upt.ro) course id, and every timetable response
that has a subject gives a ready campusVirtualUrl (null when unknown), so web and mobile link each
class without building URLs.

- scripts/campus-virtual-courses.py (stdlib): searches the catalog with the read-only
  core_course_search_courses, matches the subjects per programme-year shortname code, and rewrites
  the overrides' campusVirtual section; ambiguous and unmatched subjects are listed and left out,
  hand: true entries survive a re-run. The token is read from the environment or an env file, sent
  only in an https POST body, never printed or written.
- import-timetable --subjects-json FILE exports the subjects read, for the script.
- The overrides validate the campusVirtual section (programmes, years, ids, duplicates, unknown keys);
  the import stores the id (subjects.campus_virtual_course_id, nullable), prints which subjects have
  none, adds the count to the run summary and warns about entries naming no subject.
- Tests: synthetic catalogs for the script (run in CI), and parser, import and endpoint tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- W1: the export carries the overrides' SHA-256 and the script refuses a file that changed since, so a
  stale export can't undo hand edits; the summary lists the entries removed and changed.
- W2: names with different numbers (1/2, I/II) never match; spelling closeness counts only when every
  word of the subject's is in the course's; initials alone no longer match; an abbreviation scores 0.95
  (3+ letters for the sheet's), and against a course that fits by name it is ambiguous; the
  cross-programme fallback keeps to the same level and year. Every match under 1.0 is listed with its score.
- W3: the token goes only to https://cv.upt.ro.
- The file is replaced atomically with a .bak; stale markers anywhere are dropped and comments inside the
  section kept; subjects in two workbooks give one entry; --subjects-json isn't written when a sheet has
  errors; the import validates campusVirtual.search and codes.
- Tests: the token stays out of output when urlopen fails or returns non-JSON; a fixture the script writes
  is parsed by ImportOverrides; markers, end of file and duplicates; a removed entry or a hand entry
  without an id clears the course; refusals assert FormatException and its message; the W2 cases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ThorSPB
ThorSPB merged commit ec4d60a into main Oct 18, 2026
3 checks passed
@ThorSPB
ThorSPB deleted the feat/45-campus-virtual-links branch October 18, 2026 10:04
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.

Link timetable subjects to their Campus Virtual (Moodle) courses

1 participant