Naalya Handbook

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.

CheckPasses for
assertStreamReadAccessClass teacher of the stream, or scoped admin — subject teachers do NOT pass (they see their subject, not the stream)
assertGradeReadAccessClass teacher ∪ subject teacher ∪ scoped READ GRADE
assertCanGradeStreamSubjectClass 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

RouteReturnsAccess
GET meThe caller's classrooms: class-teacher streams + subject×stream pairsOwn profile
GET streams/:id/rosterThe stream's year-bound roster (via enrollmentService.listRoster)Stream read
GET streams/:id/subjectsStream's subjects — class teacher/admin see all; a subject teacher sees only their ownMixed
GET .../subjects/:subjectId/gradesThe grade matrix (gradeService.getMatrix)Grade read
PUT .../subjects/:subjectId/gradesBulk grade upsert — validates the subject is offered and every enrollment is on the roster, then gradeService.bulkUpsertTeachers only, audited
GET streams/:id/gradebookStudents × subjects with totals and percentage averageClass teacher / admin
GET enrollments/:id/gradesOne 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.

Where to go next

On this page