Naalya Handbook
Feature Tour

CBT Exams

Computer-based testing — how subject teachers author exams, assign them to streams, and oversee submissions.

CBT — computer-based testing — is the deepest single feature in the staff hub. A subject teacher writes an exam, assigns it to the streams they teach, and oversees how it runs. Three different jobs, and the trick to understanding the feature is to see that they sit on two different navigation axes at once.

  • Subject teachers author, working subject-first down the My Subjects path.
  • Class teachers oversee, working class-first down the Classrooms path — read-only.

A dual-role teacher walks both. Get that split clear in your head and the rest of CBT falls into place, because almost every file below belongs to one axis or the other.

The mental model: an exam lives under a Subject

A CBT exam is owned by a Subject, not a class. To reach students it gets assigned to one or more Streams — a stream being one section of a class in a given term. So the same exam can run for several streams at once, and a class teacher sees it from the other end without ever owning it.

The lifecycle

Every exam moves through a small, strict status machine. You'll see these three words everywhere — on the badge, in the query layer, in the action menu:

draft → published → closed

  • draft — fully editable. Edit details, add and reorder questions, delete the whole thing.
  • published — locked content, live to assigned streams. The only forward move left is Close.
  • closed — done. Read-only; assignments can no longer be changed.

The status drives the UI directly. In the exam detail page the Actions dropdown only renders for draft or published, and which items appear depends on the status:

src/routes/.../my-subjects/$subjectId_.exams.$examId.tsx
const isDraft = exam.status === 'draft'
// draft → Edit details · Edit questions · Publish · Delete
// published → Close exam
{(isDraft || exam.status === 'published') && <DropdownMenu>{/* ... */}</DropdownMenu>}

The transitions themselves are one-line calls in the query layer — there's no clever client logic, the backend enforces the rules:

src/queries/cbt-examination/cbt-examination.query.ts
readonly publish = async (id: string) => this.exec(this.op.publishCbtExamination({ id }))
readonly close   = async (id: string) => this.exec(this.op.closeCbtExamination({ id }))

Authoring (the My Subjects path)

This is the subject teacher's home. The My Subjects section in the sidebar is derived client-side — there's no dedicated endpoint, it just pivots the teacher's classroom assignments by subject. Pick a subject and you land on its page, which lists that subject's exams in a data table.

The authoring routes
my-subjects/$subjectId.tsx                       # the subject's exam list + "Create exam"
my-subjects/$subjectId_.exams.$examId.tsx        # the exam workspace (Details · Questions · Assignments)
my-subjects/$subjectId_.exams.$examId_.edit.tsx  # the question builder

Creating an exam opens the CreateCbtExam surface (src/components/surfaces/create-cbt-exam.surface.tsx); editing its metadata later opens the EditCbtExamDetails side panel. That's the standard surfaces-and-panels split you met in Panels & Surfaces — a surface for a focused create flow, a side panel for an edit-in-place.

The question builder

The heart of authoring lives in src/components/cbt/question-builder/. An exam is a list of questions, and each question has a type that decides how it's answered and graded. Five types are supported today:

src/components/cbt/question-builder/question-draft.ts
export const QUESTION_TYPE_LABELS = {
  mcq_single: 'Single choice',   // pick one
  mcq_multi:  'Multiple choice', // pick several
  short_text: 'Short text',      // matched against accepted answers
  long_text:  'Long text',       // free response
  math:       'Math',            // canonical expression + tolerance
}

The builder edits drafts (QuestionDraft), not server records — you stage your work locally and save. The shape carries everything a question might need: a prompt, marks, an optional time limit, options for the MCQ types, and a config block for the answer-matching rules of short_text and math (acceptedAnswers, matchMode, canonicalExpression, tolerance). The question-card.tsx component renders one editable card; emptyDraft(type) seeds the right starter fields for the chosen type.

Questions are only editable while draft

Once an exam is published, content is frozen — no adding, editing, or reordering questions. That's why the "Edit questions" route is reachable only from a draft exam. Publish is the point of no return for content; assignments are the only thing you keep tuning afterward.

Assigning to streams

An exam with great questions still reaches nobody until it's assigned. The Assignments tab (src/components/cbt/cbt-exam-assignments.tsx) is where a teacher makes the exam available to a stream and sets the rules for that stream: an opens and closes window, plus a max attempts cap.

src/components/cbt/cbt-exam-assignments.tsx
const handleAssign = () =>
  openPanel(AssignCbtExamStream, { size: 'normal' }, { examinationId: examId, subjectId })
// each assignment row → Opens · Closes · Max attempts, grouped by class

Assignments are per-stream, so one exam can run on different schedules for different sections. Removing an assignment is a guarded destructive action — it pops the Alert surface first, because pulling a stream cuts its students off from the exam. And once the exam is closed, the whole tab goes read-only: no Assign button, no row actions.

Oversight (the Classrooms path)

Here's the second axis. A class teacher is responsible for a stream regardless of who teaches each subject in it — so they need to see every exam assigned to their stream, across all subjects, even ones they didn't author. That's the Classrooms path, and it is strictly read-only.

The oversight route
classrooms/$classId_.streams.$streamId_.exams.$examId.tsx

This is a deliberately separate route from the authoring workspace — a class teacher landing on the subject-teacher's page would get a back-link into an empty, owner-scoped list. To avoid duplicating the polished view, both routes render the same shared CbtExamPreview (src/components/cbt/cbt-exam-preview.tsx) — the Details and Questions tabs. The difference is the hero: the authoring route owns the Actions dropdown, the oversight route has none.

Same preview, different permissions

Read access is relationship-first: you may view an exam if you're its owner, a subject teacher of it, the class teacher of its stream, or an admin with the read permission. Authoring (publish, edit, delete) stays narrower — owner or subject-teacher only. The page never invents these rules; the backend grants the access and the route renders accordingly. See Permissions.

Where the pieces live

A quick map so you can find your way around:

ConcernWhere
Authoring routesroutes/.../my-subjects/
Oversight routeroutes/.../classrooms/$classId_.streams.$streamId_.exams.$examId.tsx
Question buildercomponents/cbt/question-builder/
Shared previewcomponents/cbt/cbt-exam-preview.tsx
Assignmentscomponents/cbt/cbt-exam-assignments.tsx
Data layerqueries/cbt-examination, cbt-question, cbt-exam-assignment
Surfaces / panelscreate-cbt-exam, edit-cbt-exam-details, assign-cbt-exam-stream

Where to go next

On this page