Classroom
The teacher workspace — relationship-authorized rosters, grade matrices, and gradebooks over one stream, with admin fallback on reads only.
The classroom module is the teacher-facing surface at classrooms/*: "show me my streams, their rosters, and their grades." It owns almost no data — it's a thin, relationship-authorized wrapper over enrollment rosters and the grade service. What it does own is an access model worth copying.
The access model — ClassroomAccessService
Routes carry @RequireUserType(STAFF) and deliberately no @RequirePermissions — authorization is the teaching relationship, checked in code, with a CASL fallback that follows one rule:
Reads may fall back to admin permissions — writes never do
A campus admin with scoped READ can see any stream's roster or gradebook. But entering grades requires being the class teacher or the subject teacher of that stream — there is no admin bypass on writes. The class docstring states this as the module's design rule.
| Check | Passes for |
|---|---|
assertStreamReadAccess | Class teacher of the stream, or scoped admin — subject teachers do NOT pass (they see their subject, not the stream) |
assertGradeReadAccess | Class teacher ∪ subject teacher ∪ scoped READ GRADE |
assertCanGradeStreamSubject | Class teacher or subject teacher only — the write gate |
Like grading, all relationship lookups resolve the StaffProfile id first — teaching joins never key on the JWT user id.
The endpoints
| Route | Returns | Access |
|---|---|---|
GET me | The caller's classrooms: class-teacher streams + subject×stream pairs | Own profile |
GET streams/:id/roster | The stream's year-bound roster (via enrollmentService.listRoster) | Stream read |
GET streams/:id/subjects | Stream's subjects — class teacher/admin see all; a subject teacher sees only their own | Mixed |
GET .../subjects/:subjectId/grades | The grade matrix (gradeService.getMatrix) | Grade read |
PUT .../subjects/:subjectId/grades | Bulk grade upsert — validates the subject is offered and every enrollment is on the roster, then gradeService.bulkUpsert | Teachers only, audited |
GET streams/:id/gradebook | Students × subjects with totals and percentage average | Class teacher / admin |
GET enrollments/:id/grades | One student's gradebook (needs the enrollment's stream) | Class teacher / admin |
Two footnotes that save debugging time: gradebook averages coerce Postgres numeric strings via Number() before summing (numerics arrive as strings), and remarks are deliberately absent from the gradebook — they belong to the report card, not the grade grid.