dynamic question type builder completion
This commit is contained in:
parent
9a17f0b3c4
commit
3d1b3ad9b8
|
|
@ -0,0 +1,13 @@
|
||||||
|
ALTER TABLE question_type_definitions
|
||||||
|
DROP COLUMN IF EXISTS stimulus_schema,
|
||||||
|
DROP COLUMN IF EXISTS response_schema;
|
||||||
|
|
||||||
|
ALTER TABLE questions
|
||||||
|
DROP COLUMN IF EXISTS dynamic_payload;
|
||||||
|
|
||||||
|
ALTER TABLE questions
|
||||||
|
DROP CONSTRAINT IF EXISTS questions_question_type_check;
|
||||||
|
|
||||||
|
ALTER TABLE questions
|
||||||
|
ADD CONSTRAINT questions_question_type_check
|
||||||
|
CHECK (question_type IN ('MCQ', 'TRUE_FALSE', 'SHORT_ANSWER', 'AUDIO'));
|
||||||
13
db/migrations/000058_dynamic_question_builder_runtime.up.sql
Normal file
13
db/migrations/000058_dynamic_question_builder_runtime.up.sql
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
ALTER TABLE questions
|
||||||
|
DROP CONSTRAINT IF EXISTS questions_question_type_check;
|
||||||
|
|
||||||
|
ALTER TABLE questions
|
||||||
|
ADD CONSTRAINT questions_question_type_check
|
||||||
|
CHECK (question_type IN ('MCQ', 'TRUE_FALSE', 'SHORT_ANSWER', 'AUDIO', 'DYNAMIC'));
|
||||||
|
|
||||||
|
ALTER TABLE questions
|
||||||
|
ADD COLUMN IF NOT EXISTS dynamic_payload JSONB NULL;
|
||||||
|
|
||||||
|
ALTER TABLE question_type_definitions
|
||||||
|
ADD COLUMN IF NOT EXISTS stimulus_schema JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||||
|
ADD COLUMN IF NOT EXISTS response_schema JSONB NOT NULL DEFAULT '[]'::jsonb;
|
||||||
|
|
@ -16,6 +16,7 @@ SELECT
|
||||||
qsi.display_order,
|
qsi.display_order,
|
||||||
q.question_text,
|
q.question_text,
|
||||||
q.question_type,
|
q.question_type,
|
||||||
|
q.dynamic_payload,
|
||||||
q.difficulty_level,
|
q.difficulty_level,
|
||||||
q.points,
|
q.points,
|
||||||
q.explanation,
|
q.explanation,
|
||||||
|
|
@ -41,6 +42,7 @@ SELECT
|
||||||
qsi.display_order,
|
qsi.display_order,
|
||||||
q.question_text,
|
q.question_text,
|
||||||
q.question_type,
|
q.question_type,
|
||||||
|
q.dynamic_payload,
|
||||||
q.difficulty_level,
|
q.difficulty_level,
|
||||||
q.points,
|
q.points,
|
||||||
q.explanation,
|
q.explanation,
|
||||||
|
|
@ -68,6 +70,7 @@ SELECT
|
||||||
qsi.display_order,
|
qsi.display_order,
|
||||||
q.question_text,
|
q.question_text,
|
||||||
q.question_type,
|
q.question_type,
|
||||||
|
q.dynamic_payload,
|
||||||
q.difficulty_level,
|
q.difficulty_level,
|
||||||
q.points,
|
q.points,
|
||||||
q.explanation,
|
q.explanation,
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,10 @@ INSERT INTO questions (
|
||||||
voice_prompt,
|
voice_prompt,
|
||||||
sample_answer_voice_prompt,
|
sample_answer_voice_prompt,
|
||||||
image_url,
|
image_url,
|
||||||
status
|
status,
|
||||||
|
dynamic_payload
|
||||||
)
|
)
|
||||||
VALUES ($1, $2, $3, COALESCE($4, 1), $5, $6, $7, $8, $9, COALESCE($10, 'DRAFT'))
|
VALUES ($1, $2, $3, COALESCE($4, 1), $5, $6, $7, $8, $9, COALESCE($10, 'DRAFT'), $11::jsonb)
|
||||||
RETURNING *;
|
RETURNING *;
|
||||||
|
|
||||||
-- name: GetQuestionByID :one
|
-- name: GetQuestionByID :one
|
||||||
|
|
@ -62,8 +63,9 @@ SET
|
||||||
sample_answer_voice_prompt = COALESCE($8, sample_answer_voice_prompt),
|
sample_answer_voice_prompt = COALESCE($8, sample_answer_voice_prompt),
|
||||||
image_url = COALESCE($9, image_url),
|
image_url = COALESCE($9, image_url),
|
||||||
status = COALESCE($10, status),
|
status = COALESCE($10, status),
|
||||||
|
dynamic_payload = COALESCE($11::jsonb, dynamic_payload),
|
||||||
updated_at = CURRENT_TIMESTAMP
|
updated_at = CURRENT_TIMESTAMP
|
||||||
WHERE id = $11;
|
WHERE id = $12;
|
||||||
|
|
||||||
-- name: ArchiveQuestion :exec
|
-- name: ArchiveQuestion :exec
|
||||||
UPDATE questions
|
UPDATE questions
|
||||||
|
|
|
||||||
298
docs/docs.go
298
docs/docs.go
|
|
@ -4560,7 +4560,7 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"post": {
|
"post": {
|
||||||
"description": "Creates a new question with options (for MCQ/TRUE_FALSE) or short answers (for SHORT_ANSWER)",
|
"description": "Creates a new question with options (for MCQ/TRUE_FALSE) or short answers (for SHORT_ANSWER). Supports question_type_definition_id for dynamic builder-linked questions.",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
|
|
@ -4721,9 +4721,221 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v1/questions/type-definitions": {
|
||||||
|
"get": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"questions"
|
||||||
|
],
|
||||||
|
"summary": "List reusable question-type definitions",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Filter by status (ACTIVE, INACTIVE)",
|
||||||
|
"name": "status",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Include system seeded definitions",
|
||||||
|
"name": "include_system",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.Response"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"description": "Stores a reusable dynamic question-type definition for future question construction. Only runtime-mappable definitions are persisted.",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"questions"
|
||||||
|
],
|
||||||
|
"summary": "Create reusable question-type definition",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Question type definition payload",
|
||||||
|
"name": "body",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handlers.createQuestionTypeDefinitionReq"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"201": {
|
||||||
|
"description": "Created",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.Response"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.ErrorResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/questions/type-definitions/{id}": {
|
||||||
|
"get": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"questions"
|
||||||
|
],
|
||||||
|
"summary": "Get reusable question-type definition by id",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Question type definition id",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.Response"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.ErrorResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Not Found",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"put": {
|
||||||
|
"description": "Updates a reusable dynamic question-type definition. Updated definitions must remain runtime-mappable.",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"questions"
|
||||||
|
],
|
||||||
|
"summary": "Update reusable question-type definition",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Question type definition id",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Update question type definition payload",
|
||||||
|
"name": "body",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handlers.updateQuestionTypeDefinitionReq"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.Response"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.ErrorResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"questions"
|
||||||
|
],
|
||||||
|
"summary": "Delete reusable question-type definition",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Question type definition id",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.Response"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.ErrorResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/v1/questions/validate-question-type-definition": {
|
"/api/v1/questions/validate-question-type-definition": {
|
||||||
"post": {
|
"post": {
|
||||||
"description": "Validates selected stimulus and response component kinds for temporary question-type definitions",
|
"description": "Validates selected stimulus and response component kinds for temporary question-type definitions (component-level validation only)",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
|
|
@ -4802,7 +5014,7 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"put": {
|
"put": {
|
||||||
"description": "Updates a question and optionally replaces its options/short answers",
|
"description": "Updates a question and optionally replaces its options/short answers. Supports question_type_definition_id for dynamic builder-linked questions.",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
|
|
@ -9452,6 +9664,10 @@ const docTemplate = `{
|
||||||
"questionType": {
|
"questionType": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"questionTypeDefinitionID": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
"sampleAnswerVoicePrompt": {
|
"sampleAnswerVoicePrompt": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|
@ -10702,8 +10918,7 @@ const docTemplate = `{
|
||||||
"handlers.createQuestionReq": {
|
"handlers.createQuestionReq": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
"question_text",
|
"question_text"
|
||||||
"question_type"
|
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"audio_correct_answer_text": {
|
"audio_correct_answer_text": {
|
||||||
|
|
@ -10731,13 +10946,10 @@ const docTemplate = `{
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"question_type": {
|
"question_type": {
|
||||||
"type": "string",
|
"type": "string"
|
||||||
"enum": [
|
},
|
||||||
"MCQ",
|
"question_type_definition_id": {
|
||||||
"TRUE_FALSE",
|
"type": "integer"
|
||||||
"SHORT_ANSWER",
|
|
||||||
"AUDIO"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"sample_answer_voice_prompt": {
|
"sample_answer_voice_prompt": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
|
@ -10812,6 +11024,39 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"handlers.createQuestionTypeDefinitionReq": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"display_name",
|
||||||
|
"key"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"description": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"display_name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"key": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"response_component_kinds": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"stimulus_component_kinds": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"handlers.initiateDirectPaymentReq": {
|
"handlers.initiateDirectPaymentReq": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
|
@ -11185,6 +11430,9 @@ const docTemplate = `{
|
||||||
"question_type": {
|
"question_type": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"question_type_definition_id": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
"sample_answer_voice_prompt": {
|
"sample_answer_voice_prompt": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|
@ -11237,6 +11485,32 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"handlers.updateQuestionTypeDefinitionReq": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"description": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"display_name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"response_component_kinds": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"stimulus_component_kinds": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"handlers.validateQuestionTypeDefinitionReq": {
|
"handlers.validateQuestionTypeDefinitionReq": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
|
||||||
|
|
@ -4552,7 +4552,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"post": {
|
"post": {
|
||||||
"description": "Creates a new question with options (for MCQ/TRUE_FALSE) or short answers (for SHORT_ANSWER)",
|
"description": "Creates a new question with options (for MCQ/TRUE_FALSE) or short answers (for SHORT_ANSWER). Supports question_type_definition_id for dynamic builder-linked questions.",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
|
|
@ -4713,9 +4713,221 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v1/questions/type-definitions": {
|
||||||
|
"get": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"questions"
|
||||||
|
],
|
||||||
|
"summary": "List reusable question-type definitions",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Filter by status (ACTIVE, INACTIVE)",
|
||||||
|
"name": "status",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Include system seeded definitions",
|
||||||
|
"name": "include_system",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.Response"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"description": "Stores a reusable dynamic question-type definition for future question construction. Only runtime-mappable definitions are persisted.",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"questions"
|
||||||
|
],
|
||||||
|
"summary": "Create reusable question-type definition",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Question type definition payload",
|
||||||
|
"name": "body",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handlers.createQuestionTypeDefinitionReq"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"201": {
|
||||||
|
"description": "Created",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.Response"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.ErrorResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/questions/type-definitions/{id}": {
|
||||||
|
"get": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"questions"
|
||||||
|
],
|
||||||
|
"summary": "Get reusable question-type definition by id",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Question type definition id",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.Response"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.ErrorResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Not Found",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"put": {
|
||||||
|
"description": "Updates a reusable dynamic question-type definition. Updated definitions must remain runtime-mappable.",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"questions"
|
||||||
|
],
|
||||||
|
"summary": "Update reusable question-type definition",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Question type definition id",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Update question type definition payload",
|
||||||
|
"name": "body",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handlers.updateQuestionTypeDefinitionReq"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.Response"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.ErrorResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"questions"
|
||||||
|
],
|
||||||
|
"summary": "Delete reusable question-type definition",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Question type definition id",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.Response"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.ErrorResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/v1/questions/validate-question-type-definition": {
|
"/api/v1/questions/validate-question-type-definition": {
|
||||||
"post": {
|
"post": {
|
||||||
"description": "Validates selected stimulus and response component kinds for temporary question-type definitions",
|
"description": "Validates selected stimulus and response component kinds for temporary question-type definitions (component-level validation only)",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
|
|
@ -4794,7 +5006,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"put": {
|
"put": {
|
||||||
"description": "Updates a question and optionally replaces its options/short answers",
|
"description": "Updates a question and optionally replaces its options/short answers. Supports question_type_definition_id for dynamic builder-linked questions.",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
|
|
@ -9444,6 +9656,10 @@
|
||||||
"questionType": {
|
"questionType": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"questionTypeDefinitionID": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
"sampleAnswerVoicePrompt": {
|
"sampleAnswerVoicePrompt": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|
@ -10694,8 +10910,7 @@
|
||||||
"handlers.createQuestionReq": {
|
"handlers.createQuestionReq": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
"question_text",
|
"question_text"
|
||||||
"question_type"
|
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"audio_correct_answer_text": {
|
"audio_correct_answer_text": {
|
||||||
|
|
@ -10723,13 +10938,10 @@
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"question_type": {
|
"question_type": {
|
||||||
"type": "string",
|
"type": "string"
|
||||||
"enum": [
|
},
|
||||||
"MCQ",
|
"question_type_definition_id": {
|
||||||
"TRUE_FALSE",
|
"type": "integer"
|
||||||
"SHORT_ANSWER",
|
|
||||||
"AUDIO"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"sample_answer_voice_prompt": {
|
"sample_answer_voice_prompt": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
|
@ -10804,6 +11016,39 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"handlers.createQuestionTypeDefinitionReq": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"display_name",
|
||||||
|
"key"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"description": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"display_name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"key": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"response_component_kinds": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"stimulus_component_kinds": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"handlers.initiateDirectPaymentReq": {
|
"handlers.initiateDirectPaymentReq": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
|
@ -11177,6 +11422,9 @@
|
||||||
"question_type": {
|
"question_type": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"question_type_definition_id": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
"sample_answer_voice_prompt": {
|
"sample_answer_voice_prompt": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|
@ -11229,6 +11477,32 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"handlers.updateQuestionTypeDefinitionReq": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"description": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"display_name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"response_component_kinds": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"stimulus_component_kinds": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"handlers.validateQuestionTypeDefinitionReq": {
|
"handlers.validateQuestionTypeDefinitionReq": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
|
||||||
|
|
@ -389,6 +389,9 @@ definitions:
|
||||||
type: string
|
type: string
|
||||||
questionType:
|
questionType:
|
||||||
type: string
|
type: string
|
||||||
|
questionTypeDefinitionID:
|
||||||
|
format: int64
|
||||||
|
type: integer
|
||||||
sampleAnswerVoicePrompt:
|
sampleAnswerVoicePrompt:
|
||||||
type: string
|
type: string
|
||||||
shortAnswers:
|
shortAnswers:
|
||||||
|
|
@ -1250,12 +1253,9 @@ definitions:
|
||||||
question_text:
|
question_text:
|
||||||
type: string
|
type: string
|
||||||
question_type:
|
question_type:
|
||||||
enum:
|
|
||||||
- MCQ
|
|
||||||
- TRUE_FALSE
|
|
||||||
- SHORT_ANSWER
|
|
||||||
- AUDIO
|
|
||||||
type: string
|
type: string
|
||||||
|
question_type_definition_id:
|
||||||
|
type: integer
|
||||||
sample_answer_voice_prompt:
|
sample_answer_voice_prompt:
|
||||||
type: string
|
type: string
|
||||||
short_answers:
|
short_answers:
|
||||||
|
|
@ -1270,7 +1270,6 @@ definitions:
|
||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- question_text
|
- question_text
|
||||||
- question_type
|
|
||||||
type: object
|
type: object
|
||||||
handlers.createQuestionSetReq:
|
handlers.createQuestionSetReq:
|
||||||
properties:
|
properties:
|
||||||
|
|
@ -1309,6 +1308,28 @@ definitions:
|
||||||
- set_type
|
- set_type
|
||||||
- title
|
- title
|
||||||
type: object
|
type: object
|
||||||
|
handlers.createQuestionTypeDefinitionReq:
|
||||||
|
properties:
|
||||||
|
description:
|
||||||
|
type: string
|
||||||
|
display_name:
|
||||||
|
type: string
|
||||||
|
key:
|
||||||
|
type: string
|
||||||
|
response_component_kinds:
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
stimulus_component_kinds:
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
required:
|
||||||
|
- display_name
|
||||||
|
- key
|
||||||
|
type: object
|
||||||
handlers.initiateDirectPaymentReq:
|
handlers.initiateDirectPaymentReq:
|
||||||
properties:
|
properties:
|
||||||
email:
|
email:
|
||||||
|
|
@ -1560,6 +1581,8 @@ definitions:
|
||||||
type: string
|
type: string
|
||||||
question_type:
|
question_type:
|
||||||
type: string
|
type: string
|
||||||
|
question_type_definition_id:
|
||||||
|
type: integer
|
||||||
sample_answer_voice_prompt:
|
sample_answer_voice_prompt:
|
||||||
type: string
|
type: string
|
||||||
short_answers:
|
short_answers:
|
||||||
|
|
@ -1594,6 +1617,23 @@ definitions:
|
||||||
title:
|
title:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
|
handlers.updateQuestionTypeDefinitionReq:
|
||||||
|
properties:
|
||||||
|
description:
|
||||||
|
type: string
|
||||||
|
display_name:
|
||||||
|
type: string
|
||||||
|
response_component_kinds:
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
stimulus_component_kinds:
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
type: object
|
||||||
handlers.validateQuestionTypeDefinitionReq:
|
handlers.validateQuestionTypeDefinitionReq:
|
||||||
properties:
|
properties:
|
||||||
response_component_kinds:
|
response_component_kinds:
|
||||||
|
|
@ -5025,7 +5065,8 @@ paths:
|
||||||
consumes:
|
consumes:
|
||||||
- application/json
|
- application/json
|
||||||
description: Creates a new question with options (for MCQ/TRUE_FALSE) or short
|
description: Creates a new question with options (for MCQ/TRUE_FALSE) or short
|
||||||
answers (for SHORT_ANSWER)
|
answers (for SHORT_ANSWER). Supports question_type_definition_id for dynamic
|
||||||
|
builder-linked questions.
|
||||||
parameters:
|
parameters:
|
||||||
- description: Create question payload
|
- description: Create question payload
|
||||||
in: body
|
in: body
|
||||||
|
|
@ -5107,7 +5148,8 @@ paths:
|
||||||
put:
|
put:
|
||||||
consumes:
|
consumes:
|
||||||
- application/json
|
- application/json
|
||||||
description: Updates a question and optionally replaces its options/short answers
|
description: Updates a question and optionally replaces its options/short answers.
|
||||||
|
Supports question_type_definition_id for dynamic builder-linked questions.
|
||||||
parameters:
|
parameters:
|
||||||
- description: Question ID
|
- description: Question ID
|
||||||
in: path
|
in: path
|
||||||
|
|
@ -5217,12 +5259,153 @@ paths:
|
||||||
summary: Search questions
|
summary: Search questions
|
||||||
tags:
|
tags:
|
||||||
- questions
|
- questions
|
||||||
|
/api/v1/questions/type-definitions:
|
||||||
|
get:
|
||||||
|
parameters:
|
||||||
|
- description: Filter by status (ACTIVE, INACTIVE)
|
||||||
|
in: query
|
||||||
|
name: status
|
||||||
|
type: string
|
||||||
|
- description: Include system seeded definitions
|
||||||
|
in: query
|
||||||
|
name: include_system
|
||||||
|
type: boolean
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/domain.Response'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/domain.ErrorResponse'
|
||||||
|
summary: List reusable question-type definitions
|
||||||
|
tags:
|
||||||
|
- questions
|
||||||
|
post:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Stores a reusable dynamic question-type definition for future question
|
||||||
|
construction. Only runtime-mappable definitions are persisted.
|
||||||
|
parameters:
|
||||||
|
- description: Question type definition payload
|
||||||
|
in: body
|
||||||
|
name: body
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/handlers.createQuestionTypeDefinitionReq'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"201":
|
||||||
|
description: Created
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/domain.Response'
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/domain.ErrorResponse'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/domain.ErrorResponse'
|
||||||
|
summary: Create reusable question-type definition
|
||||||
|
tags:
|
||||||
|
- questions
|
||||||
|
/api/v1/questions/type-definitions/{id}:
|
||||||
|
delete:
|
||||||
|
parameters:
|
||||||
|
- description: Question type definition id
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: integer
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/domain.Response'
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/domain.ErrorResponse'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/domain.ErrorResponse'
|
||||||
|
summary: Delete reusable question-type definition
|
||||||
|
tags:
|
||||||
|
- questions
|
||||||
|
get:
|
||||||
|
parameters:
|
||||||
|
- description: Question type definition id
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: integer
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/domain.Response'
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/domain.ErrorResponse'
|
||||||
|
"404":
|
||||||
|
description: Not Found
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/domain.ErrorResponse'
|
||||||
|
summary: Get reusable question-type definition by id
|
||||||
|
tags:
|
||||||
|
- questions
|
||||||
|
put:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Updates a reusable dynamic question-type definition. Updated definitions
|
||||||
|
must remain runtime-mappable.
|
||||||
|
parameters:
|
||||||
|
- description: Question type definition id
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: integer
|
||||||
|
- description: Update question type definition payload
|
||||||
|
in: body
|
||||||
|
name: body
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/handlers.updateQuestionTypeDefinitionReq'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/domain.Response'
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/domain.ErrorResponse'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/domain.ErrorResponse'
|
||||||
|
summary: Update reusable question-type definition
|
||||||
|
tags:
|
||||||
|
- questions
|
||||||
/api/v1/questions/validate-question-type-definition:
|
/api/v1/questions/validate-question-type-definition:
|
||||||
post:
|
post:
|
||||||
consumes:
|
consumes:
|
||||||
- application/json
|
- application/json
|
||||||
description: Validates selected stimulus and response component kinds for temporary
|
description: Validates selected stimulus and response component kinds for temporary
|
||||||
question-type definitions
|
question-type definitions (component-level validation only)
|
||||||
parameters:
|
parameters:
|
||||||
- description: Stimulus and response component kinds
|
- description: Stimulus and response component kinds
|
||||||
in: body
|
in: body
|
||||||
|
|
|
||||||
|
|
@ -261,6 +261,8 @@ type Question struct {
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
ImageUrl pgtype.Text `json:"image_url"`
|
ImageUrl pgtype.Text `json:"image_url"`
|
||||||
|
QuestionTypeDefinitionID pgtype.Int8 `json:"question_type_definition_id"`
|
||||||
|
DynamicPayload []byte `json:"dynamic_payload"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type QuestionAudioAnswer struct {
|
type QuestionAudioAnswer struct {
|
||||||
|
|
@ -322,6 +324,21 @@ type QuestionShortAnswer struct {
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type QuestionTypeDefinition struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Key string `json:"key"`
|
||||||
|
DisplayName string `json:"display_name"`
|
||||||
|
Description pgtype.Text `json:"description"`
|
||||||
|
StimulusComponentKinds []string `json:"stimulus_component_kinds"`
|
||||||
|
ResponseComponentKinds []string `json:"response_component_kinds"`
|
||||||
|
IsSystem bool `json:"is_system"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
|
StimulusSchema []byte `json:"stimulus_schema"`
|
||||||
|
ResponseSchema []byte `json:"response_schema"`
|
||||||
|
}
|
||||||
|
|
||||||
type Rating struct {
|
type Rating struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
UserID int64 `json:"user_id"`
|
UserID int64 `json:"user_id"`
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,7 @@ SELECT
|
||||||
qsi.display_order,
|
qsi.display_order,
|
||||||
q.question_text,
|
q.question_text,
|
||||||
q.question_type,
|
q.question_type,
|
||||||
|
q.dynamic_payload,
|
||||||
q.difficulty_level,
|
q.difficulty_level,
|
||||||
q.points,
|
q.points,
|
||||||
q.explanation,
|
q.explanation,
|
||||||
|
|
@ -87,6 +88,7 @@ type GetPublishedQuestionsInSetRow struct {
|
||||||
DisplayOrder int32 `json:"display_order"`
|
DisplayOrder int32 `json:"display_order"`
|
||||||
QuestionText string `json:"question_text"`
|
QuestionText string `json:"question_text"`
|
||||||
QuestionType string `json:"question_type"`
|
QuestionType string `json:"question_type"`
|
||||||
|
DynamicPayload []byte `json:"dynamic_payload"`
|
||||||
DifficultyLevel pgtype.Text `json:"difficulty_level"`
|
DifficultyLevel pgtype.Text `json:"difficulty_level"`
|
||||||
Points int32 `json:"points"`
|
Points int32 `json:"points"`
|
||||||
Explanation pgtype.Text `json:"explanation"`
|
Explanation pgtype.Text `json:"explanation"`
|
||||||
|
|
@ -113,6 +115,7 @@ func (q *Queries) GetPublishedQuestionsInSet(ctx context.Context, setID int64) (
|
||||||
&i.DisplayOrder,
|
&i.DisplayOrder,
|
||||||
&i.QuestionText,
|
&i.QuestionText,
|
||||||
&i.QuestionType,
|
&i.QuestionType,
|
||||||
|
&i.DynamicPayload,
|
||||||
&i.DifficultyLevel,
|
&i.DifficultyLevel,
|
||||||
&i.Points,
|
&i.Points,
|
||||||
&i.Explanation,
|
&i.Explanation,
|
||||||
|
|
@ -140,6 +143,7 @@ SELECT
|
||||||
qsi.display_order,
|
qsi.display_order,
|
||||||
q.question_text,
|
q.question_text,
|
||||||
q.question_type,
|
q.question_type,
|
||||||
|
q.dynamic_payload,
|
||||||
q.difficulty_level,
|
q.difficulty_level,
|
||||||
q.points,
|
q.points,
|
||||||
q.explanation,
|
q.explanation,
|
||||||
|
|
@ -164,6 +168,7 @@ type GetQuestionSetItemsRow struct {
|
||||||
DisplayOrder int32 `json:"display_order"`
|
DisplayOrder int32 `json:"display_order"`
|
||||||
QuestionText string `json:"question_text"`
|
QuestionText string `json:"question_text"`
|
||||||
QuestionType string `json:"question_type"`
|
QuestionType string `json:"question_type"`
|
||||||
|
DynamicPayload []byte `json:"dynamic_payload"`
|
||||||
DifficultyLevel pgtype.Text `json:"difficulty_level"`
|
DifficultyLevel pgtype.Text `json:"difficulty_level"`
|
||||||
Points int32 `json:"points"`
|
Points int32 `json:"points"`
|
||||||
Explanation pgtype.Text `json:"explanation"`
|
Explanation pgtype.Text `json:"explanation"`
|
||||||
|
|
@ -191,6 +196,7 @@ func (q *Queries) GetQuestionSetItems(ctx context.Context, setID int64) ([]GetQu
|
||||||
&i.DisplayOrder,
|
&i.DisplayOrder,
|
||||||
&i.QuestionText,
|
&i.QuestionText,
|
||||||
&i.QuestionType,
|
&i.QuestionType,
|
||||||
|
&i.DynamicPayload,
|
||||||
&i.DifficultyLevel,
|
&i.DifficultyLevel,
|
||||||
&i.Points,
|
&i.Points,
|
||||||
&i.Explanation,
|
&i.Explanation,
|
||||||
|
|
@ -220,6 +226,7 @@ SELECT
|
||||||
qsi.display_order,
|
qsi.display_order,
|
||||||
q.question_text,
|
q.question_text,
|
||||||
q.question_type,
|
q.question_type,
|
||||||
|
q.dynamic_payload,
|
||||||
q.difficulty_level,
|
q.difficulty_level,
|
||||||
q.points,
|
q.points,
|
||||||
q.explanation,
|
q.explanation,
|
||||||
|
|
@ -255,6 +262,7 @@ type GetQuestionSetItemsPaginatedRow struct {
|
||||||
DisplayOrder int32 `json:"display_order"`
|
DisplayOrder int32 `json:"display_order"`
|
||||||
QuestionText string `json:"question_text"`
|
QuestionText string `json:"question_text"`
|
||||||
QuestionType string `json:"question_type"`
|
QuestionType string `json:"question_type"`
|
||||||
|
DynamicPayload []byte `json:"dynamic_payload"`
|
||||||
DifficultyLevel pgtype.Text `json:"difficulty_level"`
|
DifficultyLevel pgtype.Text `json:"difficulty_level"`
|
||||||
Points int32 `json:"points"`
|
Points int32 `json:"points"`
|
||||||
Explanation pgtype.Text `json:"explanation"`
|
Explanation pgtype.Text `json:"explanation"`
|
||||||
|
|
@ -288,6 +296,7 @@ func (q *Queries) GetQuestionSetItemsPaginated(ctx context.Context, arg GetQuest
|
||||||
&i.DisplayOrder,
|
&i.DisplayOrder,
|
||||||
&i.QuestionText,
|
&i.QuestionText,
|
||||||
&i.QuestionType,
|
&i.QuestionType,
|
||||||
|
&i.DynamicPayload,
|
||||||
&i.DifficultyLevel,
|
&i.DifficultyLevel,
|
||||||
&i.Points,
|
&i.Points,
|
||||||
&i.Explanation,
|
&i.Explanation,
|
||||||
|
|
|
||||||
|
|
@ -33,10 +33,11 @@ INSERT INTO questions (
|
||||||
voice_prompt,
|
voice_prompt,
|
||||||
sample_answer_voice_prompt,
|
sample_answer_voice_prompt,
|
||||||
image_url,
|
image_url,
|
||||||
status
|
status,
|
||||||
|
dynamic_payload
|
||||||
)
|
)
|
||||||
VALUES ($1, $2, $3, COALESCE($4, 1), $5, $6, $7, $8, $9, COALESCE($10, 'DRAFT'))
|
VALUES ($1, $2, $3, COALESCE($4, 1), $5, $6, $7, $8, $9, COALESCE($10, 'DRAFT'), $11::jsonb)
|
||||||
RETURNING id, question_text, question_type, difficulty_level, points, explanation, tips, voice_prompt, sample_answer_voice_prompt, status, created_at, updated_at, image_url
|
RETURNING id, question_text, question_type, difficulty_level, points, explanation, tips, voice_prompt, sample_answer_voice_prompt, status, created_at, updated_at, image_url, question_type_definition_id, dynamic_payload
|
||||||
`
|
`
|
||||||
|
|
||||||
type CreateQuestionParams struct {
|
type CreateQuestionParams struct {
|
||||||
|
|
@ -50,6 +51,7 @@ type CreateQuestionParams struct {
|
||||||
SampleAnswerVoicePrompt pgtype.Text `json:"sample_answer_voice_prompt"`
|
SampleAnswerVoicePrompt pgtype.Text `json:"sample_answer_voice_prompt"`
|
||||||
ImageUrl pgtype.Text `json:"image_url"`
|
ImageUrl pgtype.Text `json:"image_url"`
|
||||||
Column10 interface{} `json:"column_10"`
|
Column10 interface{} `json:"column_10"`
|
||||||
|
Column11 []byte `json:"column_11"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) CreateQuestion(ctx context.Context, arg CreateQuestionParams) (Question, error) {
|
func (q *Queries) CreateQuestion(ctx context.Context, arg CreateQuestionParams) (Question, error) {
|
||||||
|
|
@ -64,6 +66,7 @@ func (q *Queries) CreateQuestion(ctx context.Context, arg CreateQuestionParams)
|
||||||
arg.SampleAnswerVoicePrompt,
|
arg.SampleAnswerVoicePrompt,
|
||||||
arg.ImageUrl,
|
arg.ImageUrl,
|
||||||
arg.Column10,
|
arg.Column10,
|
||||||
|
arg.Column11,
|
||||||
)
|
)
|
||||||
var i Question
|
var i Question
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
|
|
@ -80,6 +83,8 @@ func (q *Queries) CreateQuestion(ctx context.Context, arg CreateQuestionParams)
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
&i.ImageUrl,
|
&i.ImageUrl,
|
||||||
|
&i.QuestionTypeDefinitionID,
|
||||||
|
&i.DynamicPayload,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
@ -95,7 +100,7 @@ func (q *Queries) DeleteQuestion(ctx context.Context, id int64) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
const GetQuestionByID = `-- name: GetQuestionByID :one
|
const GetQuestionByID = `-- name: GetQuestionByID :one
|
||||||
SELECT id, question_text, question_type, difficulty_level, points, explanation, tips, voice_prompt, sample_answer_voice_prompt, status, created_at, updated_at, image_url
|
SELECT id, question_text, question_type, difficulty_level, points, explanation, tips, voice_prompt, sample_answer_voice_prompt, status, created_at, updated_at, image_url, question_type_definition_id, dynamic_payload
|
||||||
FROM questions
|
FROM questions
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
`
|
`
|
||||||
|
|
@ -117,6 +122,8 @@ func (q *Queries) GetQuestionByID(ctx context.Context, id int64) (Question, erro
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
&i.ImageUrl,
|
&i.ImageUrl,
|
||||||
|
&i.QuestionTypeDefinitionID,
|
||||||
|
&i.DynamicPayload,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
@ -193,7 +200,7 @@ func (q *Queries) GetQuestionWithOptions(ctx context.Context, id int64) ([]GetQu
|
||||||
}
|
}
|
||||||
|
|
||||||
const GetQuestionsByIDs = `-- name: GetQuestionsByIDs :many
|
const GetQuestionsByIDs = `-- name: GetQuestionsByIDs :many
|
||||||
SELECT id, question_text, question_type, difficulty_level, points, explanation, tips, voice_prompt, sample_answer_voice_prompt, status, created_at, updated_at, image_url
|
SELECT id, question_text, question_type, difficulty_level, points, explanation, tips, voice_prompt, sample_answer_voice_prompt, status, created_at, updated_at, image_url, question_type_definition_id, dynamic_payload
|
||||||
FROM questions
|
FROM questions
|
||||||
WHERE id = ANY($1::BIGINT[])
|
WHERE id = ANY($1::BIGINT[])
|
||||||
ORDER BY id
|
ORDER BY id
|
||||||
|
|
@ -222,6 +229,8 @@ func (q *Queries) GetQuestionsByIDs(ctx context.Context, dollar_1 []int64) ([]Qu
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
&i.ImageUrl,
|
&i.ImageUrl,
|
||||||
|
&i.QuestionTypeDefinitionID,
|
||||||
|
&i.DynamicPayload,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -236,7 +245,7 @@ func (q *Queries) GetQuestionsByIDs(ctx context.Context, dollar_1 []int64) ([]Qu
|
||||||
const ListQuestions = `-- name: ListQuestions :many
|
const ListQuestions = `-- name: ListQuestions :many
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(*) OVER () AS total_count,
|
COUNT(*) OVER () AS total_count,
|
||||||
q.id, q.question_text, q.question_type, q.difficulty_level, q.points, q.explanation, q.tips, q.voice_prompt, q.sample_answer_voice_prompt, q.status, q.created_at, q.updated_at, q.image_url
|
q.id, q.question_text, q.question_type, q.difficulty_level, q.points, q.explanation, q.tips, q.voice_prompt, q.sample_answer_voice_prompt, q.status, q.created_at, q.updated_at, q.image_url, q.question_type_definition_id, q.dynamic_payload
|
||||||
FROM questions q
|
FROM questions q
|
||||||
WHERE status != 'ARCHIVED'
|
WHERE status != 'ARCHIVED'
|
||||||
AND ($1::VARCHAR IS NULL OR $1 = '' OR question_type = $1)
|
AND ($1::VARCHAR IS NULL OR $1 = '' OR question_type = $1)
|
||||||
|
|
@ -270,6 +279,8 @@ type ListQuestionsRow struct {
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
ImageUrl pgtype.Text `json:"image_url"`
|
ImageUrl pgtype.Text `json:"image_url"`
|
||||||
|
QuestionTypeDefinitionID pgtype.Int8 `json:"question_type_definition_id"`
|
||||||
|
DynamicPayload []byte `json:"dynamic_payload"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) ListQuestions(ctx context.Context, arg ListQuestionsParams) ([]ListQuestionsRow, error) {
|
func (q *Queries) ListQuestions(ctx context.Context, arg ListQuestionsParams) ([]ListQuestionsRow, error) {
|
||||||
|
|
@ -302,6 +313,8 @@ func (q *Queries) ListQuestions(ctx context.Context, arg ListQuestionsParams) ([
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
&i.ImageUrl,
|
&i.ImageUrl,
|
||||||
|
&i.QuestionTypeDefinitionID,
|
||||||
|
&i.DynamicPayload,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -316,7 +329,7 @@ func (q *Queries) ListQuestions(ctx context.Context, arg ListQuestionsParams) ([
|
||||||
const SearchQuestions = `-- name: SearchQuestions :many
|
const SearchQuestions = `-- name: SearchQuestions :many
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(*) OVER () AS total_count,
|
COUNT(*) OVER () AS total_count,
|
||||||
q.id, q.question_text, q.question_type, q.difficulty_level, q.points, q.explanation, q.tips, q.voice_prompt, q.sample_answer_voice_prompt, q.status, q.created_at, q.updated_at, q.image_url
|
q.id, q.question_text, q.question_type, q.difficulty_level, q.points, q.explanation, q.tips, q.voice_prompt, q.sample_answer_voice_prompt, q.status, q.created_at, q.updated_at, q.image_url, q.question_type_definition_id, q.dynamic_payload
|
||||||
FROM questions q
|
FROM questions q
|
||||||
WHERE status != 'ARCHIVED'
|
WHERE status != 'ARCHIVED'
|
||||||
AND question_text ILIKE '%' || $1 || '%'
|
AND question_text ILIKE '%' || $1 || '%'
|
||||||
|
|
@ -346,6 +359,8 @@ type SearchQuestionsRow struct {
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
ImageUrl pgtype.Text `json:"image_url"`
|
ImageUrl pgtype.Text `json:"image_url"`
|
||||||
|
QuestionTypeDefinitionID pgtype.Int8 `json:"question_type_definition_id"`
|
||||||
|
DynamicPayload []byte `json:"dynamic_payload"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) SearchQuestions(ctx context.Context, arg SearchQuestionsParams) ([]SearchQuestionsRow, error) {
|
func (q *Queries) SearchQuestions(ctx context.Context, arg SearchQuestionsParams) ([]SearchQuestionsRow, error) {
|
||||||
|
|
@ -372,6 +387,8 @@ func (q *Queries) SearchQuestions(ctx context.Context, arg SearchQuestionsParams
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
&i.ImageUrl,
|
&i.ImageUrl,
|
||||||
|
&i.QuestionTypeDefinitionID,
|
||||||
|
&i.DynamicPayload,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -396,8 +413,9 @@ SET
|
||||||
sample_answer_voice_prompt = COALESCE($8, sample_answer_voice_prompt),
|
sample_answer_voice_prompt = COALESCE($8, sample_answer_voice_prompt),
|
||||||
image_url = COALESCE($9, image_url),
|
image_url = COALESCE($9, image_url),
|
||||||
status = COALESCE($10, status),
|
status = COALESCE($10, status),
|
||||||
|
dynamic_payload = COALESCE($11::jsonb, dynamic_payload),
|
||||||
updated_at = CURRENT_TIMESTAMP
|
updated_at = CURRENT_TIMESTAMP
|
||||||
WHERE id = $11
|
WHERE id = $12
|
||||||
`
|
`
|
||||||
|
|
||||||
type UpdateQuestionParams struct {
|
type UpdateQuestionParams struct {
|
||||||
|
|
@ -411,6 +429,7 @@ type UpdateQuestionParams struct {
|
||||||
SampleAnswerVoicePrompt pgtype.Text `json:"sample_answer_voice_prompt"`
|
SampleAnswerVoicePrompt pgtype.Text `json:"sample_answer_voice_prompt"`
|
||||||
ImageUrl pgtype.Text `json:"image_url"`
|
ImageUrl pgtype.Text `json:"image_url"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
|
Column11 []byte `json:"column_11"`
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -426,6 +445,7 @@ func (q *Queries) UpdateQuestion(ctx context.Context, arg UpdateQuestionParams)
|
||||||
arg.SampleAnswerVoicePrompt,
|
arg.SampleAnswerVoicePrompt,
|
||||||
arg.ImageUrl,
|
arg.ImageUrl,
|
||||||
arg.Status,
|
arg.Status,
|
||||||
|
arg.Column11,
|
||||||
arg.ID,
|
arg.ID,
|
||||||
)
|
)
|
||||||
return err
|
return err
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,14 @@ import (
|
||||||
type StimulusComponentKind string
|
type StimulusComponentKind string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
StimulusQuestionText StimulusComponentKind = "QUESTION_TEXT"
|
||||||
StimulusPrepTime StimulusComponentKind = "PREP_TIME"
|
StimulusPrepTime StimulusComponentKind = "PREP_TIME"
|
||||||
StimulusInstruction StimulusComponentKind = "INSTRUCTION"
|
StimulusInstruction StimulusComponentKind = "INSTRUCTION"
|
||||||
|
StimulusAudioPrompt StimulusComponentKind = "AUDIO_PROMPT"
|
||||||
StimulusAudioClip StimulusComponentKind = "AUDIO_CLIP"
|
StimulusAudioClip StimulusComponentKind = "AUDIO_CLIP"
|
||||||
StimulusTextPassage StimulusComponentKind = "TEXT_PASSAGE"
|
StimulusTextPassage StimulusComponentKind = "TEXT_PASSAGE"
|
||||||
StimulusImage StimulusComponentKind = "IMAGE"
|
StimulusImage StimulusComponentKind = "IMAGE"
|
||||||
|
StimulusChart StimulusComponentKind = "CHART"
|
||||||
StimulusMatchingInputs StimulusComponentKind = "MATCHING_INPUTS"
|
StimulusMatchingInputs StimulusComponentKind = "MATCHING_INPUTS"
|
||||||
StimulusSelectMissingWords StimulusComponentKind = "SELECT_MISSING_WORDS"
|
StimulusSelectMissingWords StimulusComponentKind = "SELECT_MISSING_WORDS"
|
||||||
StimulusTable StimulusComponentKind = "TABLE"
|
StimulusTable StimulusComponentKind = "TABLE"
|
||||||
|
|
@ -29,20 +32,45 @@ const (
|
||||||
ResponseTextInput ResponseComponentKind = "TEXT_INPUT"
|
ResponseTextInput ResponseComponentKind = "TEXT_INPUT"
|
||||||
ResponseShortAnswer ResponseComponentKind = "SHORT_ANSWER"
|
ResponseShortAnswer ResponseComponentKind = "SHORT_ANSWER"
|
||||||
ResponseMultipleChoice ResponseComponentKind = "MULTIPLE_CHOICE"
|
ResponseMultipleChoice ResponseComponentKind = "MULTIPLE_CHOICE"
|
||||||
|
ResponseOption ResponseComponentKind = "OPTION"
|
||||||
ResponseAnswerTimer ResponseComponentKind = "ANSWER_TIMER"
|
ResponseAnswerTimer ResponseComponentKind = "ANSWER_TIMER"
|
||||||
ResponseSelectMissingWords ResponseComponentKind = "SELECT_MISSING_WORDS"
|
ResponseSelectMissingWords ResponseComponentKind = "SELECT_MISSING_WORDS"
|
||||||
ResponsePDFUpload ResponseComponentKind = "PDF_UPLOAD"
|
ResponsePDFUpload ResponseComponentKind = "PDF_UPLOAD"
|
||||||
ResponseMatchingAnswer ResponseComponentKind = "MATCHING_ANSWER"
|
ResponseMatchingAnswer ResponseComponentKind = "MATCHING_ANSWER"
|
||||||
ResponseLabelSelection ResponseComponentKind = "LABEL_SELECTION"
|
ResponseLabelSelection ResponseComponentKind = "LABEL_SELECTION"
|
||||||
|
ResponseSequenceOrder ResponseComponentKind = "SEQUENCE_ORDER"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type DynamicElementDefinition struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Kind string `json:"kind"`
|
||||||
|
Label *string `json:"label,omitempty"`
|
||||||
|
Required bool `json:"required"`
|
||||||
|
Config map[string]interface{} `json:"config,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DynamicElementInstance struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Kind string `json:"kind"`
|
||||||
|
Value interface{} `json:"value,omitempty"`
|
||||||
|
Meta map[string]interface{} `json:"meta,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DynamicQuestionPayload struct {
|
||||||
|
Stimulus []DynamicElementInstance `json:"stimulus"`
|
||||||
|
Response []DynamicElementInstance `json:"response"`
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
stimulusCatalog = []StimulusComponentKind{
|
stimulusCatalog = []StimulusComponentKind{
|
||||||
|
StimulusQuestionText,
|
||||||
StimulusPrepTime,
|
StimulusPrepTime,
|
||||||
StimulusInstruction,
|
StimulusInstruction,
|
||||||
|
StimulusAudioPrompt,
|
||||||
StimulusAudioClip,
|
StimulusAudioClip,
|
||||||
StimulusTextPassage,
|
StimulusTextPassage,
|
||||||
StimulusImage,
|
StimulusImage,
|
||||||
|
StimulusChart,
|
||||||
StimulusMatchingInputs,
|
StimulusMatchingInputs,
|
||||||
StimulusSelectMissingWords,
|
StimulusSelectMissingWords,
|
||||||
StimulusTable,
|
StimulusTable,
|
||||||
|
|
@ -55,11 +83,13 @@ var (
|
||||||
ResponseTextInput,
|
ResponseTextInput,
|
||||||
ResponseShortAnswer,
|
ResponseShortAnswer,
|
||||||
ResponseMultipleChoice,
|
ResponseMultipleChoice,
|
||||||
|
ResponseOption,
|
||||||
ResponseAnswerTimer,
|
ResponseAnswerTimer,
|
||||||
ResponseSelectMissingWords,
|
ResponseSelectMissingWords,
|
||||||
ResponsePDFUpload,
|
ResponsePDFUpload,
|
||||||
ResponseMatchingAnswer,
|
ResponseMatchingAnswer,
|
||||||
ResponseLabelSelection,
|
ResponseLabelSelection,
|
||||||
|
ResponseSequenceOrder,
|
||||||
}
|
}
|
||||||
responseSet map[string]struct{}
|
responseSet map[string]struct{}
|
||||||
|
|
||||||
|
|
@ -183,6 +213,178 @@ func ValidateDynamicQuestionTypeDefinition(stimulusKinds, responseKinds []string
|
||||||
return fmt.Errorf("%s", strings.Join(errs, "; "))
|
return fmt.Errorf("%s", strings.Join(errs, "; "))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ResolveRuntimeQuestionTypeFromDefinition derives the legacy runtime question_type code used by
|
||||||
|
// existing question execution paths. Empty string means the definition cannot be executed yet.
|
||||||
|
func ResolveRuntimeQuestionTypeFromDefinition(key string, responseKinds []string) string {
|
||||||
|
normalizedKey := strings.ToLower(strings.TrimSpace(key))
|
||||||
|
if normalizedKey == "true_false" || normalizedKey == "true-false" {
|
||||||
|
return "TRUE_FALSE"
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, kind := range normalizeKindList(responseKinds) {
|
||||||
|
switch kind {
|
||||||
|
case string(ResponseAudioResponse):
|
||||||
|
return "AUDIO"
|
||||||
|
case string(ResponseMultipleChoice):
|
||||||
|
return "MCQ"
|
||||||
|
case string(ResponseShortAnswer),
|
||||||
|
string(ResponseTextInput),
|
||||||
|
string(ResponseSelectMissingWords),
|
||||||
|
string(ResponseMatchingAnswer),
|
||||||
|
string(ResponseLabelSelection),
|
||||||
|
string(ResponsePDFUpload):
|
||||||
|
return "SHORT_ANSWER"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidatePersistableQuestionTypeDefinition ensures definitions that pass component-level validation
|
||||||
|
// are also mappable to the currently supported runtime question_type set.
|
||||||
|
func ValidatePersistableQuestionTypeDefinition(key string, responseKinds []string) error {
|
||||||
|
if ResolveRuntimeQuestionTypeFromDefinition(key, responseKinds) == "" {
|
||||||
|
return fmt.Errorf("unable to map definition to runtime question_type")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateDefinitionSchemas(stimulusSchema, responseSchema []DynamicElementDefinition) error {
|
||||||
|
var errs []string
|
||||||
|
stimulusIDs := make(map[string]struct{})
|
||||||
|
responseIDs := make(map[string]struct{})
|
||||||
|
|
||||||
|
for i, el := range stimulusSchema {
|
||||||
|
id := strings.TrimSpace(el.ID)
|
||||||
|
kind := strings.TrimSpace(el.Kind)
|
||||||
|
|
||||||
|
if id == "" {
|
||||||
|
errs = append(errs, fmt.Sprintf("stimulus schema [%d]: id is required", i))
|
||||||
|
} else if _, exists := stimulusIDs[id]; exists {
|
||||||
|
errs = append(errs, fmt.Sprintf("stimulus schema: duplicate id %q", id))
|
||||||
|
} else {
|
||||||
|
stimulusIDs[id] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !IsValidStimulusComponentKind(kind) {
|
||||||
|
errs = append(errs, fmt.Sprintf("stimulus schema: unknown kind %q", kind))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, el := range responseSchema {
|
||||||
|
id := strings.TrimSpace(el.ID)
|
||||||
|
kind := strings.TrimSpace(el.Kind)
|
||||||
|
|
||||||
|
if id == "" {
|
||||||
|
errs = append(errs, fmt.Sprintf("response schema [%d]: id is required", i))
|
||||||
|
} else if _, exists := responseIDs[id]; exists {
|
||||||
|
errs = append(errs, fmt.Sprintf("response schema: duplicate id %q", id))
|
||||||
|
} else {
|
||||||
|
responseIDs[id] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !IsValidResponseComponentKind(kind) {
|
||||||
|
errs = append(errs, fmt.Sprintf("response schema: unknown kind %q", kind))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("%s", strings.Join(errs, "; "))
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateDynamicPayloadAgainstDefinition(payload DynamicQuestionPayload, def QuestionTypeDefinition) error {
|
||||||
|
var errs []string
|
||||||
|
if len(payload.Stimulus) == 0 {
|
||||||
|
errs = append(errs, "dynamic_payload.stimulus must contain at least one element")
|
||||||
|
}
|
||||||
|
if len(payload.Response) == 0 {
|
||||||
|
errs = append(errs, "dynamic_payload.response must contain at least one element")
|
||||||
|
}
|
||||||
|
|
||||||
|
allowedStimulus := make(map[string]struct{})
|
||||||
|
allowedResponse := make(map[string]struct{})
|
||||||
|
for _, k := range def.StimulusComponentKinds {
|
||||||
|
allowedStimulus[strings.TrimSpace(k)] = struct{}{}
|
||||||
|
}
|
||||||
|
for _, k := range def.ResponseComponentKinds {
|
||||||
|
allowedResponse[strings.TrimSpace(k)] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
requiredStimulusIDs := make(map[string]struct{})
|
||||||
|
requiredResponseIDs := make(map[string]struct{})
|
||||||
|
for _, el := range def.StimulusSchema {
|
||||||
|
if el.Required {
|
||||||
|
requiredStimulusIDs[strings.TrimSpace(el.ID)] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, el := range def.ResponseSchema {
|
||||||
|
if el.Required {
|
||||||
|
requiredResponseIDs[strings.TrimSpace(el.ID)] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
seenStimulusIDs := make(map[string]struct{})
|
||||||
|
seenResponseIDs := make(map[string]struct{})
|
||||||
|
|
||||||
|
for i, el := range payload.Stimulus {
|
||||||
|
id := strings.TrimSpace(el.ID)
|
||||||
|
kind := strings.TrimSpace(el.Kind)
|
||||||
|
|
||||||
|
if id == "" {
|
||||||
|
errs = append(errs, fmt.Sprintf("dynamic_payload.stimulus[%d]: id is required", i))
|
||||||
|
} else {
|
||||||
|
seenStimulusIDs[id] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !IsValidStimulusComponentKind(kind) {
|
||||||
|
errs = append(errs, fmt.Sprintf("dynamic_payload.stimulus[%d]: invalid kind %q", i, kind))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := allowedStimulus[kind]; !ok {
|
||||||
|
errs = append(errs, fmt.Sprintf("dynamic_payload.stimulus[%d]: kind %q is not allowed by selected definition", i, kind))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, el := range payload.Response {
|
||||||
|
id := strings.TrimSpace(el.ID)
|
||||||
|
kind := strings.TrimSpace(el.Kind)
|
||||||
|
|
||||||
|
if id == "" {
|
||||||
|
errs = append(errs, fmt.Sprintf("dynamic_payload.response[%d]: id is required", i))
|
||||||
|
} else {
|
||||||
|
seenResponseIDs[id] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !IsValidResponseComponentKind(kind) {
|
||||||
|
errs = append(errs, fmt.Sprintf("dynamic_payload.response[%d]: invalid kind %q", i, kind))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := allowedResponse[kind]; !ok {
|
||||||
|
errs = append(errs, fmt.Sprintf("dynamic_payload.response[%d]: kind %q is not allowed by selected definition", i, kind))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for id := range requiredStimulusIDs {
|
||||||
|
if _, ok := seenStimulusIDs[id]; !ok {
|
||||||
|
errs = append(errs, fmt.Sprintf("dynamic_payload.stimulus: required element id %q is missing", id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for id := range requiredResponseIDs {
|
||||||
|
if _, ok := seenResponseIDs[id]; !ok {
|
||||||
|
errs = append(errs, fmt.Sprintf("dynamic_payload.response: required element id %q is missing", id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("%s", strings.Join(errs, "; "))
|
||||||
|
}
|
||||||
|
|
||||||
func normalizeKindList(in []string) []string {
|
func normalizeKindList(in []string) []string {
|
||||||
var out []string
|
var out []string
|
||||||
for _, s := range in {
|
for _, s := range in {
|
||||||
|
|
|
||||||
|
|
@ -64,3 +64,71 @@ func TestValidateDynamicQuestionTypeDefinition_twoPrepTimes(t *testing.T) {
|
||||||
t.Fatalf("expected at most one PREP_TIME, got %v", err)
|
t.Fatalf("expected at most one PREP_TIME, got %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestResolveRuntimeQuestionTypeFromDefinition_trueFalseKey(t *testing.T) {
|
||||||
|
got := ResolveRuntimeQuestionTypeFromDefinition("true_false", []string{"MULTIPLE_CHOICE"})
|
||||||
|
if got != "TRUE_FALSE" {
|
||||||
|
t.Fatalf("expected TRUE_FALSE, got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveRuntimeQuestionTypeFromDefinition_pdfUpload(t *testing.T) {
|
||||||
|
got := ResolveRuntimeQuestionTypeFromDefinition("doc_upload", []string{"PDF_UPLOAD"})
|
||||||
|
if got != "SHORT_ANSWER" {
|
||||||
|
t.Fatalf("expected SHORT_ANSWER, got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveRuntimeQuestionTypeFromDefinition_skipsAuxiliaryKinds(t *testing.T) {
|
||||||
|
got := ResolveRuntimeQuestionTypeFromDefinition("timed_mcq", []string{"ANSWER_TIMER", "MULTIPLE_CHOICE"})
|
||||||
|
if got != "MCQ" {
|
||||||
|
t.Fatalf("expected MCQ, got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidatePersistableQuestionTypeDefinition_unmappable(t *testing.T) {
|
||||||
|
err := ValidatePersistableQuestionTypeDefinition("custom", []string{"ANSWER_TIMER"})
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "unable to map definition") {
|
||||||
|
t.Fatalf("expected unmappable definition error, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateDefinitionSchemas_valid(t *testing.T) {
|
||||||
|
err := ValidateDefinitionSchemas(
|
||||||
|
[]DynamicElementDefinition{
|
||||||
|
{ID: "stimulus_1", Kind: "QUESTION_TEXT", Required: true},
|
||||||
|
{ID: "stimulus_2", Kind: "IMAGE", Required: false},
|
||||||
|
},
|
||||||
|
[]DynamicElementDefinition{
|
||||||
|
{ID: "response_1", Kind: "OPTION", Required: true},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected nil, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateDynamicPayloadAgainstDefinition_requiredMissing(t *testing.T) {
|
||||||
|
def := QuestionTypeDefinition{
|
||||||
|
StimulusComponentKinds: []string{"QUESTION_TEXT"},
|
||||||
|
ResponseComponentKinds: []string{"OPTION"},
|
||||||
|
StimulusSchema: []DynamicElementDefinition{
|
||||||
|
{ID: "stimulus_1", Kind: "QUESTION_TEXT", Required: true},
|
||||||
|
},
|
||||||
|
ResponseSchema: []DynamicElementDefinition{
|
||||||
|
{ID: "response_1", Kind: "OPTION", Required: true},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := DynamicQuestionPayload{
|
||||||
|
Stimulus: []DynamicElementInstance{
|
||||||
|
{ID: "stimulus_1", Kind: "QUESTION_TEXT"},
|
||||||
|
},
|
||||||
|
Response: []DynamicElementInstance{},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := ValidateDynamicPayloadAgainstDefinition(payload, def)
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "required element id") {
|
||||||
|
t.Fatalf("expected required element error, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ type QuestionTypeDefinition struct {
|
||||||
Description *string
|
Description *string
|
||||||
StimulusComponentKinds []string
|
StimulusComponentKinds []string
|
||||||
ResponseComponentKinds []string
|
ResponseComponentKinds []string
|
||||||
|
StimulusSchema []DynamicElementDefinition
|
||||||
|
ResponseSchema []DynamicElementDefinition
|
||||||
IsSystem bool
|
IsSystem bool
|
||||||
Status string
|
Status string
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
|
|
@ -21,6 +23,8 @@ type CreateQuestionTypeDefinitionInput struct {
|
||||||
Description *string
|
Description *string
|
||||||
StimulusComponentKinds []string
|
StimulusComponentKinds []string
|
||||||
ResponseComponentKinds []string
|
ResponseComponentKinds []string
|
||||||
|
StimulusSchema []DynamicElementDefinition
|
||||||
|
ResponseSchema []DynamicElementDefinition
|
||||||
IsSystem bool
|
IsSystem bool
|
||||||
Status *string
|
Status *string
|
||||||
}
|
}
|
||||||
|
|
@ -30,5 +34,7 @@ type UpdateQuestionTypeDefinitionInput struct {
|
||||||
Description *string
|
Description *string
|
||||||
StimulusComponentKinds []string
|
StimulusComponentKinds []string
|
||||||
ResponseComponentKinds []string
|
ResponseComponentKinds []string
|
||||||
|
StimulusSchema []DynamicElementDefinition
|
||||||
|
ResponseSchema []DynamicElementDefinition
|
||||||
Status *string
|
Status *string
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ type Question struct {
|
||||||
QuestionText string
|
QuestionText string
|
||||||
QuestionType string
|
QuestionType string
|
||||||
QuestionTypeDefinitionID *int64
|
QuestionTypeDefinitionID *int64
|
||||||
|
DynamicPayload *DynamicQuestionPayload
|
||||||
DifficultyLevel *string
|
DifficultyLevel *string
|
||||||
Points int32
|
Points int32
|
||||||
Explanation *string
|
Explanation *string
|
||||||
|
|
@ -123,6 +124,7 @@ type QuestionSetItemWithQuestion struct {
|
||||||
QuestionSetItem
|
QuestionSetItem
|
||||||
QuestionText string
|
QuestionText string
|
||||||
QuestionType string
|
QuestionType string
|
||||||
|
DynamicPayload *DynamicQuestionPayload
|
||||||
DifficultyLevel *string
|
DifficultyLevel *string
|
||||||
Points int32
|
Points int32
|
||||||
Explanation *string
|
Explanation *string
|
||||||
|
|
@ -138,6 +140,7 @@ type CreateQuestionInput struct {
|
||||||
QuestionText string
|
QuestionText string
|
||||||
QuestionType string
|
QuestionType string
|
||||||
QuestionTypeDefinitionID *int64
|
QuestionTypeDefinitionID *int64
|
||||||
|
DynamicPayload *DynamicQuestionPayload
|
||||||
DifficultyLevel *string
|
DifficultyLevel *string
|
||||||
Points *int32
|
Points *int32
|
||||||
Explanation *string
|
Explanation *string
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package repository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -67,6 +68,8 @@ func questionToDomain(q dbgen.Question) domain.Question {
|
||||||
ID: q.ID,
|
ID: q.ID,
|
||||||
QuestionText: q.QuestionText,
|
QuestionText: q.QuestionText,
|
||||||
QuestionType: q.QuestionType,
|
QuestionType: q.QuestionType,
|
||||||
|
QuestionTypeDefinitionID: fromPgInt8(q.QuestionTypeDefinitionID),
|
||||||
|
DynamicPayload: parseDynamicPayload(q.DynamicPayload),
|
||||||
DifficultyLevel: fromPgText(q.DifficultyLevel),
|
DifficultyLevel: fromPgText(q.DifficultyLevel),
|
||||||
Points: q.Points,
|
Points: q.Points,
|
||||||
Explanation: fromPgText(q.Explanation),
|
Explanation: fromPgText(q.Explanation),
|
||||||
|
|
@ -80,6 +83,28 @@ func questionToDomain(q dbgen.Question) domain.Question {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseDynamicPayload(raw []byte) *domain.DynamicQuestionPayload {
|
||||||
|
if len(raw) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var payload domain.DynamicQuestionPayload
|
||||||
|
if err := json.Unmarshal(raw, &payload); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &payload
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeDynamicPayload(payload *domain.DynamicQuestionPayload) []byte {
|
||||||
|
if payload == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
b, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Store) setQuestionTypeDefinitionID(ctx context.Context, questionID int64, definitionID *int64) error {
|
func (s *Store) setQuestionTypeDefinitionID(ctx context.Context, questionID int64, definitionID *int64) error {
|
||||||
_, err := s.conn.Exec(ctx, `
|
_, err := s.conn.Exec(ctx, `
|
||||||
UPDATE questions
|
UPDATE questions
|
||||||
|
|
@ -156,11 +181,22 @@ func questionTypeDefinitionToDomain(
|
||||||
description pgtype.Text,
|
description pgtype.Text,
|
||||||
stimulusKinds []string,
|
stimulusKinds []string,
|
||||||
responseKinds []string,
|
responseKinds []string,
|
||||||
|
stimulusSchema []byte,
|
||||||
|
responseSchema []byte,
|
||||||
isSystem bool,
|
isSystem bool,
|
||||||
status string,
|
status string,
|
||||||
createdAt time.Time,
|
createdAt time.Time,
|
||||||
updatedAt pgtype.Timestamptz,
|
updatedAt pgtype.Timestamptz,
|
||||||
) domain.QuestionTypeDefinition {
|
) domain.QuestionTypeDefinition {
|
||||||
|
var stimulusSchemaDef []domain.DynamicElementDefinition
|
||||||
|
var responseSchemaDef []domain.DynamicElementDefinition
|
||||||
|
if len(stimulusSchema) > 0 {
|
||||||
|
_ = json.Unmarshal(stimulusSchema, &stimulusSchemaDef)
|
||||||
|
}
|
||||||
|
if len(responseSchema) > 0 {
|
||||||
|
_ = json.Unmarshal(responseSchema, &responseSchemaDef)
|
||||||
|
}
|
||||||
|
|
||||||
return domain.QuestionTypeDefinition{
|
return domain.QuestionTypeDefinition{
|
||||||
ID: id,
|
ID: id,
|
||||||
Key: key,
|
Key: key,
|
||||||
|
|
@ -168,6 +204,8 @@ func questionTypeDefinitionToDomain(
|
||||||
Description: fromPgText(description),
|
Description: fromPgText(description),
|
||||||
StimulusComponentKinds: stimulusKinds,
|
StimulusComponentKinds: stimulusKinds,
|
||||||
ResponseComponentKinds: responseKinds,
|
ResponseComponentKinds: responseKinds,
|
||||||
|
StimulusSchema: stimulusSchemaDef,
|
||||||
|
ResponseSchema: responseSchemaDef,
|
||||||
IsSystem: isSystem,
|
IsSystem: isSystem,
|
||||||
Status: status,
|
Status: status,
|
||||||
CreatedAt: createdAt,
|
CreatedAt: createdAt,
|
||||||
|
|
@ -200,24 +238,85 @@ func normalizeDefinitionKey(key string) string {
|
||||||
return key
|
return key
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func kindsFromStimulusSchema(schema []domain.DynamicElementDefinition) []string {
|
||||||
|
seen := make(map[string]struct{})
|
||||||
|
out := make([]string, 0, len(schema))
|
||||||
|
for _, el := range schema {
|
||||||
|
kind := strings.TrimSpace(el.Kind)
|
||||||
|
if kind == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, exists := seen[kind]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[kind] = struct{}{}
|
||||||
|
out = append(out, kind)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func kindsFromResponseSchema(schema []domain.DynamicElementDefinition) []string {
|
||||||
|
seen := make(map[string]struct{})
|
||||||
|
out := make([]string, 0, len(schema))
|
||||||
|
for _, el := range schema {
|
||||||
|
kind := strings.TrimSpace(el.Kind)
|
||||||
|
if kind == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, exists := seen[kind]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[kind] = struct{}{}
|
||||||
|
out = append(out, kind)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustJSON(v interface{}) []byte {
|
||||||
|
b, err := json.Marshal(v)
|
||||||
|
if err != nil {
|
||||||
|
return []byte("[]")
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Store) CreateQuestionTypeDefinition(ctx context.Context, input domain.CreateQuestionTypeDefinitionInput) (domain.QuestionTypeDefinition, error) {
|
func (s *Store) CreateQuestionTypeDefinition(ctx context.Context, input domain.CreateQuestionTypeDefinitionInput) (domain.QuestionTypeDefinition, error) {
|
||||||
|
normalizedKey := normalizeDefinitionKey(input.Key)
|
||||||
stimulusKinds := normalizeDefinitionKinds(input.StimulusComponentKinds)
|
stimulusKinds := normalizeDefinitionKinds(input.StimulusComponentKinds)
|
||||||
responseKinds := normalizeDefinitionKinds(input.ResponseComponentKinds)
|
responseKinds := normalizeDefinitionKinds(input.ResponseComponentKinds)
|
||||||
|
stimulusSchema := input.StimulusSchema
|
||||||
|
responseSchema := input.ResponseSchema
|
||||||
|
|
||||||
|
if len(stimulusKinds) == 0 && len(stimulusSchema) > 0 {
|
||||||
|
stimulusKinds = kindsFromStimulusSchema(stimulusSchema)
|
||||||
|
}
|
||||||
|
if len(responseKinds) == 0 && len(responseSchema) > 0 {
|
||||||
|
responseKinds = kindsFromResponseSchema(responseSchema)
|
||||||
|
}
|
||||||
|
|
||||||
if err := domain.ValidateDynamicQuestionTypeDefinition(stimulusKinds, responseKinds); err != nil {
|
if err := domain.ValidateDynamicQuestionTypeDefinition(stimulusKinds, responseKinds); err != nil {
|
||||||
return domain.QuestionTypeDefinition{}, err
|
return domain.QuestionTypeDefinition{}, err
|
||||||
}
|
}
|
||||||
|
if err := domain.ValidateDefinitionSchemas(stimulusSchema, responseSchema); err != nil {
|
||||||
|
return domain.QuestionTypeDefinition{}, err
|
||||||
|
}
|
||||||
|
if err := domain.ValidatePersistableQuestionTypeDefinition(normalizedKey, responseKinds); err != nil {
|
||||||
|
return domain.QuestionTypeDefinition{}, err
|
||||||
|
}
|
||||||
|
|
||||||
row := s.conn.QueryRow(ctx, `
|
row := s.conn.QueryRow(ctx, `
|
||||||
INSERT INTO question_type_definitions
|
INSERT INTO question_type_definitions
|
||||||
(key, display_name, description, stimulus_component_kinds, response_component_kinds, is_system, status)
|
(key, display_name, description, stimulus_component_kinds, response_component_kinds, stimulus_schema, response_schema, is_system, status)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
VALUES ($1, $2, $3, $4, $5, $6::jsonb, $7::jsonb, $8, $9)
|
||||||
RETURNING id, key, display_name, description, stimulus_component_kinds, response_component_kinds, is_system, status, created_at, updated_at
|
RETURNING id, key, display_name, description, stimulus_component_kinds, response_component_kinds, stimulus_schema, response_schema, is_system, status, created_at, updated_at
|
||||||
`,
|
`,
|
||||||
normalizeDefinitionKey(input.Key),
|
normalizedKey,
|
||||||
strings.TrimSpace(input.DisplayName),
|
strings.TrimSpace(input.DisplayName),
|
||||||
toPgText(input.Description),
|
toPgText(input.Description),
|
||||||
stimulusKinds,
|
stimulusKinds,
|
||||||
responseKinds,
|
responseKinds,
|
||||||
|
mustJSON(stimulusSchema),
|
||||||
|
mustJSON(responseSchema),
|
||||||
input.IsSystem,
|
input.IsSystem,
|
||||||
normalizeDefinitionStatus(input.Status),
|
normalizeDefinitionStatus(input.Status),
|
||||||
)
|
)
|
||||||
|
|
@ -229,21 +328,23 @@ func (s *Store) CreateQuestionTypeDefinition(ctx context.Context, input domain.C
|
||||||
description pgtype.Text
|
description pgtype.Text
|
||||||
stimulus []string
|
stimulus []string
|
||||||
response []string
|
response []string
|
||||||
|
stimulusSch []byte
|
||||||
|
responseSch []byte
|
||||||
isSystem bool
|
isSystem bool
|
||||||
status string
|
status string
|
||||||
createdAt time.Time
|
createdAt time.Time
|
||||||
updatedAt pgtype.Timestamptz
|
updatedAt pgtype.Timestamptz
|
||||||
)
|
)
|
||||||
if err := row.Scan(&id, &key, &displayName, &description, &stimulus, &response, &isSystem, &status, &createdAt, &updatedAt); err != nil {
|
if err := row.Scan(&id, &key, &displayName, &description, &stimulus, &response, &stimulusSch, &responseSch, &isSystem, &status, &createdAt, &updatedAt); err != nil {
|
||||||
return domain.QuestionTypeDefinition{}, err
|
return domain.QuestionTypeDefinition{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return questionTypeDefinitionToDomain(id, key, displayName, description, stimulus, response, isSystem, status, createdAt, updatedAt), nil
|
return questionTypeDefinitionToDomain(id, key, displayName, description, stimulus, response, stimulusSch, responseSch, isSystem, status, createdAt, updatedAt), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) GetQuestionTypeDefinitionByID(ctx context.Context, id int64) (domain.QuestionTypeDefinition, error) {
|
func (s *Store) GetQuestionTypeDefinitionByID(ctx context.Context, id int64) (domain.QuestionTypeDefinition, error) {
|
||||||
row := s.conn.QueryRow(ctx, `
|
row := s.conn.QueryRow(ctx, `
|
||||||
SELECT id, key, display_name, description, stimulus_component_kinds, response_component_kinds, is_system, status, created_at, updated_at
|
SELECT id, key, display_name, description, stimulus_component_kinds, response_component_kinds, stimulus_schema, response_schema, is_system, status, created_at, updated_at
|
||||||
FROM question_type_definitions
|
FROM question_type_definitions
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
`, id)
|
`, id)
|
||||||
|
|
@ -254,20 +355,22 @@ func (s *Store) GetQuestionTypeDefinitionByID(ctx context.Context, id int64) (do
|
||||||
description pgtype.Text
|
description pgtype.Text
|
||||||
stimulus []string
|
stimulus []string
|
||||||
response []string
|
response []string
|
||||||
|
stimulusSch []byte
|
||||||
|
responseSch []byte
|
||||||
isSystem bool
|
isSystem bool
|
||||||
status string
|
status string
|
||||||
createdAt time.Time
|
createdAt time.Time
|
||||||
updatedAt pgtype.Timestamptz
|
updatedAt pgtype.Timestamptz
|
||||||
)
|
)
|
||||||
if err := row.Scan(&id, &key, &displayName, &description, &stimulus, &response, &isSystem, &status, &createdAt, &updatedAt); err != nil {
|
if err := row.Scan(&id, &key, &displayName, &description, &stimulus, &response, &stimulusSch, &responseSch, &isSystem, &status, &createdAt, &updatedAt); err != nil {
|
||||||
return domain.QuestionTypeDefinition{}, err
|
return domain.QuestionTypeDefinition{}, err
|
||||||
}
|
}
|
||||||
return questionTypeDefinitionToDomain(id, key, displayName, description, stimulus, response, isSystem, status, createdAt, updatedAt), nil
|
return questionTypeDefinitionToDomain(id, key, displayName, description, stimulus, response, stimulusSch, responseSch, isSystem, status, createdAt, updatedAt), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) ListQuestionTypeDefinitions(ctx context.Context, status *string, includeSystem bool) ([]domain.QuestionTypeDefinition, error) {
|
func (s *Store) ListQuestionTypeDefinitions(ctx context.Context, status *string, includeSystem bool) ([]domain.QuestionTypeDefinition, error) {
|
||||||
rows, err := s.conn.Query(ctx, `
|
rows, err := s.conn.Query(ctx, `
|
||||||
SELECT id, key, display_name, description, stimulus_component_kinds, response_component_kinds, is_system, status, created_at, updated_at
|
SELECT id, key, display_name, description, stimulus_component_kinds, response_component_kinds, stimulus_schema, response_schema, is_system, status, created_at, updated_at
|
||||||
FROM question_type_definitions
|
FROM question_type_definitions
|
||||||
WHERE ($1::VARCHAR IS NULL OR status = $1)
|
WHERE ($1::VARCHAR IS NULL OR status = $1)
|
||||||
AND ($2::BOOLEAN = TRUE OR is_system = FALSE)
|
AND ($2::BOOLEAN = TRUE OR is_system = FALSE)
|
||||||
|
|
@ -287,15 +390,17 @@ func (s *Store) ListQuestionTypeDefinitions(ctx context.Context, status *string,
|
||||||
description pgtype.Text
|
description pgtype.Text
|
||||||
stimulus []string
|
stimulus []string
|
||||||
response []string
|
response []string
|
||||||
|
stimulusSch []byte
|
||||||
|
responseSch []byte
|
||||||
isSystem bool
|
isSystem bool
|
||||||
defStatus string
|
defStatus string
|
||||||
createdAt time.Time
|
createdAt time.Time
|
||||||
updatedAt pgtype.Timestamptz
|
updatedAt pgtype.Timestamptz
|
||||||
)
|
)
|
||||||
if err := rows.Scan(&id, &key, &displayName, &description, &stimulus, &response, &isSystem, &defStatus, &createdAt, &updatedAt); err != nil {
|
if err := rows.Scan(&id, &key, &displayName, &description, &stimulus, &response, &stimulusSch, &responseSch, &isSystem, &defStatus, &createdAt, &updatedAt); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
out = append(out, questionTypeDefinitionToDomain(id, key, displayName, description, stimulus, response, isSystem, defStatus, createdAt, updatedAt))
|
out = append(out, questionTypeDefinitionToDomain(id, key, displayName, description, stimulus, response, stimulusSch, responseSch, isSystem, defStatus, createdAt, updatedAt))
|
||||||
}
|
}
|
||||||
|
|
||||||
return out, rows.Err()
|
return out, rows.Err()
|
||||||
|
|
@ -326,10 +431,31 @@ func (s *Store) UpdateQuestionTypeDefinition(ctx context.Context, id int64, inpu
|
||||||
if input.ResponseComponentKinds != nil {
|
if input.ResponseComponentKinds != nil {
|
||||||
responseKinds = normalizeDefinitionKinds(input.ResponseComponentKinds)
|
responseKinds = normalizeDefinitionKinds(input.ResponseComponentKinds)
|
||||||
}
|
}
|
||||||
|
stimulusSchema := existing.StimulusSchema
|
||||||
|
if input.StimulusSchema != nil {
|
||||||
|
stimulusSchema = input.StimulusSchema
|
||||||
|
}
|
||||||
|
responseSchema := existing.ResponseSchema
|
||||||
|
if input.ResponseSchema != nil {
|
||||||
|
responseSchema = input.ResponseSchema
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(stimulusKinds) == 0 && len(stimulusSchema) > 0 {
|
||||||
|
stimulusKinds = kindsFromStimulusSchema(stimulusSchema)
|
||||||
|
}
|
||||||
|
if len(responseKinds) == 0 && len(responseSchema) > 0 {
|
||||||
|
responseKinds = kindsFromResponseSchema(responseSchema)
|
||||||
|
}
|
||||||
|
|
||||||
if err := domain.ValidateDynamicQuestionTypeDefinition(stimulusKinds, responseKinds); err != nil {
|
if err := domain.ValidateDynamicQuestionTypeDefinition(stimulusKinds, responseKinds); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := domain.ValidateDefinitionSchemas(stimulusSchema, responseSchema); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := domain.ValidatePersistableQuestionTypeDefinition(existing.Key, responseKinds); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
status := existing.Status
|
status := existing.Status
|
||||||
if input.Status != nil {
|
if input.Status != nil {
|
||||||
|
|
@ -342,10 +468,12 @@ func (s *Store) UpdateQuestionTypeDefinition(ctx context.Context, id int64, inpu
|
||||||
description = $3,
|
description = $3,
|
||||||
stimulus_component_kinds = $4,
|
stimulus_component_kinds = $4,
|
||||||
response_component_kinds = $5,
|
response_component_kinds = $5,
|
||||||
status = $6,
|
stimulus_schema = $6::jsonb,
|
||||||
|
response_schema = $7::jsonb,
|
||||||
|
status = $8,
|
||||||
updated_at = NOW()
|
updated_at = NOW()
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
`, id, displayName, toPgText(description), stimulusKinds, responseKinds, status)
|
`, id, displayName, toPgText(description), stimulusKinds, responseKinds, mustJSON(stimulusSchema), mustJSON(responseSchema), status)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -396,6 +524,7 @@ func (s *Store) CreateQuestion(ctx context.Context, input domain.CreateQuestionI
|
||||||
SampleAnswerVoicePrompt: toPgText(input.SampleAnswerVoicePrompt),
|
SampleAnswerVoicePrompt: toPgText(input.SampleAnswerVoicePrompt),
|
||||||
ImageUrl: toPgText(input.ImageURL),
|
ImageUrl: toPgText(input.ImageURL),
|
||||||
Column10: status,
|
Column10: status,
|
||||||
|
Column11: encodeDynamicPayload(input.DynamicPayload),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return domain.Question{}, err
|
return domain.Question{}, err
|
||||||
|
|
@ -450,7 +579,9 @@ func (s *Store) CreateQuestion(ctx context.Context, input domain.CreateQuestionI
|
||||||
return domain.Question{}, err
|
return domain.Question{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return questionToDomain(question), nil
|
created := questionToDomain(question)
|
||||||
|
created.QuestionTypeDefinitionID = input.QuestionTypeDefinitionID
|
||||||
|
return created, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) GetQuestionByID(ctx context.Context, id int64) (domain.Question, error) {
|
func (s *Store) GetQuestionByID(ctx context.Context, id int64) (domain.Question, error) {
|
||||||
|
|
@ -535,6 +666,8 @@ func (s *Store) ListQuestions(ctx context.Context, questionType, difficulty, sta
|
||||||
ID: r.ID,
|
ID: r.ID,
|
||||||
QuestionText: r.QuestionText,
|
QuestionText: r.QuestionText,
|
||||||
QuestionType: r.QuestionType,
|
QuestionType: r.QuestionType,
|
||||||
|
QuestionTypeDefinitionID: fromPgInt8(r.QuestionTypeDefinitionID),
|
||||||
|
DynamicPayload: parseDynamicPayload(r.DynamicPayload),
|
||||||
DifficultyLevel: fromPgText(r.DifficultyLevel),
|
DifficultyLevel: fromPgText(r.DifficultyLevel),
|
||||||
Points: r.Points,
|
Points: r.Points,
|
||||||
Explanation: fromPgText(r.Explanation),
|
Explanation: fromPgText(r.Explanation),
|
||||||
|
|
@ -571,6 +704,8 @@ func (s *Store) SearchQuestions(ctx context.Context, query string, limit, offset
|
||||||
ID: r.ID,
|
ID: r.ID,
|
||||||
QuestionText: r.QuestionText,
|
QuestionText: r.QuestionText,
|
||||||
QuestionType: r.QuestionType,
|
QuestionType: r.QuestionType,
|
||||||
|
QuestionTypeDefinitionID: fromPgInt8(r.QuestionTypeDefinitionID),
|
||||||
|
DynamicPayload: parseDynamicPayload(r.DynamicPayload),
|
||||||
DifficultyLevel: fromPgText(r.DifficultyLevel),
|
DifficultyLevel: fromPgText(r.DifficultyLevel),
|
||||||
Points: r.Points,
|
Points: r.Points,
|
||||||
Explanation: fromPgText(r.Explanation),
|
Explanation: fromPgText(r.Explanation),
|
||||||
|
|
@ -609,6 +744,7 @@ func (s *Store) UpdateQuestion(ctx context.Context, id int64, input domain.Creat
|
||||||
SampleAnswerVoicePrompt: toPgText(input.SampleAnswerVoicePrompt),
|
SampleAnswerVoicePrompt: toPgText(input.SampleAnswerVoicePrompt),
|
||||||
ImageUrl: toPgText(input.ImageURL),
|
ImageUrl: toPgText(input.ImageURL),
|
||||||
Status: status,
|
Status: status,
|
||||||
|
Column11: encodeDynamicPayload(input.DynamicPayload),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -979,6 +1115,7 @@ func (s *Store) GetQuestionSetItems(ctx context.Context, setID int64) ([]domain.
|
||||||
},
|
},
|
||||||
QuestionText: r.QuestionText,
|
QuestionText: r.QuestionText,
|
||||||
QuestionType: r.QuestionType,
|
QuestionType: r.QuestionType,
|
||||||
|
DynamicPayload: parseDynamicPayload(r.DynamicPayload),
|
||||||
DifficultyLevel: fromPgText(r.DifficultyLevel),
|
DifficultyLevel: fromPgText(r.DifficultyLevel),
|
||||||
Points: r.Points,
|
Points: r.Points,
|
||||||
Explanation: fromPgText(r.Explanation),
|
Explanation: fromPgText(r.Explanation),
|
||||||
|
|
@ -1023,6 +1160,7 @@ func (s *Store) GetQuestionSetItemsPaginated(ctx context.Context, setID int64, q
|
||||||
},
|
},
|
||||||
QuestionText: r.QuestionText,
|
QuestionText: r.QuestionText,
|
||||||
QuestionType: r.QuestionType,
|
QuestionType: r.QuestionType,
|
||||||
|
DynamicPayload: parseDynamicPayload(r.DynamicPayload),
|
||||||
DifficultyLevel: fromPgText(r.DifficultyLevel),
|
DifficultyLevel: fromPgText(r.DifficultyLevel),
|
||||||
Points: r.Points,
|
Points: r.Points,
|
||||||
Explanation: fromPgText(r.Explanation),
|
Explanation: fromPgText(r.Explanation),
|
||||||
|
|
@ -1054,6 +1192,7 @@ func (s *Store) GetPublishedQuestionsInSet(ctx context.Context, setID int64) ([]
|
||||||
},
|
},
|
||||||
QuestionText: r.QuestionText,
|
QuestionText: r.QuestionText,
|
||||||
QuestionType: r.QuestionType,
|
QuestionType: r.QuestionType,
|
||||||
|
DynamicPayload: parseDynamicPayload(r.DynamicPayload),
|
||||||
DifficultyLevel: fromPgText(r.DifficultyLevel),
|
DifficultyLevel: fromPgText(r.DifficultyLevel),
|
||||||
Points: r.Points,
|
Points: r.Points,
|
||||||
Explanation: fromPgText(r.Explanation),
|
Explanation: fromPgText(r.Explanation),
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ type createQuestionTypeDefinitionReq struct {
|
||||||
Description *string `json:"description"`
|
Description *string `json:"description"`
|
||||||
StimulusComponentKinds []string `json:"stimulus_component_kinds"`
|
StimulusComponentKinds []string `json:"stimulus_component_kinds"`
|
||||||
ResponseComponentKinds []string `json:"response_component_kinds"`
|
ResponseComponentKinds []string `json:"response_component_kinds"`
|
||||||
|
StimulusSchema []domain.DynamicElementDefinition `json:"stimulus_schema"`
|
||||||
|
ResponseSchema []domain.DynamicElementDefinition `json:"response_schema"`
|
||||||
Status *string `json:"status"`
|
Status *string `json:"status"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -32,6 +34,8 @@ type updateQuestionTypeDefinitionReq struct {
|
||||||
Description *string `json:"description"`
|
Description *string `json:"description"`
|
||||||
StimulusComponentKinds []string `json:"stimulus_component_kinds"`
|
StimulusComponentKinds []string `json:"stimulus_component_kinds"`
|
||||||
ResponseComponentKinds []string `json:"response_component_kinds"`
|
ResponseComponentKinds []string `json:"response_component_kinds"`
|
||||||
|
StimulusSchema []domain.DynamicElementDefinition `json:"stimulus_schema"`
|
||||||
|
ResponseSchema []domain.DynamicElementDefinition `json:"response_schema"`
|
||||||
Status *string `json:"status"`
|
Status *string `json:"status"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -54,7 +58,7 @@ func (h *Handler) GetQuestionTypeComponentCatalog(c *fiber.Ctx) error {
|
||||||
|
|
||||||
// ValidateQuestionTypeDefinition godoc
|
// ValidateQuestionTypeDefinition godoc
|
||||||
// @Summary Validate dynamic question-type definition
|
// @Summary Validate dynamic question-type definition
|
||||||
// @Description Validates selected stimulus and response component kinds for temporary question-type definitions
|
// @Description Validates selected stimulus and response component kinds for temporary question-type definitions (component-level validation only)
|
||||||
// @Tags questions
|
// @Tags questions
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
|
|
@ -86,7 +90,7 @@ func (h *Handler) ValidateQuestionTypeDefinition(c *fiber.Ctx) error {
|
||||||
|
|
||||||
// CreateQuestionTypeDefinition godoc
|
// CreateQuestionTypeDefinition godoc
|
||||||
// @Summary Create reusable question-type definition
|
// @Summary Create reusable question-type definition
|
||||||
// @Description Stores a reusable dynamic question-type definition for future question construction
|
// @Description Stores a reusable dynamic question-type definition for future question construction. Only runtime-mappable definitions are persisted.
|
||||||
// @Tags questions
|
// @Tags questions
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
|
|
@ -116,6 +120,8 @@ func (h *Handler) CreateQuestionTypeDefinition(c *fiber.Ctx) error {
|
||||||
Description: req.Description,
|
Description: req.Description,
|
||||||
StimulusComponentKinds: req.StimulusComponentKinds,
|
StimulusComponentKinds: req.StimulusComponentKinds,
|
||||||
ResponseComponentKinds: req.ResponseComponentKinds,
|
ResponseComponentKinds: req.ResponseComponentKinds,
|
||||||
|
StimulusSchema: req.StimulusSchema,
|
||||||
|
ResponseSchema: req.ResponseSchema,
|
||||||
Status: req.Status,
|
Status: req.Status,
|
||||||
IsSystem: false,
|
IsSystem: false,
|
||||||
})
|
})
|
||||||
|
|
@ -197,6 +203,7 @@ func (h *Handler) GetQuestionTypeDefinitionByID(c *fiber.Ctx) error {
|
||||||
|
|
||||||
// UpdateQuestionTypeDefinition godoc
|
// UpdateQuestionTypeDefinition godoc
|
||||||
// @Summary Update reusable question-type definition
|
// @Summary Update reusable question-type definition
|
||||||
|
// @Description Updates a reusable dynamic question-type definition. Updated definitions must remain runtime-mappable.
|
||||||
// @Tags questions
|
// @Tags questions
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
|
|
@ -233,6 +240,8 @@ func (h *Handler) UpdateQuestionTypeDefinition(c *fiber.Ctx) error {
|
||||||
Description: req.Description,
|
Description: req.Description,
|
||||||
StimulusComponentKinds: req.StimulusComponentKinds,
|
StimulusComponentKinds: req.StimulusComponentKinds,
|
||||||
ResponseComponentKinds: req.ResponseComponentKinds,
|
ResponseComponentKinds: req.ResponseComponentKinds,
|
||||||
|
StimulusSchema: req.StimulusSchema,
|
||||||
|
ResponseSchema: req.ResponseSchema,
|
||||||
Status: req.Status,
|
Status: req.Status,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ type createQuestionReq struct {
|
||||||
QuestionText string `json:"question_text" validate:"required"`
|
QuestionText string `json:"question_text" validate:"required"`
|
||||||
QuestionType string `json:"question_type"`
|
QuestionType string `json:"question_type"`
|
||||||
QuestionTypeDefinitionID *int64 `json:"question_type_definition_id"`
|
QuestionTypeDefinitionID *int64 `json:"question_type_definition_id"`
|
||||||
|
DynamicPayload *domain.DynamicQuestionPayload `json:"dynamic_payload"`
|
||||||
DifficultyLevel *string `json:"difficulty_level"`
|
DifficultyLevel *string `json:"difficulty_level"`
|
||||||
Points *int32 `json:"points"`
|
Points *int32 `json:"points"`
|
||||||
Explanation *string `json:"explanation"`
|
Explanation *string `json:"explanation"`
|
||||||
|
|
@ -58,6 +59,8 @@ type questionRes struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
QuestionText string `json:"question_text"`
|
QuestionText string `json:"question_text"`
|
||||||
QuestionType string `json:"question_type"`
|
QuestionType string `json:"question_type"`
|
||||||
|
QuestionTypeDefinitionID *int64 `json:"question_type_definition_id,omitempty"`
|
||||||
|
DynamicPayload *domain.DynamicQuestionPayload `json:"dynamic_payload,omitempty"`
|
||||||
DifficultyLevel *string `json:"difficulty_level,omitempty"`
|
DifficultyLevel *string `json:"difficulty_level,omitempty"`
|
||||||
Points int32 `json:"points"`
|
Points int32 `json:"points"`
|
||||||
Explanation *string `json:"explanation,omitempty"`
|
Explanation *string `json:"explanation,omitempty"`
|
||||||
|
|
@ -78,22 +81,7 @@ type listQuestionsRes struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolveQuestionTypeFromDefinition(def domain.QuestionTypeDefinition) string {
|
func resolveQuestionTypeFromDefinition(def domain.QuestionTypeDefinition) string {
|
||||||
key := strings.ToLower(strings.TrimSpace(def.Key))
|
return domain.ResolveRuntimeQuestionTypeFromDefinition(def.Key, def.ResponseComponentKinds)
|
||||||
if key == "true_false" || key == "true-false" {
|
|
||||||
return "TRUE_FALSE"
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, kind := range def.ResponseComponentKinds {
|
|
||||||
switch strings.TrimSpace(kind) {
|
|
||||||
case "AUDIO_RESPONSE":
|
|
||||||
return "AUDIO"
|
|
||||||
case "MULTIPLE_CHOICE":
|
|
||||||
return "MCQ"
|
|
||||||
case "SHORT_ANSWER", "TEXT_INPUT", "SELECT_MISSING_WORDS", "MATCHING_ANSWER", "LABEL_SELECTION":
|
|
||||||
return "SHORT_ANSWER"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeRuntimeQuestionType(v string) string {
|
func normalizeRuntimeQuestionType(v string) string {
|
||||||
|
|
@ -102,7 +90,7 @@ func normalizeRuntimeQuestionType(v string) string {
|
||||||
|
|
||||||
// CreateQuestion godoc
|
// CreateQuestion godoc
|
||||||
// @Summary Create a new question
|
// @Summary Create a new question
|
||||||
// @Description Creates a new question with options (for MCQ/TRUE_FALSE) or short answers (for SHORT_ANSWER)
|
// @Description Creates a new question with options (for MCQ/TRUE_FALSE) or short answers (for SHORT_ANSWER). Supports question_type_definition_id for dynamic builder-linked questions.
|
||||||
// @Tags questions
|
// @Tags questions
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
|
|
@ -129,22 +117,33 @@ func (h *Handler) CreateQuestion(c *fiber.Ctx) error {
|
||||||
Error: err.Error(),
|
Error: err.Error(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
inferred := resolveQuestionTypeFromDefinition(def)
|
|
||||||
if inferred == "" {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Unsupported dynamic question type definition",
|
|
||||||
Error: "unable to map definition to runtime question_type",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if questionType == "" {
|
if questionType == "" {
|
||||||
questionType = inferred
|
questionType = "DYNAMIC"
|
||||||
}
|
}
|
||||||
if questionType != inferred {
|
if questionType != "DYNAMIC" {
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
Message: "Mismatched question_type and definition",
|
Message: "Invalid question_type for dynamic definition",
|
||||||
Error: "question_type must match the selected question_type_definition_id",
|
Error: "question_type must be DYNAMIC when question_type_definition_id is provided",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
if req.DynamicPayload == nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Missing dynamic_payload",
|
||||||
|
Error: "dynamic_payload is required when question_type_definition_id is provided",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if err := domain.ValidateDynamicPayloadAgainstDefinition(*req.DynamicPayload, def); err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Invalid dynamic_payload",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if req.QuestionTypeDefinitionID == nil && req.DynamicPayload != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Invalid dynamic_payload usage",
|
||||||
|
Error: "dynamic_payload requires question_type_definition_id",
|
||||||
|
})
|
||||||
}
|
}
|
||||||
if questionType == "" {
|
if questionType == "" {
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
|
@ -153,11 +152,17 @@ func (h *Handler) CreateQuestion(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
switch questionType {
|
switch questionType {
|
||||||
case "MCQ", "TRUE_FALSE", "SHORT_ANSWER", "AUDIO":
|
case "MCQ", "TRUE_FALSE", "SHORT_ANSWER", "AUDIO", "DYNAMIC":
|
||||||
default:
|
default:
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
Message: "Invalid question_type",
|
Message: "Invalid question_type",
|
||||||
Error: "question_type must be one of MCQ, TRUE_FALSE, SHORT_ANSWER, AUDIO",
|
Error: "question_type must be one of MCQ, TRUE_FALSE, SHORT_ANSWER, AUDIO, DYNAMIC",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if questionType == "DYNAMIC" && req.QuestionTypeDefinitionID == nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Invalid dynamic question",
|
||||||
|
Error: "question_type_definition_id is required for DYNAMIC question_type",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -184,6 +189,7 @@ func (h *Handler) CreateQuestion(c *fiber.Ctx) error {
|
||||||
QuestionText: req.QuestionText,
|
QuestionText: req.QuestionText,
|
||||||
QuestionType: questionType,
|
QuestionType: questionType,
|
||||||
QuestionTypeDefinitionID: req.QuestionTypeDefinitionID,
|
QuestionTypeDefinitionID: req.QuestionTypeDefinitionID,
|
||||||
|
DynamicPayload: req.DynamicPayload,
|
||||||
DifficultyLevel: req.DifficultyLevel,
|
DifficultyLevel: req.DifficultyLevel,
|
||||||
Points: req.Points,
|
Points: req.Points,
|
||||||
Explanation: req.Explanation,
|
Explanation: req.Explanation,
|
||||||
|
|
@ -221,6 +227,8 @@ func (h *Handler) CreateQuestion(c *fiber.Ctx) error {
|
||||||
ID: question.ID,
|
ID: question.ID,
|
||||||
QuestionText: question.QuestionText,
|
QuestionText: question.QuestionText,
|
||||||
QuestionType: question.QuestionType,
|
QuestionType: question.QuestionType,
|
||||||
|
QuestionTypeDefinitionID: question.QuestionTypeDefinitionID,
|
||||||
|
DynamicPayload: question.DynamicPayload,
|
||||||
DifficultyLevel: question.DifficultyLevel,
|
DifficultyLevel: question.DifficultyLevel,
|
||||||
Points: question.Points,
|
Points: question.Points,
|
||||||
Explanation: question.Explanation,
|
Explanation: question.Explanation,
|
||||||
|
|
@ -292,6 +300,8 @@ func (h *Handler) GetQuestionByID(c *fiber.Ctx) error {
|
||||||
ID: question.ID,
|
ID: question.ID,
|
||||||
QuestionText: question.QuestionText,
|
QuestionText: question.QuestionText,
|
||||||
QuestionType: question.QuestionType,
|
QuestionType: question.QuestionType,
|
||||||
|
QuestionTypeDefinitionID: question.QuestionTypeDefinitionID,
|
||||||
|
DynamicPayload: question.DynamicPayload,
|
||||||
DifficultyLevel: question.DifficultyLevel,
|
DifficultyLevel: question.DifficultyLevel,
|
||||||
Points: question.Points,
|
Points: question.Points,
|
||||||
Explanation: question.Explanation,
|
Explanation: question.Explanation,
|
||||||
|
|
@ -357,6 +367,8 @@ func (h *Handler) ListQuestions(c *fiber.Ctx) error {
|
||||||
ID: q.ID,
|
ID: q.ID,
|
||||||
QuestionText: q.QuestionText,
|
QuestionText: q.QuestionText,
|
||||||
QuestionType: q.QuestionType,
|
QuestionType: q.QuestionType,
|
||||||
|
QuestionTypeDefinitionID: q.QuestionTypeDefinitionID,
|
||||||
|
DynamicPayload: q.DynamicPayload,
|
||||||
DifficultyLevel: q.DifficultyLevel,
|
DifficultyLevel: q.DifficultyLevel,
|
||||||
Points: q.Points,
|
Points: q.Points,
|
||||||
Explanation: q.Explanation,
|
Explanation: q.Explanation,
|
||||||
|
|
@ -415,6 +427,8 @@ func (h *Handler) SearchQuestions(c *fiber.Ctx) error {
|
||||||
ID: q.ID,
|
ID: q.ID,
|
||||||
QuestionText: q.QuestionText,
|
QuestionText: q.QuestionText,
|
||||||
QuestionType: q.QuestionType,
|
QuestionType: q.QuestionType,
|
||||||
|
QuestionTypeDefinitionID: q.QuestionTypeDefinitionID,
|
||||||
|
DynamicPayload: q.DynamicPayload,
|
||||||
DifficultyLevel: q.DifficultyLevel,
|
DifficultyLevel: q.DifficultyLevel,
|
||||||
Points: q.Points,
|
Points: q.Points,
|
||||||
Status: q.Status,
|
Status: q.Status,
|
||||||
|
|
@ -435,6 +449,7 @@ type updateQuestionReq struct {
|
||||||
QuestionText *string `json:"question_text"`
|
QuestionText *string `json:"question_text"`
|
||||||
QuestionType *string `json:"question_type"`
|
QuestionType *string `json:"question_type"`
|
||||||
QuestionTypeDefinitionID *int64 `json:"question_type_definition_id"`
|
QuestionTypeDefinitionID *int64 `json:"question_type_definition_id"`
|
||||||
|
DynamicPayload *domain.DynamicQuestionPayload `json:"dynamic_payload"`
|
||||||
DifficultyLevel *string `json:"difficulty_level"`
|
DifficultyLevel *string `json:"difficulty_level"`
|
||||||
Points *int32 `json:"points"`
|
Points *int32 `json:"points"`
|
||||||
Explanation *string `json:"explanation"`
|
Explanation *string `json:"explanation"`
|
||||||
|
|
@ -450,7 +465,7 @@ type updateQuestionReq struct {
|
||||||
|
|
||||||
// UpdateQuestion godoc
|
// UpdateQuestion godoc
|
||||||
// @Summary Update a question
|
// @Summary Update a question
|
||||||
// @Description Updates a question and optionally replaces its options/short answers
|
// @Description Updates a question and optionally replaces its options/short answers. Supports question_type_definition_id for dynamic builder-linked questions.
|
||||||
// @Tags questions
|
// @Tags questions
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
|
|
@ -478,6 +493,14 @@ func (h *Handler) UpdateQuestion(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
existingQuestion, err := h.questionsSvc.GetQuestionByID(c.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Question not found",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
var options []domain.CreateQuestionOptionInput
|
var options []domain.CreateQuestionOptionInput
|
||||||
for _, opt := range req.Options {
|
for _, opt := range req.Options {
|
||||||
options = append(options, domain.CreateQuestionOptionInput{
|
options = append(options, domain.CreateQuestionOptionInput{
|
||||||
|
|
@ -495,44 +518,80 @@ func (h *Handler) UpdateQuestion(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
questionText := ""
|
questionText := existingQuestion.QuestionText
|
||||||
if req.QuestionText != nil {
|
if req.QuestionText != nil {
|
||||||
questionText = *req.QuestionText
|
questionText = *req.QuestionText
|
||||||
}
|
}
|
||||||
questionType := ""
|
questionType := normalizeRuntimeQuestionType(existingQuestion.QuestionType)
|
||||||
if req.QuestionType != nil {
|
if req.QuestionType != nil {
|
||||||
questionType = normalizeRuntimeQuestionType(*req.QuestionType)
|
questionType = normalizeRuntimeQuestionType(*req.QuestionType)
|
||||||
}
|
}
|
||||||
|
effectiveDefinitionID := existingQuestion.QuestionTypeDefinitionID
|
||||||
if req.QuestionTypeDefinitionID != nil {
|
if req.QuestionTypeDefinitionID != nil {
|
||||||
def, err := h.questionsSvc.GetQuestionTypeDefinitionByID(c.Context(), *req.QuestionTypeDefinitionID)
|
effectiveDefinitionID = req.QuestionTypeDefinitionID
|
||||||
|
}
|
||||||
|
effectiveDynamicPayload := existingQuestion.DynamicPayload
|
||||||
|
if req.DynamicPayload != nil {
|
||||||
|
effectiveDynamicPayload = req.DynamicPayload
|
||||||
|
}
|
||||||
|
|
||||||
|
if effectiveDefinitionID != nil {
|
||||||
|
def, err := h.questionsSvc.GetQuestionTypeDefinitionByID(c.Context(), *effectiveDefinitionID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
Message: "Invalid question_type_definition_id",
|
Message: "Invalid question_type_definition_id",
|
||||||
Error: err.Error(),
|
Error: err.Error(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
inferred := resolveQuestionTypeFromDefinition(def)
|
|
||||||
if inferred == "" {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Unsupported dynamic question type definition",
|
|
||||||
Error: "unable to map definition to runtime question_type",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if questionType == "" {
|
if questionType == "" {
|
||||||
questionType = inferred
|
questionType = "DYNAMIC"
|
||||||
}
|
}
|
||||||
if questionType != inferred {
|
if questionType != "DYNAMIC" {
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
Message: "Mismatched question_type and definition",
|
Message: "Invalid question_type for dynamic definition",
|
||||||
Error: "question_type must match the selected question_type_definition_id",
|
Error: "question_type must be DYNAMIC when question_type_definition_id is provided",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
if effectiveDynamicPayload == nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Missing dynamic_payload",
|
||||||
|
Error: "dynamic_payload is required for dynamic questions",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if err := domain.ValidateDynamicPayloadAgainstDefinition(*effectiveDynamicPayload, def); err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Invalid dynamic_payload",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if effectiveDefinitionID == nil && req.DynamicPayload != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Invalid dynamic_payload usage",
|
||||||
|
Error: "dynamic_payload requires question_type_definition_id",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
switch questionType {
|
||||||
|
case "MCQ", "TRUE_FALSE", "SHORT_ANSWER", "AUDIO", "DYNAMIC":
|
||||||
|
default:
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Invalid question_type",
|
||||||
|
Error: "question_type must be one of MCQ, TRUE_FALSE, SHORT_ANSWER, AUDIO, DYNAMIC",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if questionType == "DYNAMIC" && effectiveDefinitionID == nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Invalid dynamic question",
|
||||||
|
Error: "question_type_definition_id is required for DYNAMIC question_type",
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
input := domain.CreateQuestionInput{
|
input := domain.CreateQuestionInput{
|
||||||
QuestionText: questionText,
|
QuestionText: questionText,
|
||||||
QuestionType: questionType,
|
QuestionType: questionType,
|
||||||
QuestionTypeDefinitionID: req.QuestionTypeDefinitionID,
|
QuestionTypeDefinitionID: effectiveDefinitionID,
|
||||||
|
DynamicPayload: effectiveDynamicPayload,
|
||||||
DifficultyLevel: req.DifficultyLevel,
|
DifficultyLevel: req.DifficultyLevel,
|
||||||
Points: req.Points,
|
Points: req.Points,
|
||||||
Explanation: req.Explanation,
|
Explanation: req.Explanation,
|
||||||
|
|
@ -1158,6 +1217,7 @@ type questionSetItemRes struct {
|
||||||
DisplayOrder int32 `json:"display_order"`
|
DisplayOrder int32 `json:"display_order"`
|
||||||
QuestionText string `json:"question_text"`
|
QuestionText string `json:"question_text"`
|
||||||
QuestionType string `json:"question_type"`
|
QuestionType string `json:"question_type"`
|
||||||
|
DynamicPayload *domain.DynamicQuestionPayload `json:"dynamic_payload,omitempty"`
|
||||||
DifficultyLevel *string `json:"difficulty_level,omitempty"`
|
DifficultyLevel *string `json:"difficulty_level,omitempty"`
|
||||||
Points int32 `json:"points"`
|
Points int32 `json:"points"`
|
||||||
Explanation *string `json:"explanation,omitempty"`
|
Explanation *string `json:"explanation,omitempty"`
|
||||||
|
|
@ -1186,6 +1246,7 @@ func questionSetItemsToRes(items []domain.QuestionSetItemWithQuestion) []questio
|
||||||
DisplayOrder: item.DisplayOrder,
|
DisplayOrder: item.DisplayOrder,
|
||||||
QuestionText: item.QuestionText,
|
QuestionText: item.QuestionText,
|
||||||
QuestionType: item.QuestionType,
|
QuestionType: item.QuestionType,
|
||||||
|
DynamicPayload: item.DynamicPayload,
|
||||||
DifficultyLevel: item.DifficultyLevel,
|
DifficultyLevel: item.DifficultyLevel,
|
||||||
Points: item.Points,
|
Points: item.Points,
|
||||||
Explanation: item.Explanation,
|
Explanation: item.Explanation,
|
||||||
|
|
@ -1334,6 +1395,8 @@ func (h *Handler) GetQuestionsInSet(c *fiber.Ctx) error {
|
||||||
ID: question.ID,
|
ID: question.ID,
|
||||||
QuestionText: question.QuestionText,
|
QuestionText: question.QuestionText,
|
||||||
QuestionType: question.QuestionType,
|
QuestionType: question.QuestionType,
|
||||||
|
QuestionTypeDefinitionID: question.QuestionTypeDefinitionID,
|
||||||
|
DynamicPayload: question.DynamicPayload,
|
||||||
DifficultyLevel: question.DifficultyLevel,
|
DifficultyLevel: question.DifficultyLevel,
|
||||||
Points: question.Points,
|
Points: question.Points,
|
||||||
Explanation: question.Explanation,
|
Explanation: question.Explanation,
|
||||||
|
|
|
||||||
|
|
@ -1,457 +0,0 @@
|
||||||
{
|
|
||||||
"info": {
|
|
||||||
"_postman_id": "f7c2e4a1-8b3d-4e9f-a2c6-11dd99ee5501",
|
|
||||||
"name": "Yimaru Exam Prep (Duolingo)",
|
|
||||||
"description": "Exam-prep tree API (`/api/v1/exam-prep/...`): catalog courses → units → modules → lessons → practices. Requires Bearer token.\n\n**Courses** = `catalog-courses` in the backend. Set collection variables before chaining requests.",
|
|
||||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
|
||||||
},
|
|
||||||
"auth": {
|
|
||||||
"type": "bearer",
|
|
||||||
"bearer": [
|
|
||||||
{
|
|
||||||
"key": "token",
|
|
||||||
"value": "{{accessToken}}",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"variable": [
|
|
||||||
{
|
|
||||||
"key": "baseUrl",
|
|
||||||
"value": "http://localhost:8080"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "accessToken",
|
|
||||||
"value": ""
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "catalogCourseId",
|
|
||||||
"value": "1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "unitId",
|
|
||||||
"value": "1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "moduleId",
|
|
||||||
"value": "1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "lessonId",
|
|
||||||
"value": "1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "practiceId",
|
|
||||||
"value": "1"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"item": [
|
|
||||||
{
|
|
||||||
"name": "Duolingo",
|
|
||||||
"item": [
|
|
||||||
{
|
|
||||||
"name": "Courses",
|
|
||||||
"description": "Backend route group: **`catalog-courses`** (`exam_prep.catalog_courses.*`)",
|
|
||||||
"item": [
|
|
||||||
{
|
|
||||||
"name": "Create catalog course",
|
|
||||||
"request": {
|
|
||||||
"method": "POST",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"name\": \"IELTS Prep\",\n \"description\": \"Optional description\",\n \"thumbnail\": null\n}"
|
|
||||||
},
|
|
||||||
"url": "{{baseUrl}}/api/v1/exam-prep/catalog-courses",
|
|
||||||
"description": "Permission: `exam_prep.catalog_courses.create`"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "List catalog courses",
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"url": "{{baseUrl}}/api/v1/exam-prep/catalog-courses?limit=20&offset=0",
|
|
||||||
"description": "Permission: `exam_prep.catalog_courses.list`"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Reorder catalog courses",
|
|
||||||
"request": {
|
|
||||||
"method": "PUT",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"ordered_ids\": [1, 2, 3]\n}"
|
|
||||||
},
|
|
||||||
"url": "{{baseUrl}}/api/v1/exam-prep/catalog-courses/reorder",
|
|
||||||
"description": "Permission: `exam_prep.catalog_courses.reorder`. Must include every id in scope exactly once."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Get catalog course by ID",
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"url": "{{baseUrl}}/api/v1/exam-prep/catalog-courses/{{catalogCourseId}}",
|
|
||||||
"description": "Permission: `exam_prep.catalog_courses.get`"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Update catalog course",
|
|
||||||
"request": {
|
|
||||||
"method": "PUT",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"name\": \"Updated name\",\n \"description\": null,\n \"thumbnail\": null,\n \"sort_order\": 1\n}"
|
|
||||||
},
|
|
||||||
"url": "{{baseUrl}}/api/v1/exam-prep/catalog-courses/{{catalogCourseId}}",
|
|
||||||
"description": "Permission: `exam_prep.catalog_courses.update`"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Delete catalog course",
|
|
||||||
"request": {
|
|
||||||
"method": "DELETE",
|
|
||||||
"url": "{{baseUrl}}/api/v1/exam-prep/catalog-courses/{{catalogCourseId}}",
|
|
||||||
"description": "Permission: `exam_prep.catalog_courses.delete`"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Units",
|
|
||||||
"description": "Nested under catalog course (`exam_prep.units.*`)",
|
|
||||||
"item": [
|
|
||||||
{
|
|
||||||
"name": "Create unit",
|
|
||||||
"request": {
|
|
||||||
"method": "POST",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"name\": \"Grammar foundations\",\n \"description\": null,\n \"thumbnail\": null\n}"
|
|
||||||
},
|
|
||||||
"url": "{{baseUrl}}/api/v1/exam-prep/catalog-courses/{{catalogCourseId}}/units",
|
|
||||||
"description": "Permission: `exam_prep.units.create`"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "List units by catalog course",
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"url": "{{baseUrl}}/api/v1/exam-prep/catalog-courses/{{catalogCourseId}}/units?limit=20&offset=0",
|
|
||||||
"description": "Permission: `exam_prep.units.list`"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Reorder units in catalog course",
|
|
||||||
"request": {
|
|
||||||
"method": "PUT",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"ordered_ids\": [1, 2, 3]\n}"
|
|
||||||
},
|
|
||||||
"url": "{{baseUrl}}/api/v1/exam-prep/catalog-courses/{{catalogCourseId}}/units/reorder",
|
|
||||||
"description": "Permission: `exam_prep.units.reorder`"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Get unit by ID",
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"url": "{{baseUrl}}/api/v1/exam-prep/units/{{unitId}}",
|
|
||||||
"description": "Permission: `exam_prep.units.get`"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Update unit",
|
|
||||||
"request": {
|
|
||||||
"method": "PUT",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"name\": \"Updated unit\",\n \"description\": null,\n \"thumbnail\": null,\n \"sort_order\": 1\n}"
|
|
||||||
},
|
|
||||||
"url": "{{baseUrl}}/api/v1/exam-prep/units/{{unitId}}",
|
|
||||||
"description": "Permission: `exam_prep.units.update`"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Delete unit",
|
|
||||||
"request": {
|
|
||||||
"method": "DELETE",
|
|
||||||
"url": "{{baseUrl}}/api/v1/exam-prep/units/{{unitId}}",
|
|
||||||
"description": "Permission: `exam_prep.units.delete`"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Modules",
|
|
||||||
"description": "Exam-prep **`unit_modules`** (`exam_prep.modules.*`)",
|
|
||||||
"item": [
|
|
||||||
{
|
|
||||||
"name": "Create module",
|
|
||||||
"request": {
|
|
||||||
"method": "POST",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"name\": \"Present tense\",\n \"description\": null,\n \"thumbnail\": null,\n \"icon\": null\n}"
|
|
||||||
},
|
|
||||||
"url": "{{baseUrl}}/api/v1/exam-prep/units/{{unitId}}/modules",
|
|
||||||
"description": "Permission: `exam_prep.modules.create`"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "List modules by unit",
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"url": "{{baseUrl}}/api/v1/exam-prep/units/{{unitId}}/modules?limit=20&offset=0",
|
|
||||||
"description": "Permission: `exam_prep.modules.list`"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Reorder modules in unit",
|
|
||||||
"request": {
|
|
||||||
"method": "PUT",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"ordered_ids\": [1, 2, 3]\n}"
|
|
||||||
},
|
|
||||||
"url": "{{baseUrl}}/api/v1/exam-prep/units/{{unitId}}/modules/reorder",
|
|
||||||
"description": "Permission: `exam_prep.modules.reorder`"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Get module by ID",
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"url": "{{baseUrl}}/api/v1/exam-prep/modules/{{moduleId}}",
|
|
||||||
"description": "Permission: `exam_prep.modules.get`"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Update module",
|
|
||||||
"request": {
|
|
||||||
"method": "PUT",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"name\": \"Updated module\",\n \"description\": null,\n \"thumbnail\": null,\n \"icon\": null,\n \"sort_order\": 1\n}"
|
|
||||||
},
|
|
||||||
"url": "{{baseUrl}}/api/v1/exam-prep/modules/{{moduleId}}",
|
|
||||||
"description": "Permission: `exam_prep.modules.update`"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Delete module",
|
|
||||||
"request": {
|
|
||||||
"method": "DELETE",
|
|
||||||
"url": "{{baseUrl}}/api/v1/exam-prep/modules/{{moduleId}}",
|
|
||||||
"description": "Permission: `exam_prep.modules.delete`"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Lessons",
|
|
||||||
"description": "`exam_prep.lessons.*`",
|
|
||||||
"item": [
|
|
||||||
{
|
|
||||||
"name": "Create lesson",
|
|
||||||
"request": {
|
|
||||||
"method": "POST",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"title\": \"Intro video\",\n \"video_url\": \"https://example.com/video\",\n \"thumbnail\": null,\n \"description\": null\n}"
|
|
||||||
},
|
|
||||||
"url": "{{baseUrl}}/api/v1/exam-prep/modules/{{moduleId}}/lessons",
|
|
||||||
"description": "Permission: `exam_prep.lessons.create`"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "List lessons by module",
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"url": "{{baseUrl}}/api/v1/exam-prep/modules/{{moduleId}}/lessons?limit=20&offset=0",
|
|
||||||
"description": "Permission: `exam_prep.lessons.list_by_module`"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Reorder lessons in module",
|
|
||||||
"request": {
|
|
||||||
"method": "PUT",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"ordered_ids\": [1, 2, 3]\n}"
|
|
||||||
},
|
|
||||||
"url": "{{baseUrl}}/api/v1/exam-prep/modules/{{moduleId}}/lessons/reorder",
|
|
||||||
"description": "Permission: `exam_prep.lessons.reorder`"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Get lesson by ID",
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"url": "{{baseUrl}}/api/v1/exam-prep/lessons/{{lessonId}}",
|
|
||||||
"description": "Permission: `exam_prep.lessons.get`"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Update lesson",
|
|
||||||
"request": {
|
|
||||||
"method": "PUT",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"title\": \"Updated lesson\",\n \"video_url\": null,\n \"thumbnail\": null,\n \"description\": null,\n \"sort_order\": 1\n}"
|
|
||||||
},
|
|
||||||
"url": "{{baseUrl}}/api/v1/exam-prep/lessons/{{lessonId}}",
|
|
||||||
"description": "Permission: `exam_prep.lessons.update`"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Delete lesson",
|
|
||||||
"request": {
|
|
||||||
"method": "DELETE",
|
|
||||||
"url": "{{baseUrl}}/api/v1/exam-prep/lessons/{{lessonId}}",
|
|
||||||
"description": "Permission: `exam_prep.lessons.delete`"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Practices",
|
|
||||||
"description": "Tied to lesson; **`question_set_id`** references shared `question_sets`. `exam_prep.practices.*`",
|
|
||||||
"item": [
|
|
||||||
{
|
|
||||||
"name": "Create practice",
|
|
||||||
"request": {
|
|
||||||
"method": "POST",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"title\": \"Drill: articles\",\n \"story_description\": null,\n \"story_image\": null,\n \"persona_id\": null,\n \"question_set_id\": 1,\n \"quick_tips\": null\n}"
|
|
||||||
},
|
|
||||||
"url": "{{baseUrl}}/api/v1/exam-prep/lessons/{{lessonId}}/practices",
|
|
||||||
"description": "Permission: `exam_prep.practices.create`"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "List practices by lesson",
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"url": "{{baseUrl}}/api/v1/exam-prep/lessons/{{lessonId}}/practices?limit=20&offset=0",
|
|
||||||
"description": "Permission: `exam_prep.practices.list_by_lesson`"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Get practice by ID",
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"url": "{{baseUrl}}/api/v1/exam-prep/practices/{{practiceId}}",
|
|
||||||
"description": "Permission: `exam_prep.practices.get`"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Update practice",
|
|
||||||
"request": {
|
|
||||||
"method": "PUT",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"title\": \"Updated practice\",\n \"story_description\": null,\n \"story_image\": null,\n \"persona_id\": null,\n \"question_set_id\": 1,\n \"quick_tips\": null\n}"
|
|
||||||
},
|
|
||||||
"url": "{{baseUrl}}/api/v1/exam-prep/practices/{{practiceId}}",
|
|
||||||
"description": "Permission: `exam_prep.practices.update`. Omit fields you do not change."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Delete practice",
|
|
||||||
"request": {
|
|
||||||
"method": "DELETE",
|
|
||||||
"url": "{{baseUrl}}/api/v1/exam-prep/practices/{{practiceId}}",
|
|
||||||
"description": "Permission: `exam_prep.practices.delete`"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -1,148 +1,240 @@
|
||||||
{
|
{
|
||||||
"info": {
|
"info": {
|
||||||
"name": "Yimaru Dynamic Question Type Builder API",
|
"_postman_id": "c9e5e08f-e878-4d8a-b24c-5f866eb4c735",
|
||||||
"_postman_id": "f0f9c795-09aa-4f5a-9cc0-1f2fcb0f1b01",
|
"name": "Dynamic Question Type Builder - Runtime Complete",
|
||||||
"description": "Complete Postman collection for the dynamic question type builder feature, including catalog, validation, reusable type-definition CRUD, and question create/update using question_type_definition_id.",
|
"description": "Complete collection for the new dynamic question type builder flow: component catalog, definition schema CRUD, dynamic payload validation, and DYNAMIC question lifecycle.",
|
||||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||||
},
|
},
|
||||||
"variable": [
|
|
||||||
{
|
|
||||||
"key": "baseUrl",
|
|
||||||
"value": "http://localhost:8080"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "apiPrefix",
|
|
||||||
"value": "/api/v1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "token",
|
|
||||||
"value": ""
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "questionTypeDefinitionId",
|
|
||||||
"value": ""
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "questionId",
|
|
||||||
"value": ""
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"auth": {
|
"auth": {
|
||||||
"type": "bearer",
|
"type": "bearer",
|
||||||
"bearer": [
|
"bearer": [
|
||||||
{
|
{
|
||||||
"key": "token",
|
"key": "token",
|
||||||
"value": "{{token}}",
|
"value": "{{access_token}}",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "base_url",
|
||||||
|
"value": "http://localhost:8080"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "access_token",
|
||||||
|
"value": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "definition_key",
|
||||||
|
"value": "dynamic_builder_{{$timestamp}}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "dynamic_definition_id",
|
||||||
|
"value": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "dynamic_question_id",
|
||||||
|
"value": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
"item": [
|
"item": [
|
||||||
{
|
{
|
||||||
"name": "01 - Builder Component Catalog",
|
"name": "01 - Builder Catalog and Validation",
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "Get Component Catalog",
|
||||||
"request": {
|
"request": {
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"header": [],
|
"header": [],
|
||||||
"url": {
|
"url": {
|
||||||
"raw": "{{baseUrl}}{{apiPrefix}}/questions/component-catalog",
|
"raw": "{{base_url}}/api/v1/questions/component-catalog",
|
||||||
"host": [
|
"host": [
|
||||||
"{{baseUrl}}"
|
"{{base_url}}"
|
||||||
],
|
],
|
||||||
"path": [
|
"path": [
|
||||||
"{{apiPrefix}}",
|
"api",
|
||||||
|
"v1",
|
||||||
"questions",
|
"questions",
|
||||||
"component-catalog"
|
"component-catalog"
|
||||||
]
|
]
|
||||||
},
|
}
|
||||||
"description": "Returns supported stimulus and response component kinds for dynamic type definitions."
|
|
||||||
},
|
},
|
||||||
"response": []
|
"response": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "02 - Validate Dynamic Type Definition",
|
"name": "Validate Definition (Valid)",
|
||||||
"request": {
|
|
||||||
"method": "POST",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"stimulus_component_kinds\": [\"INSTRUCTION\", \"TEXT_PASSAGE\"],\n \"response_component_kinds\": [\"MULTIPLE_CHOICE\"]\n}"
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"raw": "{{baseUrl}}{{apiPrefix}}/questions/validate-question-type-definition",
|
|
||||||
"host": [
|
|
||||||
"{{baseUrl}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"{{apiPrefix}}",
|
|
||||||
"questions",
|
|
||||||
"validate-question-type-definition"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"description": "Validates a candidate dynamic question-type definition before saving."
|
|
||||||
},
|
|
||||||
"response": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "03 - Create Question Type Definition (MCQ Dynamic)",
|
|
||||||
"request": {
|
|
||||||
"method": "POST",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"key\": \"mcq_dynamic_vocab\",\n \"display_name\": \"MCQ Dynamic Vocabulary\",\n \"description\": \"Dynamic multiple-choice template for vocabulary checks.\",\n \"stimulus_component_kinds\": [\"INSTRUCTION\", \"TEXT_PASSAGE\"],\n \"response_component_kinds\": [\"MULTIPLE_CHOICE\"],\n \"status\": \"ACTIVE\"\n}"
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"raw": "{{baseUrl}}{{apiPrefix}}/questions/type-definitions",
|
|
||||||
"host": [
|
|
||||||
"{{baseUrl}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"{{apiPrefix}}",
|
|
||||||
"questions",
|
|
||||||
"type-definitions"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"description": "Creates a reusable dynamic question-type definition."
|
|
||||||
},
|
|
||||||
"event": [
|
"event": [
|
||||||
{
|
{
|
||||||
"listen": "test",
|
"listen": "test",
|
||||||
"script": {
|
"script": {
|
||||||
"exec": [
|
"exec": [
|
||||||
"pm.test('Status is 201', function () { pm.response.to.have.status(201); });",
|
"pm.test(\"Status code is 200\", function () {",
|
||||||
"var json = pm.response.json();",
|
" pm.response.to.have.status(200);",
|
||||||
"if (json && json.data && json.data.id) {",
|
"});",
|
||||||
" pm.collectionVariables.set('questionTypeDefinitionId', json.data.id);",
|
"const body = pm.response.json();",
|
||||||
"}"
|
"pm.test(\"valid = true\", function () {",
|
||||||
|
" pm.expect(body.data.valid).to.eql(true);",
|
||||||
|
"});"
|
||||||
],
|
],
|
||||||
"type": "text/javascript"
|
"type": "text/javascript"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"stimulus_component_kinds\": [\"QUESTION_TEXT\", \"IMAGE\", \"TABLE\"],\n \"response_component_kinds\": [\"OPTION\", \"ANSWER_TIMER\"]\n}"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/questions/validate-question-type-definition",
|
||||||
|
"host": [
|
||||||
|
"{{base_url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"v1",
|
||||||
|
"questions",
|
||||||
|
"validate-question-type-definition"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"response": []
|
"response": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "04 - List Question Type Definitions (Include System)",
|
"name": "Validate Definition (Invalid - Timer Only)",
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"exec": [
|
||||||
|
"pm.test(\"Status code is 400\", function () {",
|
||||||
|
" pm.response.to.have.status(400);",
|
||||||
|
"});"
|
||||||
|
],
|
||||||
|
"type": "text/javascript"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"stimulus_component_kinds\": [\"QUESTION_TEXT\"],\n \"response_component_kinds\": [\"ANSWER_TIMER\"]\n}"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/questions/validate-question-type-definition",
|
||||||
|
"host": [
|
||||||
|
"{{base_url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"v1",
|
||||||
|
"questions",
|
||||||
|
"validate-question-type-definition"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "02 - Definition CRUD (Schema-Driven)",
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "Create Dynamic Definition (with schema)",
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"exec": [
|
||||||
|
"pm.test(\"Status code is 201\", function () {",
|
||||||
|
" pm.response.to.have.status(201);",
|
||||||
|
"});",
|
||||||
|
"const body = pm.response.json();",
|
||||||
|
"pm.collectionVariables.set(\"dynamic_definition_id\", body.data.id);"
|
||||||
|
],
|
||||||
|
"type": "text/javascript"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"key\": \"{{definition_key}}\",\n \"display_name\": \"Dynamic Grammar + Visual MCQ\",\n \"description\": \"Reusable dynamic question type with question text, image, table, and option responses.\",\n \"stimulus_component_kinds\": [\"QUESTION_TEXT\", \"IMAGE\", \"TABLE\"],\n \"response_component_kinds\": [\"OPTION\", \"ANSWER_TIMER\"],\n \"stimulus_schema\": [\n {\n \"id\": \"prompt\",\n \"kind\": \"QUESTION_TEXT\",\n \"label\": \"Prompt\",\n \"required\": true,\n \"config\": {\n \"max_length\": 1000\n }\n },\n {\n \"id\": \"illustration\",\n \"kind\": \"IMAGE\",\n \"label\": \"Supporting Image\",\n \"required\": false,\n \"config\": {\n \"allowed_formats\": [\"png\", \"jpg\", \"webp\"]\n }\n },\n {\n \"id\": \"data_table\",\n \"kind\": \"TABLE\",\n \"label\": \"Reference Table\",\n \"required\": false,\n \"config\": {\n \"max_rows\": 20,\n \"max_columns\": 8\n }\n }\n ],\n \"response_schema\": [\n {\n \"id\": \"choices\",\n \"kind\": \"OPTION\",\n \"label\": \"Answer Options\",\n \"required\": true,\n \"config\": {\n \"min_options\": 2,\n \"max_options\": 6,\n \"allow_multiple\": false\n }\n },\n {\n \"id\": \"timer\",\n \"kind\": \"ANSWER_TIMER\",\n \"label\": \"Answer Time Limit\",\n \"required\": false,\n \"config\": {\n \"min_seconds\": 5,\n \"max_seconds\": 180\n }\n }\n ],\n \"status\": \"ACTIVE\"\n}"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/questions/type-definitions",
|
||||||
|
"host": [
|
||||||
|
"{{base_url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"v1",
|
||||||
|
"questions",
|
||||||
|
"type-definitions"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Get Definition By ID",
|
||||||
"request": {
|
"request": {
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"header": [],
|
"header": [],
|
||||||
"url": {
|
"url": {
|
||||||
"raw": "{{baseUrl}}{{apiPrefix}}/questions/type-definitions?include_system=true&status=ACTIVE",
|
"raw": "{{base_url}}/api/v1/questions/type-definitions/{{dynamic_definition_id}}",
|
||||||
"host": [
|
"host": [
|
||||||
"{{baseUrl}}"
|
"{{base_url}}"
|
||||||
],
|
],
|
||||||
"path": [
|
"path": [
|
||||||
"{{apiPrefix}}",
|
"api",
|
||||||
|
"v1",
|
||||||
|
"questions",
|
||||||
|
"type-definitions",
|
||||||
|
"{{dynamic_definition_id}}"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "List Definitions (include system)",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/questions/type-definitions?include_system=true&status=ACTIVE",
|
||||||
|
"host": [
|
||||||
|
"{{base_url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"v1",
|
||||||
"questions",
|
"questions",
|
||||||
"type-definitions"
|
"type-definitions"
|
||||||
],
|
],
|
||||||
|
|
@ -156,263 +248,416 @@
|
||||||
"value": "ACTIVE"
|
"value": "ACTIVE"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
|
||||||
"description": "Lists reusable dynamic definitions (system + custom)."
|
|
||||||
},
|
|
||||||
"response": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "05 - Get Question Type Definition By ID",
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"header": [],
|
|
||||||
"url": {
|
|
||||||
"raw": "{{baseUrl}}{{apiPrefix}}/questions/type-definitions/{{questionTypeDefinitionId}}",
|
|
||||||
"host": [
|
|
||||||
"{{baseUrl}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"{{apiPrefix}}",
|
|
||||||
"questions",
|
|
||||||
"type-definitions",
|
|
||||||
"{{questionTypeDefinitionId}}"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"description": "Fetches one dynamic type-definition by ID."
|
|
||||||
},
|
|
||||||
"response": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "06 - Update Question Type Definition",
|
|
||||||
"request": {
|
|
||||||
"method": "PUT",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
}
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"display_name\": \"MCQ Dynamic Vocabulary (Updated)\",\n \"description\": \"Updated dynamic MCQ template.\",\n \"stimulus_component_kinds\": [\"INSTRUCTION\", \"TEXT_PASSAGE\", \"IMAGE\"],\n \"response_component_kinds\": [\"MULTIPLE_CHOICE\", \"ANSWER_TIMER\"],\n \"status\": \"ACTIVE\"\n}"
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"raw": "{{baseUrl}}{{apiPrefix}}/questions/type-definitions/{{questionTypeDefinitionId}}",
|
|
||||||
"host": [
|
|
||||||
"{{baseUrl}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"{{apiPrefix}}",
|
|
||||||
"questions",
|
|
||||||
"type-definitions",
|
|
||||||
"{{questionTypeDefinitionId}}"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"description": "Updates dynamic definition (except key/system flag)."
|
|
||||||
},
|
},
|
||||||
"response": []
|
"response": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "07 - Create Question Using question_type_definition_id",
|
"name": "Update Definition (schema evolution)",
|
||||||
"request": {
|
|
||||||
"method": "POST",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"question_text\": \"Choose the correct synonym for 'rapid'.\",\n \"question_type_definition_id\": {{questionTypeDefinitionId}},\n \"difficulty_level\": \"EASY\",\n \"points\": 1,\n \"status\": \"PUBLISHED\",\n \"options\": [\n { \"option_text\": \"Slow\", \"option_order\": 1, \"is_correct\": false },\n { \"option_text\": \"Quick\", \"option_order\": 2, \"is_correct\": true },\n { \"option_text\": \"Heavy\", \"option_order\": 3, \"is_correct\": false },\n { \"option_text\": \"Late\", \"option_order\": 4, \"is_correct\": false }\n ]\n}"
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"raw": "{{baseUrl}}{{apiPrefix}}/questions",
|
|
||||||
"host": [
|
|
||||||
"{{baseUrl}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"{{apiPrefix}}",
|
|
||||||
"questions"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"description": "Creates a question by binding to a dynamic definition. Backend infers runtime question_type from the definition."
|
|
||||||
},
|
|
||||||
"event": [
|
"event": [
|
||||||
{
|
{
|
||||||
"listen": "test",
|
"listen": "test",
|
||||||
"script": {
|
"script": {
|
||||||
"exec": [
|
"exec": [
|
||||||
"pm.test('Status is 201', function () { pm.response.to.have.status(201); });",
|
"pm.test(\"Status code is 200\", function () {",
|
||||||
"var json = pm.response.json();",
|
" pm.response.to.have.status(200);",
|
||||||
"if (json && json.data && json.data.id) {",
|
"});"
|
||||||
" pm.collectionVariables.set('questionId', json.data.id);",
|
|
||||||
"}"
|
|
||||||
],
|
],
|
||||||
"type": "text/javascript"
|
"type": "text/javascript"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"response": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "08 - Create Question (Explicit question_type + Definition)",
|
|
||||||
"request": {
|
|
||||||
"method": "POST",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"question_text\": \"Pick the antonym of 'expand'.\",\n \"question_type\": \"MCQ\",\n \"question_type_definition_id\": {{questionTypeDefinitionId}},\n \"difficulty_level\": \"MEDIUM\",\n \"points\": 2,\n \"options\": [\n { \"option_text\": \"Increase\", \"option_order\": 1, \"is_correct\": false },\n { \"option_text\": \"Contract\", \"option_order\": 2, \"is_correct\": true },\n { \"option_text\": \"Stretch\", \"option_order\": 3, \"is_correct\": false },\n { \"option_text\": \"Grow\", \"option_order\": 4, \"is_correct\": false }\n ]\n}"
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"raw": "{{baseUrl}}{{apiPrefix}}/questions",
|
|
||||||
"host": [
|
|
||||||
"{{baseUrl}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"{{apiPrefix}}",
|
|
||||||
"questions"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"description": "Valid combination: explicit question_type must match the type inferred from the selected definition."
|
|
||||||
},
|
|
||||||
"response": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "09 - Update Question (Switch/Attach Definition)",
|
|
||||||
"request": {
|
"request": {
|
||||||
"method": "PUT",
|
"method": "PUT",
|
||||||
"header": [
|
"header": [
|
||||||
{
|
{
|
||||||
"key": "Content-Type",
|
"key": "Content-Type",
|
||||||
"value": "application/json"
|
"value": "application/json",
|
||||||
|
"type": "text"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"body": {
|
"body": {
|
||||||
"mode": "raw",
|
"mode": "raw",
|
||||||
"raw": "{\n \"question_type_definition_id\": {{questionTypeDefinitionId}},\n \"question_type\": \"MCQ\",\n \"question_text\": \"Choose the best definition of 'meticulous'.\",\n \"options\": [\n { \"option_text\": \"Careless\", \"option_order\": 1, \"is_correct\": false },\n { \"option_text\": \"Very careful and precise\", \"option_order\": 2, \"is_correct\": true },\n { \"option_text\": \"Quickly done\", \"option_order\": 3, \"is_correct\": false }\n ],\n \"status\": \"PUBLISHED\"\n}"
|
"raw": "{\n \"display_name\": \"Dynamic Grammar + Visual MCQ (Updated)\",\n \"response_component_kinds\": [\"OPTION\", \"ANSWER_TIMER\", \"SEQUENCE_ORDER\"],\n \"response_schema\": [\n {\n \"id\": \"choices\",\n \"kind\": \"OPTION\",\n \"label\": \"Answer Options\",\n \"required\": true,\n \"config\": {\n \"min_options\": 2,\n \"max_options\": 6,\n \"allow_multiple\": false\n }\n },\n {\n \"id\": \"timer\",\n \"kind\": \"ANSWER_TIMER\",\n \"label\": \"Answer Time Limit\",\n \"required\": false,\n \"config\": {\n \"min_seconds\": 5,\n \"max_seconds\": 180\n }\n },\n {\n \"id\": \"ordering\",\n \"kind\": \"SEQUENCE_ORDER\",\n \"label\": \"Optional Sequence Order\",\n \"required\": false,\n \"config\": {\n \"max_items\": 8\n }\n }\n ]\n}"
|
||||||
},
|
},
|
||||||
"url": {
|
"url": {
|
||||||
"raw": "{{baseUrl}}{{apiPrefix}}/questions/{{questionId}}",
|
"raw": "{{base_url}}/api/v1/questions/type-definitions/{{dynamic_definition_id}}",
|
||||||
"host": [
|
"host": [
|
||||||
"{{baseUrl}}"
|
"{{base_url}}"
|
||||||
],
|
],
|
||||||
"path": [
|
"path": [
|
||||||
"{{apiPrefix}}",
|
"api",
|
||||||
|
"v1",
|
||||||
"questions",
|
"questions",
|
||||||
"{{questionId}}"
|
"type-definitions",
|
||||||
|
"{{dynamic_definition_id}}"
|
||||||
]
|
]
|
||||||
},
|
}
|
||||||
"description": "Updates a question and links (or re-links) it to a dynamic definition."
|
|
||||||
},
|
},
|
||||||
"response": []
|
"response": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "10 - Get Question By ID",
|
"name": "Update Definition (Invalid Schema Kind) - Expect 400",
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"exec": [
|
||||||
|
"pm.test(\"Status code is 400\", function () {",
|
||||||
|
" pm.response.to.have.status(400);",
|
||||||
|
"});"
|
||||||
|
],
|
||||||
|
"type": "text/javascript"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
"request": {
|
"request": {
|
||||||
"method": "GET",
|
"method": "PUT",
|
||||||
"header": [],
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"response_schema\": [\n {\n \"id\": \"bad\",\n \"kind\": \"NON_EXISTENT_KIND\",\n \"required\": true\n }\n ]\n}"
|
||||||
|
},
|
||||||
"url": {
|
"url": {
|
||||||
"raw": "{{baseUrl}}{{apiPrefix}}/questions/{{questionId}}",
|
"raw": "{{base_url}}/api/v1/questions/type-definitions/{{dynamic_definition_id}}",
|
||||||
"host": [
|
"host": [
|
||||||
"{{baseUrl}}"
|
"{{base_url}}"
|
||||||
],
|
],
|
||||||
"path": [
|
"path": [
|
||||||
"{{apiPrefix}}",
|
"api",
|
||||||
|
"v1",
|
||||||
"questions",
|
"questions",
|
||||||
"{{questionId}}"
|
"type-definitions",
|
||||||
|
"{{dynamic_definition_id}}"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"description": "Returns question details (options/short_answers/audio fields as applicable)."
|
{
|
||||||
|
"name": "03 - Dynamic Question Runtime",
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "Create Dynamic Question (valid payload)",
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"exec": [
|
||||||
|
"pm.test(\"Status code is 201\", function () {",
|
||||||
|
" pm.response.to.have.status(201);",
|
||||||
|
"});",
|
||||||
|
"const body = pm.response.json();",
|
||||||
|
"pm.collectionVariables.set(\"dynamic_question_id\", body.data.id);",
|
||||||
|
"pm.test(\"Question type is DYNAMIC\", function () {",
|
||||||
|
" pm.expect(body.data.question_type).to.eql(\"DYNAMIC\");",
|
||||||
|
"});"
|
||||||
|
],
|
||||||
|
"type": "text/javascript"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"question_text\": \"Complete the sentence using the table and image.\",\n \"question_type\": \"DYNAMIC\",\n \"question_type_definition_id\": {{dynamic_definition_id}},\n \"difficulty_level\": \"MEDIUM\",\n \"points\": 2,\n \"status\": \"DRAFT\",\n \"dynamic_payload\": {\n \"stimulus\": [\n {\n \"id\": \"prompt\",\n \"kind\": \"QUESTION_TEXT\",\n \"value\": \"Select the best completion based on the visual and table.\"\n },\n {\n \"id\": \"illustration\",\n \"kind\": \"IMAGE\",\n \"value\": \"https://cdn.example.com/images/sample-grammar-scene.jpg\"\n },\n {\n \"id\": \"data_table\",\n \"kind\": \"TABLE\",\n \"value\": {\n \"columns\": [\"Verb\", \"Past Form\"],\n \"rows\": [\n [\"go\", \"went\"],\n [\"write\", \"wrote\"]\n ]\n }\n }\n ],\n \"response\": [\n {\n \"id\": \"choices\",\n \"kind\": \"OPTION\",\n \"value\": {\n \"options\": [\n {\"id\": \"a\", \"text\": \"I goed to school\", \"is_correct\": false},\n {\"id\": \"b\", \"text\": \"I went to school\", \"is_correct\": true}\n ]\n }\n },\n {\n \"id\": \"timer\",\n \"kind\": \"ANSWER_TIMER\",\n \"value\": {\n \"seconds\": 30\n }\n }\n ]\n }\n}"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/questions",
|
||||||
|
"host": [
|
||||||
|
"{{base_url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"v1",
|
||||||
|
"questions"
|
||||||
|
]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"response": []
|
"response": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "11 - List Questions",
|
"name": "Get Dynamic Question By ID",
|
||||||
"request": {
|
"request": {
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"header": [],
|
"header": [],
|
||||||
"url": {
|
"url": {
|
||||||
"raw": "{{baseUrl}}{{apiPrefix}}/questions?question_type=MCQ&limit=10&offset=0",
|
"raw": "{{base_url}}/api/v1/questions/{{dynamic_question_id}}",
|
||||||
"host": [
|
"host": [
|
||||||
"{{baseUrl}}"
|
"{{base_url}}"
|
||||||
],
|
],
|
||||||
"path": [
|
"path": [
|
||||||
"{{apiPrefix}}",
|
"api",
|
||||||
|
"v1",
|
||||||
|
"questions",
|
||||||
|
"{{dynamic_question_id}}"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Update Dynamic Question (valid payload)",
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"exec": [
|
||||||
|
"pm.test(\"Status code is 200\", function () {",
|
||||||
|
" pm.response.to.have.status(200);",
|
||||||
|
"});"
|
||||||
|
],
|
||||||
|
"type": "text/javascript"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"request": {
|
||||||
|
"method": "PUT",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"question_text\": \"Updated: choose the best sentence.\",\n \"question_type_definition_id\": {{dynamic_definition_id}},\n \"question_type\": \"DYNAMIC\",\n \"status\": \"PUBLISHED\",\n \"dynamic_payload\": {\n \"stimulus\": [\n {\n \"id\": \"prompt\",\n \"kind\": \"QUESTION_TEXT\",\n \"value\": \"Pick the grammatically correct option.\"\n }\n ],\n \"response\": [\n {\n \"id\": \"choices\",\n \"kind\": \"OPTION\",\n \"value\": {\n \"options\": [\n {\"id\": \"a\", \"text\": \"He go home yesterday\", \"is_correct\": false},\n {\"id\": \"b\", \"text\": \"He went home yesterday\", \"is_correct\": true}\n ]\n }\n }\n ]\n }\n}"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/questions/{{dynamic_question_id}}",
|
||||||
|
"host": [
|
||||||
|
"{{base_url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"v1",
|
||||||
|
"questions",
|
||||||
|
"{{dynamic_question_id}}"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "List Questions (question_type=DYNAMIC)",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/questions?question_type=DYNAMIC&limit=20&offset=0",
|
||||||
|
"host": [
|
||||||
|
"{{base_url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"v1",
|
||||||
"questions"
|
"questions"
|
||||||
],
|
],
|
||||||
"query": [
|
"query": [
|
||||||
{
|
{
|
||||||
"key": "question_type",
|
"key": "question_type",
|
||||||
"value": "MCQ"
|
"value": "DYNAMIC"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "limit",
|
"key": "limit",
|
||||||
"value": "10"
|
"value": "20"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "offset",
|
"key": "offset",
|
||||||
"value": "0"
|
"value": "0"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
}
|
||||||
"description": "Lists questions filtered by runtime question_type."
|
|
||||||
},
|
},
|
||||||
"response": []
|
"response": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "12 - Negative Test: Mismatched Type and Definition",
|
"name": "Search Questions",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/questions/search?q=choose&limit=20&offset=0",
|
||||||
|
"host": [
|
||||||
|
"{{base_url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"v1",
|
||||||
|
"questions",
|
||||||
|
"search"
|
||||||
|
],
|
||||||
|
"query": [
|
||||||
|
{
|
||||||
|
"key": "q",
|
||||||
|
"value": "choose"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "limit",
|
||||||
|
"value": "20"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "offset",
|
||||||
|
"value": "0"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "04 - Negative Runtime Validation Cases",
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "Create Dynamic Question without dynamic_payload - Expect 400",
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"exec": [
|
||||||
|
"pm.test(\"Status code is 400\", function () {",
|
||||||
|
" pm.response.to.have.status(400);",
|
||||||
|
"});"
|
||||||
|
],
|
||||||
|
"type": "text/javascript"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
"request": {
|
"request": {
|
||||||
"method": "POST",
|
"method": "POST",
|
||||||
"header": [
|
"header": [
|
||||||
{
|
{
|
||||||
"key": "Content-Type",
|
"key": "Content-Type",
|
||||||
"value": "application/json"
|
"value": "application/json",
|
||||||
|
"type": "text"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"body": {
|
"body": {
|
||||||
"mode": "raw",
|
"mode": "raw",
|
||||||
"raw": "{\n \"question_text\": \"This should fail.\",\n \"question_type\": \"AUDIO\",\n \"question_type_definition_id\": {{questionTypeDefinitionId}},\n \"options\": [\n { \"option_text\": \"A\", \"option_order\": 1, \"is_correct\": true }\n ]\n}"
|
"raw": "{\n \"question_text\": \"Should fail\",\n \"question_type\": \"DYNAMIC\",\n \"question_type_definition_id\": {{dynamic_definition_id}},\n \"difficulty_level\": \"EASY\"\n}"
|
||||||
},
|
},
|
||||||
"url": {
|
"url": {
|
||||||
"raw": "{{baseUrl}}{{apiPrefix}}/questions",
|
"raw": "{{base_url}}/api/v1/questions",
|
||||||
"host": [
|
"host": [
|
||||||
"{{baseUrl}}"
|
"{{base_url}}"
|
||||||
],
|
],
|
||||||
"path": [
|
"path": [
|
||||||
"{{apiPrefix}}",
|
"api",
|
||||||
|
"v1",
|
||||||
"questions"
|
"questions"
|
||||||
]
|
]
|
||||||
},
|
}
|
||||||
"description": "Expected 400 because explicit question_type does not match inferred type from definition."
|
|
||||||
},
|
},
|
||||||
"response": []
|
"response": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "13 - Delete Custom Question Type Definition",
|
"name": "Create Dynamic Question with disallowed element kind - Expect 400",
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"exec": [
|
||||||
|
"pm.test(\"Status code is 400\", function () {",
|
||||||
|
" pm.response.to.have.status(400);",
|
||||||
|
"});"
|
||||||
|
],
|
||||||
|
"type": "text/javascript"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"question_text\": \"Should fail because AUDIO_CLIP is not allowed by selected definition\",\n \"question_type\": \"DYNAMIC\",\n \"question_type_definition_id\": {{dynamic_definition_id}},\n \"dynamic_payload\": {\n \"stimulus\": [\n {\n \"id\": \"audio1\",\n \"kind\": \"AUDIO_CLIP\",\n \"value\": \"https://cdn.example.com/audio/not-allowed.mp3\"\n }\n ],\n \"response\": [\n {\n \"id\": \"choices\",\n \"kind\": \"OPTION\",\n \"value\": {\n \"options\": [\n {\"id\": \"a\", \"text\": \"A\", \"is_correct\": true},\n {\"id\": \"b\", \"text\": \"B\", \"is_correct\": false}\n ]\n }\n }\n ]\n }\n}"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/questions",
|
||||||
|
"host": [
|
||||||
|
"{{base_url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"v1",
|
||||||
|
"questions"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "05 - Cleanup",
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "Delete Dynamic Question",
|
||||||
"request": {
|
"request": {
|
||||||
"method": "DELETE",
|
"method": "DELETE",
|
||||||
"header": [],
|
"header": [],
|
||||||
"url": {
|
"url": {
|
||||||
"raw": "{{baseUrl}}{{apiPrefix}}/questions/type-definitions/{{questionTypeDefinitionId}}",
|
"raw": "{{base_url}}/api/v1/questions/{{dynamic_question_id}}",
|
||||||
"host": [
|
"host": [
|
||||||
"{{baseUrl}}"
|
"{{base_url}}"
|
||||||
],
|
],
|
||||||
"path": [
|
"path": [
|
||||||
"{{apiPrefix}}",
|
"api",
|
||||||
|
"v1",
|
||||||
|
"questions",
|
||||||
|
"{{dynamic_question_id}}"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Delete Dynamic Definition",
|
||||||
|
"request": {
|
||||||
|
"method": "DELETE",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/questions/type-definitions/{{dynamic_definition_id}}",
|
||||||
|
"host": [
|
||||||
|
"{{base_url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"v1",
|
||||||
"questions",
|
"questions",
|
||||||
"type-definitions",
|
"type-definitions",
|
||||||
"{{questionTypeDefinitionId}}"
|
"{{dynamic_definition_id}}"
|
||||||
]
|
]
|
||||||
},
|
}
|
||||||
"description": "Deletes a custom definition. System definitions cannot be deleted."
|
|
||||||
},
|
},
|
||||||
"response": []
|
"response": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
{
|
|
||||||
"id": "2e932640-c314-4f18-b273-3a55fcb9384e",
|
|
||||||
"name": "Yimaru Local Dynamic Builder",
|
|
||||||
"values": [
|
|
||||||
{
|
|
||||||
"key": "baseUrl",
|
|
||||||
"value": "http://localhost:8080",
|
|
||||||
"type": "default",
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "apiPrefix",
|
|
||||||
"value": "/api/v1",
|
|
||||||
"type": "default",
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "token",
|
|
||||||
"value": "",
|
|
||||||
"type": "secret",
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "questionTypeDefinitionId",
|
|
||||||
"value": "",
|
|
||||||
"type": "default",
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "questionId",
|
|
||||||
"value": "",
|
|
||||||
"type": "default",
|
|
||||||
"enabled": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"_postman_variable_scope": "environment",
|
|
||||||
"_postman_exported_at": "2026-05-07T08:41:00.000Z",
|
|
||||||
"_postman_exported_using": "Cursor Agent"
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue
Block a user