Logotipo Hedhog

Get Hedhog updates in your inbox

New releases, fresh recipes, and breaking changes — no spam.

Back to LMS modulesLMS Submodule

LMS Submodule

exams

Exam bank with a global question subject catalog, five question types (multiple choice, true/false, essay, fill-in-the-blank, matching), a standalone question bank, and the full learner attempt lifecycle.

Source path: libraries/lms/src/exam

Module file: exam.module.ts

Introduction

The Exams submodule is split across three controllers that together cover authoring and taking exams. `ExamController` (`/lms/exams`) owns exam definitions — title, code, scoring threshold, time limit, shuffle/answer-key-visibility flags, level, and status — plus a nested question-management surface for adding, editing, removing, and reordering each exam's questions. Five question types are supported: multiple_choice and true_false are auto-graded against marked-correct alternatives (true_false strictly requires exactly two), essay is stored ungraded for manual review, fill_blank is graded by case-insensitive exact match against one or more accepted answers, and matching is graded by exact left/right pair-set comparison. A small but easy-to-miss design choice: `GET /lms/exams/:id` returns `null` rather than a 404 when the exam doesn't exist, and several question-management endpoints (delete, reorder) are no-ops rather than errors when the target doesn't exist or the submitted id set doesn't match — callers should check response shape rather than relying purely on HTTP status.

`QuestionController` (`/lms/questions`) is a separate, flatter standalone question bank — questions created here are not linked to any exam at creation time, useful for building a reusable item bank before assembling exams, or for other parts of the platform that need ad-hoc graded questions outside the exam-attempt flow.

`ExamAttemptController` (also under `/lms/exams`) is the learner-facing half: starting an attempt, saving in-progress answers, and submitting for grading. `GET /:id/attempt` auto-finalizes an attempt if its time limit has silently elapsed, and resolves which `person` record the attempt belongs to via a fallback chain — explicit `studentId` override, then email-matched person, then (as a last resort) auto-creating a new person record for the calling user, which is a notable side effect worth knowing about before scripting against this endpoint with a fresh service account. Starting a new attempt is idempotent against an existing unexpired in-progress attempt, and submission publishes an `lms.exam.attempt.submitted` integration event carrying the final score and pass/fail outcome for downstream consumers (e.g. achievement-granting or enterprise license reporting) to react to.

HTTP Endpoints

19 endpoints

Paginated list of question subjects/categories.

Query
page, pageSize, search

Creates a question subject; rejects empty or case-insensitive duplicate names.

Body

FieldTypeRequiredNotes
namestringyes
descriptionstringno

Paginated exam list with per-exam average score.

Query
page, pageSize, search, status

Aggregate stats: total/published exams, total questions, overall average score.

Gets an exam by ID; returns null (not 404) if not found.

Params
id (int)

Lists the exam's questions in order, including type-specific alternatives/answers/pairs.

Params
id (int)

Adds a question to the exam; per-type validation enforced (objective needs ≥2 alternatives with ≥1 correct, true_false exactly 2, fill_blank ≥1 answer, matching ≥2 unique pairs).

Params
id (int)

Body

FieldTypeRequiredNotes
subjectIdnumberyes
statementstringyes
pointsnumberyesMin 1
questionType'multiple_choice' | 'true_false' | 'essay' | 'fill_blank' | 'matching'noDefault multiple_choice
explanationstringno
alternatives{ text, isCorrect }[]noMax 2000 chars per text
fillBlankAnswers{ answer, alternatives? }[]noMin size 1 if present
matchingPairs{ id, leftText, rightText }[]noMin size 2 if present

Updates a question (same validators as create, applied when fields/type change).

Params
id, questionId (int)

Removes a question and re-sequences remaining order (no-op if not found).

Params
id, questionId (int)

Reorders questions; returns {success:false} if the submitted ID set does not exactly match the exam's questions.

Params
id (int)

Body

FieldTypeRequiredNotes
questionIdsnumber[]yesArrayMinSize(1)

Creates an exam (exam_type fixed to "test").

Body

FieldTypeRequiredNotes
titlestringyesMax 255 chars
codestringnoMax 32 chars
minScorenumbernoPassing threshold on a base-10 scale, default 7
timeLimitnumbernoMinutes, default 60
shufflebooleanno
showAnswerKeyAfterFinishbooleanno
level'beginner' | 'intermediate' | 'advanced'noDefault beginner
status'draft' | 'published' | 'closed' | 'archived'noDefault draft
primaryColorstringnoMax 32 chars
secondaryColorstringnoMax 32 chars

Updates exam fields (all optional, same shape as create).

Params
id (int)

Hard-deletes the exam.

Params
id (int)

Lists the global standalone question bank (substring match on statement).

Query
search?, page?, limit? (default 50)

Creates a standalone question, not linked to any exam. Same per-type validation as exam questions.

Body

FieldTypeRequiredNotes
subjectIdnumberno
statementstringyes
explanationstringno
questionType'multiple_choice' | 'true_false' | 'essay' | 'fill_blank' | 'matching'noDefault multiple_choice
pointsnumbernoDefault 1
alternatives{ text, isCorrect }[]no
fillBlankAnswers{ answer, alternatives? }[]no
matchingPairs{ id, leftText, rightText }[]no

Current attempt state for the user; auto-finalizes an expired in-progress attempt; resolves/creates a person record if none exists.

Params
id (int)
Query
studentId?

Starts a new attempt, or returns an existing unexpired in-progress one. Rejects if the attempt quota is exhausted or the exam has no questions.

Params
id (int)

Body

FieldTypeRequiredNotes
studentIdnumbernoAdmin-initiated attempt for another student

Saves answers mid-attempt with per-type auto-grading where applicable.

Params
id, attemptId (int)

Body

FieldTypeRequiredNotes
answersSaveExamAttemptAnswerDto[]yes{ questionId, examOptionId?, answerText?, matchingPairs? }[]

Finalizes the attempt, computes the score, and publishes lms.exam.attempt.submitted. Idempotent if already completed.

Params
id, attemptId (int)

Body

FieldTypeRequiredNotes
answersSaveExamAttemptAnswerDto[]yes
forcebooleannoBypass the require-all-answered check

MCP Tools

Named tools callable by AI models via the Model Context Protocol (MCP).

  • lms.exams.list

    Lists exams with optional pagination and filters

  • lms.exams.stats

    Returns aggregate statistics for exams

  • lms.exams.get

    Returns a single exam by ID

  • lms.exams.create

    Creates a new exam

  • lms.exams.update

    Updates an existing exam

  • lms.exams.delete

    Removes an exam by ID

  • lms.exams.question-subjects.list

    Lists question subjects (topics) available for exam questions

  • lms.exams.question-subjects.create

    Creates a new question subject

  • lms.exams.questions.list

    Lists all questions for an exam

  • lms.exams.questions.create

    Creates a new question for an exam

  • lms.exams.questions.update

    Updates a question in an exam

  • lms.exams.questions.delete

    Removes a question from an exam

  • lms.exams.questions.reorder

    Reorders questions within an exam

  • lms.exams.attempt.get

    Returns the current attempt state for an exam for the authenticated user

  • lms.exams.attempt.start

    Starts a new exam attempt for the authenticated user

  • lms.exams.attempt.save-answers

    Saves answers for an in-progress exam attempt

  • lms.exams.attempt.submit

    Submits an exam attempt for grading