Repository navigation
Be 24 multiple professors - #96
Merged
Merged
Conversation
…rse and updated old docstring
…ors. And renamed TAs tab to Staff
…r the same course
Setup before would only show students view even to people who are professor in a course
…onents. letting a simultaneous prof and student use the app
PROFESSOR_WHITELIST is gone. Every login now writes User.role = STUDENT; PROFESSOR and TA exist only on CourseEnrollment, granted per class by an admin. ADMIN_WHITELIST is the only remaining global permission. Two routes read the global role as an authorization shortcut and would have silently granted a leftover PROFESSOR user access to every course: - GET /api/sessions/[sessionId] skipped the enrollment check entirely - POST /api/questions/[id]/answers let a global professor answer anywhere Both now require an enrollment like everyone else. The Socket.IO viewer count filtered on the cookie role, which is now always STUDENT — it would have counted the professor as a viewer of their own room. It reads the CourseEnrollment role recorded at session:join instead. DEV_ROLE no longer does anything, so drop it from the auth route, server.ts and dev-all.ts. The dev:all PROF/TA/STUDENT personas are now just labels for which tab is which; assign them roles by adding them to a classlist. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Create Lecture button, the /create-class page and POST /api/courses all gated on the global PROFESSOR role, which no longer exists. They now check ADMIN_WHITELIST. Professors are assigned to a class by an admin; they do not make one. The home page header was gated on having at least one course, so a brand-new admin would have landed on the student empty state with no way to create anything. It now shows for admins regardless. Also drops a leftover `role: "PROFESSOR"` from the creator upsert in POST /api/courses — global roles are always STUDENT now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds Classlist, and turns a Course row into a *room*: one professor's Q&A
space inside a classlist. Course gains classlistId and professorId; the room's
display label is derived from the professor's User.name rather than stored, so
it follows them when a placeholder name is replaced.
A room has exactly one professor. Prisma cannot express a partial unique
index, so the invariant is enforced in SQL:
CREATE UNIQUE INDEX ... ON "CourseEnrollment" ("courseId")
WHERE "role" = 'PROFESSOR'
The migration backfills existing data rather than leaving it orphaned: every
current course becomes a single-room classlist owned by its creator, extra
PROFESSOR enrollments left over from the co-professor model are demoted to TA,
and a course whose creator was never enrolled gets that enrollment created —
otherwise it would end up with no professor at all.
User.hasLoggedIn tracks whether User.name is real or a placeholder. Step 3b
needs it: an admin types a temporary name for a professor who has never signed
in, and Shibboleth replaces it on first login.
The admin bulk-delete routes and the test cleanups now clear Classlist too —
its createdById FK is RESTRICT, so deleting users failed without it. Those
routes also protected "professors" by the global User.role, which is always
STUDENT now; they check for a PROFESSOR enrollment instead.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
POST /api/classlists replaces POST /api/courses. It creates the classlist, then a room for the admin who made it and a room for every professor they named. The admin is PROFESSOR of their own room and TA on all the others; each other professor is PROFESSOR of exactly one room and has no row at all in the rest, so those rooms are invisible to them. A professor who also appears on the uploaded roster is deliberately left out of the student enrollments — otherwise the roster would hand them student access to every room the split exists to keep them out of. Room labels are the professor's surname alone, derived from User.name at read time rather than stored. Two Smiths on one classlist get a first initial, and two J. Smiths fall back to the UTORid (src/lib/roomLabel.ts, unit-tested). The create form now takes a UTORid and a name per professor instead of a textarea of UTORids. GET /api/users/lookup (admin-only) resolves each UTORid as it is typed: someone who has signed in has their real name filled in and locked, and someone who has not gets an amber note that the name is temporary until they sign in for the first time. Verified against the dev database: room count, one professor per room, the roster/TA overlap resolving to TA, professors having no access to each other's rooms, and the partial unique index rejecting a second professor. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds the endpoints the manage screen needs, all of them fanning changes out to every room so the rooms never drift apart: GET/PATCH/DELETE /api/classlists/[id] detail, rename, delete GET/POST/PUT/DELETE .../students roster, pooled and synced POST/DELETE .../professors add or remove a room Adding a professor builds their room seeded with the class's current roster and TAs; removing one tears their room down. A professor and a room are the same thing here, so neither can exist without the other. Removal refuses on the last professor and on a live room. Renaming fans out because every room carries its own copy of the code and semester for display — renaming only the classlist would leave rooms labelled with the old code. Roster reads pool across rooms and keep the strongest role, so a TA in one room is a TA on the class. Removing someone demotes a TA to student or drops a student — one or the other, never a demote followed by a delete that undoes it. Room owners are refused here and sent to the professors endpoint. deleteRooms() in classlistService centralises the manual teardown order that Course's lack of cascades requires. Verified over HTTP against a dev server: 24 checks covering creation, adding and removing professors, roster sync, TA demotion, rename fan-out, the refusal cases, and deletion. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Replaces CourseViewer and ProfCourseViewer with one ClassBrowser. The gallery now shows a card per class — TES101, not one card per room — with a badge when any of its rooms is live. Opening a class swaps the gallery for its rooms, behind a back link: one card per professor, green with a Join button when live, and the professor's own room offering Start Live Session / End instead. Only rooms the viewer is enrolled in are listed, so a professor's class opens onto exactly one room while a student's opens onto all of them. That filtering is the API's job, not the UI's — GET /api/classlists already returns only what the caller can reach. CourseViewer paired a course with its live session by matching course.name against session.course.name, which quietly broke as soon as two classes shared a name. The session now arrives attached to its room. ManageClassModal moves to the classlist endpoints. Its Staff tab becomes Rooms & TAs: the professor list is now a list of rooms showing who runs each, how many members it has, and whether it is live, with the same UTORid-plus-name entry the create form uses. Removing one says plainly that it deletes the room. Onboarding picked its walkthrough from the global role, which is always STUDENT now — a professor would have been shown the student one. It waits for the enrollment answer and branches on that instead. Verified across three signed-in dev servers: 25 checks that an admin sees every room (professor of theirs, TA of the rest), a professor sees only their own and is refused the others' sessions, roles and management, and a student sees all rooms as a student. Plus 8 render tests for the drill-down itself. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Six things the rest of the app still assumed rooms were shared:
- A socket switching or leaving a session left `session:<id>:instructors`
behind. A professor moving to a room where they are a student would have kept
receiving the old room's INSTRUCTOR_ONLY questions.
- POST /api/courses/[id]/students accepted role: "PROFESSOR", which now trips
the partial unique index and surfaces as a 500. It refuses with an
explanation and points at the classlist endpoint that creates a room.
- Its DELETE still carried the co-professor guards ("cannot remove the last
professor"). A room's professor is the room, so demoting them is refused
outright rather than conditionally.
- PATCH/DELETE /api/courses/[courseId] are gone. Renaming one room desynced it
from its classlist's code, and deleting one stranded the classlist. Both live
at classlist level now, and nothing called these any more.
- A join code let a professor enrol as a student in a colleague's room on the
same class — walking straight around the isolation. joinSession refuses when
the joiner already runs a room on that classlist.
- The admin room-delete and user-delete routes left classlists behind with no
rooms: invisible on the home page, and a RESTRICT foreign key that blocks
deleting the user who created them. Both clean up what they empty.
stress-tests/seed.ts also built a room with no classlist and no professor set,
and gave its professor the global PROFESSOR role that no longer means anything.
Verified against three signed-in dev servers: 13 checks covering each refusal
and that a professor can still manage TAs on their own room.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every room on a class carries the same code, so the dashboard's course table listed N identical "TES101" rows with no way to tell them apart. Rows now show whose room each is, search matches the professor's name or UTORid, and the tab and delete copy say "rooms". The stats bar counts classlists separately from rooms, which is the number that differs when a class has several professors. FEATURES.md and the admin guide described professor-created courses with co-professors sharing one room. They now describe classlists, per-professor rooms, who can see what, and where each kind of management lives — the class gear icon for admins, Manage TAs inside a session for a room's professor. Also clears comments still referring to co-professors and the global role. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The preview table cut off at four rows and said "Showing 4 of 5 students" with no way to see the rest — an admin could not check that the roster they were about to upload was actually right. It now pages four at a time with prev/next arrows, a position readout, and a page counter. Short last pages are padded with spacer rows so the card keeps its height instead of jumping, and the controls are hidden entirely when everyone already fits. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The name an admin typed for a professor was being written onto that person's User record as a placeholder, then overwritten with their real name the first time they signed in — so the label an admin chose could change under them, and the form had to warn about it. That is backwards: the name is the room's, not the professor's. Course.name now holds it. It is typed at creation, never rewritten by a login, survives a class rename, and only an admin changes it — via a new PATCH /api/classlists/[id]/rooms, or by clicking the name in the Rooms tab. Nothing branches on whether someone has signed in before, so the machinery that existed to decide that is gone: User.hasLoggedIn, GET /api/users/lookup, the autofill, the amber "temporary name" warning, and the surname disambiguation that silently rewrote two Smiths into initials. Two rooms may now share a name if an admin gives them one; that is the admin's call to make. upsertPersonByUtorid no longer writes names at all. A new user is named after their UTORid until Shibboleth gives us the real one. Fixes a bug this surfaced: adding a professor to an existing class filtered the creating admin out of the new room's TA list, because the filter that keeps sibling professors out did not exempt them. The admin was left with no enrollment, so the room they had just created was invisible on their own home page. Verified with 80 checks across four probes against live dev servers. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The header subtitle and the empty state both announced an empty list, one right above the other. The empty state already says it, with more room to explain what to do about it, so the subtitle now only appears when there are classes to describe. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The roster's UTORids were devstu2..devstu5, which read like dev:all personas — easy to paste into the professor boxes by mistake, and the class then gets built with students running the rooms. They are rosterstuN now, so nothing on the roster can be confused for someone who should be teaching. Both student personas are on the roster, so the student tabs actually see the class, and .env gains a step-by-step walkthrough: which port is whom, what to type at each field, and what each tab should show at every stage. Verified by running that walkthrough end to end against six signed-in dev servers — 37 checks covering creation, isolation, going live, renaming a room, adding and removing a professor, and the refusals. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The CSV listed only students, so there was nothing in it to tell you which UTORids to type into the professor boxes — and the roster's own names were the nearest thing to hand, which is how two students ended up running rooms twice in a row. Its last three rows are now devprof2, devprof3 and devta, matching ports 3001 / 3002 / 3003. The bottom of the file is exactly what goes into the create form. Having them on the student roster is also the case most worth testing: a professor who appears on it must not be handed student access to a colleague's room, and a TA on it must come out a TA. The .env walkthrough now calls both out as things to check in step 2. Verified against six signed-in dev servers running the documented steps — 43 checks, including that each professor's only row is PROFESSOR in their own room, that devta is a TA in all three, and that the manage screen lists 5 students rather than all 8 roster rows. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A room's professor and its TAs teach the same lecture off the same deck, so they now share control of it. `slide:change` accepts either; uploading still accepts only the professor, so a TA can move the slides everyone sees but can never swap the file out from under them. requireSocketInstructor enforced PROFESSOR while the `:instructors` socket room it shares a word with contains professors *and* TAs — reading it as either one was a standing invitation to widen access by accident. It is gone, replaced by requireSocketProfessor and requireSocketProfessorOrTA. Both call sites had to be revisited to compile, which is the point: answer-mode stays the professor's, slide movement does not. The viewer's two-state sync toggle becomes three modes: following track the shared page — the default for students and TAs browsing move around privately, room untouched presenting moving the page moves it for the whole room Present takes the deck and pulls the room to the presenter's page so nobody has to guess where it went, and a presenter accepts another presenter's move rather than fighting it, so two of them converge instead of drifting. The professor still arrives presenting, and now has a way to stop — which they need, since a TA may be the one driving. Students are unchanged except in wording: "Following Professor" is no longer always true, so it reads "Following Live". Covered by 9 integration tests over the two guards, including that a TA of a neighbouring room on the same class gets nothing here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Replaces the concurrent-presenter model with exclusive control, which is what a lecture actually looks like: one person has the deck, and a TA who wants it takes it. A professor or TA who is not driving sees exactly the bar a student sees — Following / Browse Freely — plus a Control Slides button. Pressing it claims the deck and pulls the room to the page they were already on, so taking over while reading ahead brings everyone along instead of stranding them. The displaced holder, professor included, drops to that same student bar with the same button back, so control passes back and forth with no release step. Control lives in Redis per session and is unclaimed by default, resolving to the room's professor until someone presses the button — a professor who starts a lecture and never touches it stays in charge with no extra write. slide:change now checks it: being on staff is no longer enough, because a page change from someone who is not driving would yank the deck out from under whoever is. The logic moved to lib/slideControl.ts rather than staying private to a socket handler, which is what makes it testable — 5 new tests cover the professor fallback, takeover displacing rather than queueing, and a room with no professor having no holder at all. performSessionEnd cleared no Redis state, so an ended lecture kept its slide position and answer mode for 24 hours. It clears all three now, which also gives releaseSlideControl a caller outside the tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Classlists have profs (each get their own room admin can join any as TA or their own as prof, students can join any and TAs can join any other profs (not admin) can only join their class.
Ranks are per class (prof ta student).
Admin is now the only global rank.
During a lec slides act as a pass it around. Only prof uploads slides. Only one (TA or prof) controls slides at a time. But at any time a TA or prof can press "Control slides" and they get the UI to change slides while the prev person gets a student view (except they have the "Control slides" to take back control any time.