CBT Exams
The computer-based-testing domain — exam, questions, per-stream assignments, and attempts, with the exam window living on the stream join.
CBT (computer-based testing) lets staff author an exam under a subject, assign it to streams (each with its own availability window and attempt cap), and have students sit it online with auto-grading for objective questions and a write-through into the gradebook. It's the largest new domain in the API: four controllers and seven services under apps/server/src/app/cbt/, wired in cbt.module.ts, which imports AcademicYearModule (for the year gates) and GradingModule (for the gradebook sync).
The entities, and why they're shaped this way
cbt_examination is the root. It belongs to a subject (subjectId + ownerStaffId) and is year-scoped (campusAcademicYearId + termId, with campusId denormalised for filters). Notable columns: a unique reference slug (minted as SlugPrefix.CBT_EXAM), a status lifecycle, a timingMode, a derived totalMarks (always Σ of question marks — recomputed on every question change, stamped at publish, never set directly), an optional passMark, and a monitoring jsonb (recordVideo, recordAudio, trackTabSwitch, trackCopyPaste, trackFocus, requireFullscreen, faceCheck — all default true except faceCheck).
cbt_examination_x_stream is the assignment join, and it carries the scheduling: availableFrom / availableTo and maxAttempts (default 1), unique per (examinationId, streamId). This is the single most important modeling decision in the domain:
The window is per stream
An exam has no dates of its own. The same exam can open Monday morning for Stream A and Wednesday for Stream B, with different attempt caps. The student runtime reads the assignment's window and cap — never the exam — so anything you build that reasons about "when is this exam open" must join through cbt_examination_x_stream.
cbt_examination_question rows hang directly off the exam — there are no sections and no shared question bank; questions are exam-owned, ordered by position, each with type, prompt, marks, optional timeLimitSeconds, and a per-type config jsonb (accepted answers for short text, match mode, etc.). cbt_examination_question_option holds MCQ options, where isCorrect is literally the answer key.
cbt_attempt is one sitting: (examinationId, studentId, attemptNumber) unique, with status, startedAt/expiresAt, the provisional autoScore/maxAutoScore, and the teacher-owned finalScore/gradedAt/gradedById. cbt_attempt_answer upserts one row per question on the unique key (attemptId, questionId, schoolId) — the tenant column is deliberately inside the key so tenant-scoped upserts can match their ON CONFLICT target (a comment in the entity explains this; copy the pattern for any upserted child table).
The enums
| Enum | Values | Notes |
|---|---|---|
| Exam status | draft → published → closed | Only drafts are editable; see the lifecycle |
| Timing mode | whole_exam / per_question | whole_exam needs durationMinutes; per_question needs timeLimitSeconds on every question |
| Question types | mcq_single, mcq_multi, short_text, long_text, math | First three auto-grade; the last two are always manual |
| Attempt status | in_progress, submitted, expired, graded, voided | graded is only ever set by a teacher |
Everything above lives in dto/cbt.types.ts; answer payloads are { selectedOptionIds?, text?, expression? }.
Scope in one sentence each
- Tenant: all CBT entities are
@TenantScoped()— the usual automaticschool_idwall. - Year: the exam carries
campusAcademicYearId; attempts, answers, questions, and assignments scope through it — none carry their own year column. - Access: no
@RequirePermissionson CBT routes — authorization is relationship-based inCbtAccessService("teaching is authorization"), with campus-scopedCBT_EXAMpermission as the admin fallback.
Where to go next
Authoring and Access
Subject-scoped creation, the draft-publish-close lifecycle, stream assignment rules, and CbtAccessService.
Student Runtime and Grading
Attempt lifecycle, lazy expiry, auto-grading, teacher finalise, and the gradebook write-through.
Academic-Year Scoping
Why the exam carries campusAcademicYearId and what the year's lock blocks.
Classroom
The teacher workspace — relationship-authorized rosters, grade matrices, and gradebooks over one stream, with admin fallback on reads only.
Authoring & Access
Subject-scoped exam creation, the draft-publish-close lifecycle, per-stream assignment rules, and the relationship-based CbtAccessService.