Yimaru-BackEnd/docs/DYNAMIC_PRACTICE_CREATION_LMS_GUIDE.md
Yared Yemane ab986a08f0 feat: expand builder docs, add practice LMS guide, remove CHART stimulus kinds
Document schema slot label assignment and usage in the admin integration guide. Add a step-by-step dynamic practice creation guide for course/module/lesson scopes. Remove CHART and FLOW_CHART from the stimulus component catalog.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-05 02:54:44 -07:00

996 lines
27 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Dynamic Practice Creation — LMS Guide (Course / Module / Lesson)
This guide explains **step by step** how to create **practices** in the Learn English LMS hierarchy using **dynamic question types** (`DYNAMIC` questions with `question_type_definition_id` + `dynamic_payload`).
It is the companion to:
- **Type builder (definitions + components):** `docs/DYNAMIC_QUESTION_TYPE_BUILDER_ADMIN_INTEGRATION.md`
- **Lesson-only quick path (legacy + dynamic):** `docs/PRACTICE_CREATION_API_GUIDE.md`
**Base URL:** `{API_HOST}/api/v1`
**Auth:** `Authorization: Bearer <access_token>`
**Content-Type:** `application/json` (except file upload)
---
## Table of contents
1. [Architecture](#1-architecture)
2. [Prerequisites and permissions](#2-prerequisites-and-permissions)
3. [Standard response envelopes](#3-standard-response-envelopes)
4. [ID map (what to store after each step)](#4-id-map-what-to-store-after-each-step)
5. [Publishing model](#5-publishing-model)
6. [End-to-end flow overview](#6-end-to-end-flow-overview)
7. [Step 0 — Resolve LMS parent IDs](#7-step-0--resolve-lms-parent-ids)
8. [Step 1 — (Optional) Upload media](#8-step-1--optional-upload-media)
9. [Step 2 — Create or select a question type definition](#9-step-2--create-or-select-a-question-type-definition)
10. [Step 3 — Create dynamic question(s)](#10-step-3--create-dynamic-questions)
11. [Step 4 — Create PRACTICE question set](#11-step-4--create-practice-question-set)
12. [Step 5 — Add questions to the set](#12-step-5--add-questions-to-the-set)
13. [Step 6 — Create practice shell (course / module / lesson)](#13-step-6--create-practice-shell-course--module--lesson)
14. [Step 7 — Verify and inspect](#14-step-7--verify-and-inspect)
15. [Optional — Reorder, update, publish](#15-optional--reorder-update-publish)
16. [Worked example — Lesson practice with TABLE + OPTION](#16-worked-example--lesson-practice-with-table--option)
17. [Scope-specific quick reference](#17-scope-specific-quick-reference)
18. [API index](#18-api-index)
19. [QA checklist](#19-qa-checklist)
---
## 1. Architecture
### LMS hierarchy
```
Program
└── Course
└── Module
└── Lesson
```
A **practice** is a learner-facing activity (story, persona, tips) backed by a **question set** containing one or more **questions**.
### Database rule (one parent only)
Each row in `lms_practices` attaches to **exactly one** of:
| Scope | `parent_kind` | `parent_id` refers to |
|-------|---------------|------------------------|
| Course-level practice | `COURSE` | `courses.id` |
| Module-level practice | `MODULE` | `modules.id` |
| Lesson-level practice | `LESSON` | `lessons.id` |
You cannot attach one practice to multiple parents. Choose the scope that matches your curriculum design.
### How dynamic questions fit in
```mermaid
flowchart LR
subgraph definitions
DEF[Question type definition]
end
subgraph content
Q1[Dynamic question 1]
Q2[Dynamic question 2]
end
subgraph packaging
QS[Question set set_type=PRACTICE]
P[Practice shell]
end
DEF --> Q1
DEF --> Q2
Q1 --> QS
Q2 --> QS
QS --> P
P --> L[Lesson / Module / Course]
```
1. **Definition** — template (which stimulus/response slots exist).
2. **Questions** — instances with `dynamic_payload` (real TABLE rows, OPTION choices, PDF URLs, etc.).
3. **Question set** — ordered list of question IDs (`set_type: "PRACTICE"`).
4. **Practice** — links `question_set_id` to a course, module, or lesson.
---
## 2. Prerequisites and permissions
### Minimum permissions (admin authoring)
| Permission | Used for |
|------------|----------|
| `questions.list` | Component catalog, list definitions |
| `questions.create` | Definitions, dynamic questions |
| `questions.get` | Load question / definition details |
| `questions.update` | Update questions, definitions, publish practice |
| `question_sets.create` | Create PRACTICE set |
| `question_set_items.add` | Link questions to set |
| `question_set_items.list` | List questions in set, type summary |
| `question_set_items.update_order` | Reorder questions |
| `practices.create` | Create practice shell |
| `practices.list` | List practices under course/module/lesson |
| `practices.get` | Get practice by id |
| `practices.update` | Publish practice (`publish_status`) |
| `lessons.get` / `modules.get` / `courses.get` | Resolve parent IDs (as needed) |
File upload: authenticated user only (`POST /files/upload`).
### Related docs
- Full definition API reference: `DYNAMIC_QUESTION_TYPE_BUILDER_ADMIN_INTEGRATION.md`
- Postman collection: `postman/Dynamic-Question-Type-Builder.postman_collection.json`
---
## 3. Standard response envelopes
### Success — `domain.Response`
```json
{
"message": "Human-readable summary",
"data": {},
"success": true,
"status_code": 200,
"metadata": null
}
```
`status_code` in the body may be `200` or `201` depending on the endpoint.
### Error — `domain.ErrorResponse`
```json
{
"message": "Short error title",
"error": "Detailed validation or system message"
}
```
---
## 4. ID map (what to store after each step)
| Step | Capture | Used in |
|------|---------|---------|
| Upload media | `url`, `object_key` | `dynamic_payload` stimulus `value` |
| Create definition | `question_type_definition_id` | Create each dynamic question |
| Create question | `question_id` | Add to set (repeat per question) |
| Create question set | `question_set_id` (`set_id`) | Create practice |
| Create practice | `practice_id` | Admin UI, learner routes |
| Parent resolution | `course_id` / `module_id` / `lesson_id` | `parent_id` + `owner_id` |
---
## 5. Publishing model
Three layers can affect learner visibility:
| Layer | Field | Values | Notes |
|-------|--------|--------|-------|
| Question | `status` | `DRAFT`, `PUBLISHED`, `INACTIVE`, `ARCHIVED` | Use `PUBLISHED` for live content |
| Question set | `status` | `DRAFT`, `PUBLISHED`, … | Use `PUBLISHED` for live sets |
| Practice shell | `publish_status` | `DRAFT`, `PUBLISHED` | Omit or `PUBLISHED` on create; use `DRAFT` to hide until ready |
**Recommendation for go-live:** set question `status`, question set `status`, and practice `publish_status` to published when learners should see the practice immediately.
**Draft practice:** create with `"publish_status": "DRAFT"`, then `PUT /practices/:id` with `"publish_status": "PUBLISHED"` when ready.
---
## 6. End-to-end flow overview
| Step | Action | APIs (typical) |
|------|--------|----------------|
| 0 | Resolve `parent_id` (course / module / lesson) | `GET /courses/:id`, `GET /modules/:id`, `GET /lessons/:id` |
| 1 | Upload images / PDF / audio (if needed) | `POST /files/upload` |
| 2 | Create or pick question type definition | `GET /questions/type-definitions`, `POST /questions/type-definitions` |
| 3 | Create one or more dynamic questions | `POST /questions` (repeat) |
| 4 | Create PRACTICE question set | `POST /question-sets` |
| 5 | Add each question to set (ordered) | `POST /question-sets/:setId/questions` (repeat) |
| 6 | Create practice at chosen scope | `POST /practices` |
| 7 | Verify | `GET /lessons/:id/practices` (or course/module), `GET /question-sets/:setId/questions` |
---
## 7. Step 0 — Resolve LMS parent IDs
Before creating a practice, know the target **`parent_id`** and matching **`owner_type`** for the question set.
### List lessons in a module (example)
| | |
|--|--|
| **GET** | `/modules/:moduleId/lessons` |
| **Permission** | `lessons.list_by_module` |
**Query (optional):** `limit`, `offset` (see lesson list handler defaults).
**Success `200` — `data`:** array of lessons; capture `id` for `parent_id` when scope is `LESSON`.
### Get lesson / module / course
| Entity | Method / path | Permission |
|--------|---------------|------------|
| Lesson | `GET /lessons/:id` | `lessons.get` |
| Module | `GET /modules/:id` | `modules.get` |
| Course | `GET /courses/:id` | `courses.get` |
Use these to confirm the parent exists and to display titles in the admin UI.
---
## 8. Step 1 — (Optional) Upload media
Required when the definition uses `IMAGE`, `AUDIO_CLIP`, `AUDIO_PROMPT`, or `PDF_ATTACHMENT` stimulus slots.
### POST `/files/upload`
**Content-Type:** `multipart/form-data`
| Field | Value |
|-------|--------|
| `file` | Binary |
| `media_type` | `image`, `audio`, `video`, or `pdf` |
**Success `200` — `data`:**
```json
{
"object_key": "pdf/a1b2c3d4-e5f6-7890-abcd-ef1234567890.pdf",
"url": "https://minio.example.com/bucket/pdf/a1b2c3d4-e5f6-7890-abcd-ef1234567890.pdf?X-Amz-Algorithm=...",
"content_type": "application/pdf",
"media_type": "pdf",
"provider": "MINIO"
}
```
**Use in `dynamic_payload`:** set stimulus `value` to `data.url` (or store `minio://{object_key}` and resolve with `GET /files/url?key=...`).
**Errors `400`:**
```json
{
"message": "Invalid media_type",
"error": "media_type must be one of: image, audio, video, pdf"
}
```
---
## 9. Step 2 — Create or select a question type definition
Skip creation if reusing an existing ACTIVE definition.
### 9.1 List existing definitions
| | |
|--|--|
| **GET** | `/questions/type-definitions?include_system=true&status=ACTIVE` |
| **Permission** | `questions.list` |
**Success `200` — `data`:** array of definitions (see shape in type builder doc).
### 9.2 (Optional) Validate kinds
| | |
|--|--|
| **POST** | `/questions/validate-question-type-definition` |
| **Permission** | `questions.create` |
**Request:**
```json
{
"stimulus_component_kinds": ["QUESTION_TEXT", "TABLE", "IMAGE"],
"response_component_kinds": ["OPTION", "ANSWER_TIMER"]
}
```
**Success `200` — `data`:**
```json
{
"valid": true
}
```
### 9.3 Create definition (example with TABLE)
| | |
|--|--|
| **POST** | `/questions/type-definitions` |
| **Permission** | `questions.create` |
**Request:**
```json
{
"key": "lesson_table_mcq_v1",
"display_name": "Lesson Table MCQ",
"description": "Prompt + optional table + image; MCQ response",
"stimulus_component_kinds": ["QUESTION_TEXT", "TABLE", "IMAGE"],
"response_component_kinds": ["OPTION"],
"stimulus_schema": [
{
"id": "prompt",
"kind": "QUESTION_TEXT",
"label": "Question prompt",
"required": true,
"config": { "max_length": 2000 }
},
{
"id": "data_table",
"kind": "TABLE",
"label": "Reference table",
"required": true,
"config": { "max_rows": 30, "max_columns": 10 }
},
{
"id": "illustration",
"kind": "IMAGE",
"label": "Supporting image",
"required": false,
"config": {}
}
],
"response_schema": [
{
"id": "choices",
"kind": "OPTION",
"label": "Answer choices",
"required": true,
"config": { "min_options": 2, "max_options": 6 }
}
],
"status": "ACTIVE"
}
```
**Success `201` — `data` (full `QuestionTypeDefinition`):**
```json
{
"id": 42,
"key": "lesson_table_mcq_v1",
"display_name": "Lesson Table MCQ",
"description": "Prompt + optional table + image; MCQ response",
"stimulus_component_kinds": ["QUESTION_TEXT", "TABLE", "IMAGE"],
"response_component_kinds": ["OPTION"],
"stimulus_schema": [ ],
"response_schema": [ ],
"is_system": false,
"status": "ACTIVE",
"created_at": "2026-06-04T10:00:00Z",
"updated_at": null
}
```
**Capture:** `data.id``question_type_definition_id` (e.g. `42`).
---
## 10. Step 3 — Create dynamic question(s)
Repeat this step for each question in the practice.
### POST `/questions`
| | |
|--|--|
| **Permission** | `questions.create` |
**Rules:**
- `question_type` must be `"DYNAMIC"`.
- `question_type_definition_id` is **required**.
- `dynamic_payload` is **required**.
- Do **not** send top-level `question_text` (prompt lives in stimulus).
- Do **not** send legacy `options` / `short_answers` for pure dynamic MCQ (use `OPTION` in payload).
**Request (TABLE + OPTION example):**
```json
{
"question_type": "DYNAMIC",
"question_type_definition_id": 42,
"difficulty_level": "MEDIUM",
"points": 2,
"status": "PUBLISHED",
"dynamic_payload": {
"stimulus": [
{
"id": "prompt",
"kind": "QUESTION_TEXT",
"value": "Using the table, choose the correct past tense."
},
{
"id": "data_table",
"kind": "TABLE",
"value": {
"columns": ["Verb", "Past Form"],
"rows": [
["go", "went"],
["write", "wrote"],
["see", "saw"]
]
}
},
{
"id": "illustration",
"kind": "IMAGE",
"value": "https://minio.example.com/bucket/image/uuid.jpg"
}
],
"response": [
{
"id": "choices",
"kind": "OPTION",
"value": {
"options": [
{ "id": "a", "text": "He goed home.", "is_correct": false },
{ "id": "b", "text": "He went home.", "is_correct": true },
{ "id": "c", "text": "He go home.", "is_correct": false }
]
}
}
]
}
}
```
**TABLE `value` contract:**
| Field | Type | Description |
|-------|------|-------------|
| `columns` | `string[]` | Header labels |
| `rows` | `string[][]` | Each row length should match `columns.length` |
**Success `201` — `data`:**
```json
{
"id": 1001,
"question_type": "DYNAMIC",
"question_type_definition_id": 42,
"dynamic_payload": {
"stimulus": [ ],
"response": [ ]
},
"difficulty_level": "MEDIUM",
"points": 2,
"status": "PUBLISHED",
"created_at": "2026-06-04T11:00:00Z"
}
```
Note: `question_text` is **omitted** from the JSON response for `DYNAMIC` questions.
**Error `400` examples:**
```json
{
"message": "Invalid dynamic_payload",
"error": "dynamic_payload.stimulus: required element id \"data_table\" is missing"
}
```
```json
{
"message": "Invalid question_text",
"error": "question_text is not used for DYNAMIC questions; set prompt text in dynamic_payload stimulus (QUESTION_TEXT, INSTRUCTION, or TEXT_PASSAGE)"
}
```
**Capture:** `data.id``question_id` (repeat list: `[1001, 1002, ...]`).
---
## 11. Step 4 — Create PRACTICE question set
The question set groups questions. Its `owner_type` / `owner_id` should match the practice scope (recommended for reporting and sequence gating).
### POST `/question-sets`
| | |
|--|--|
| **Permission** | `question_sets.create` |
**Request — lesson scope:**
```json
{
"title": "Lesson 12 — Dynamic drill",
"description": "Practice question set for lesson 12",
"set_type": "PRACTICE",
"owner_type": "LESSON",
"owner_id": 12,
"status": "PUBLISHED",
"shuffle_questions": false
}
```
**Request — module scope:**
```json
{
"title": "Module 3 — Review set",
"set_type": "PRACTICE",
"owner_type": "MODULE",
"owner_id": 3,
"status": "PUBLISHED",
"shuffle_questions": false
}
```
**Request — course scope:**
```json
{
"title": "Course 1 — Capstone practice",
"set_type": "PRACTICE",
"owner_type": "COURSE",
"owner_id": 1,
"status": "PUBLISHED",
"shuffle_questions": false
}
```
| Field | Required | Notes |
|-------|----------|-------|
| `title` | Yes | Admin display |
| `set_type` | Yes | Must be `"PRACTICE"` for LMS practices |
| `owner_type` | Recommended | `LESSON`, `MODULE`, or `COURSE` (match practice parent) |
| `owner_id` | Recommended | ID of that entity |
| `description` | No | |
| `status` | No | Default `DRAFT`; use `PUBLISHED` for learners |
| `shuffle_questions` | No | Default `false` |
| `time_limit_minutes` | No | Optional |
| `passing_score` | No | Optional |
| `intro_video_url` | No | Optional |
**Success `201` — `data`:**
```json
{
"id": 55,
"title": "Lesson 12 — Dynamic drill",
"description": "Practice question set for lesson 12",
"set_type": "PRACTICE",
"owner_type": "LESSON",
"owner_id": 12,
"shuffle_questions": false,
"status": "PUBLISHED",
"created_at": "2026-06-04T11:30:00Z"
}
```
**Capture:** `data.id``question_set_id` / `set_id` (e.g. `55`).
---
## 12. Step 5 — Add questions to the set
Run once per `question_id`. `display_order` controls sequence (important for `STUDENT` practice gating).
### POST `/question-sets/:setId/questions`
| | |
|--|--|
| **Permission** | `question_set_items.add` |
**Path:** `setId` = question set id from Step 4.
**Request (first question):**
```json
{
"question_id": 1001,
"display_order": 1
}
```
**Request (second question):**
```json
{
"question_id": 1002,
"display_order": 2
}
```
| Field | Type | Required |
|-------|------|----------|
| `question_id` | `int64` | Yes |
| `display_order` | `int32` | No |
**Success `201` — `data`:**
```json
{
"id": 901,
"set_id": 55,
"question_id": 1001,
"display_order": 1
}
```
**Errors:** `400` invalid ids; `500` link failure.
### (Optional) Question type summary for set
| | |
|--|--|
| **GET** | `/question-sets/:setId/question-types` |
| **Permission** | `question_set_items.list` |
**Success `200` — `data`:**
```json
{
"question_set_id": 55,
"total_questions": 2,
"question_types": [
{
"question_type_definition_id": 42,
"key": "lesson_table_mcq_v1",
"display_name": "Lesson Table MCQ",
"count": 2
}
]
}
```
---
## 13. Step 6 — Create practice shell (course / module / lesson)
Links the question set to exactly one LMS parent.
### POST `/practices`
| | |
|--|--|
| **Permission** | `practices.create` |
**Request — lesson:**
```json
{
"parent_kind": "LESSON",
"parent_id": 12,
"title": "Lesson 12 — Table MCQ practice",
"story_description": "Read the table and choose the best answer.",
"story_image": "https://minio.example.com/bucket/image/story.webp",
"question_set_id": 55,
"quick_tips": "Check every row in the table before selecting.",
"publish_status": "DRAFT"
}
```
**Request — module:**
```json
{
"parent_kind": "MODULE",
"parent_id": 3,
"title": "Module 3 review",
"question_set_id": 55,
"publish_status": "PUBLISHED"
}
```
**Request — course:**
```json
{
"parent_kind": "COURSE",
"parent_id": 1,
"title": "Course-wide practice",
"question_set_id": 55,
"publish_status": "PUBLISHED"
}
```
| Field | Type | Required | Notes |
|-------|------|----------|-------|
| `parent_kind` | string | Yes | `COURSE`, `MODULE`, or `LESSON` |
| `parent_id` | int64 | Yes | Target entity id |
| `question_set_id` | int64 | Yes | From Step 4 |
| `title` | string | No | Empty string allowed |
| `story_description` | string | No | |
| `story_image` | string | No | URL |
| `persona_id` | int64 | No | `lms_personas` catalog id |
| `quick_tips` | string | No | |
| `publish_status` | string | No | `DRAFT` or `PUBLISHED`; default `PUBLISHED` if omitted |
**Success `201` — `data` (`domain.Practice`):**
```json
{
"id": 37,
"parent_kind": "LESSON",
"parent_id": 12,
"title": "Lesson 12 — Table MCQ practice",
"story_description": "Read the table and choose the best answer.",
"story_image": "https://minio.example.com/bucket/image/story.webp",
"persona_id": null,
"question_set_id": 55,
"publish_status": "DRAFT",
"quick_tips": "Check every row in the table before selecting.",
"created_at": "2026-06-04T12:00:00Z",
"updated_at": null
}
```
**Errors:**
| Status | `message` | Typical `error` |
|--------|-----------|-----------------|
| `404` | Lesson not found | Parent id invalid |
| `404` | Question set not found | Bad `question_set_id` |
| `404` | Persona not found | Bad `persona_id` |
| `400` | Invalid parent | Bad `parent_kind` |
**Capture:** `data.id``practice_id` (e.g. `37`).
---
## 14. Step 7 — Verify and inspect
### 14.1 List practices under parent
| Scope | GET |
|-------|-----|
| Lesson | `/lessons/:id/practices?limit=20&offset=0` |
| Module | `/modules/:id/practices?limit=20&offset=0` |
| Course | `/courses/:id/practices?limit=20&offset=0` |
**Permission:** `practices.list`
**Success `200` — `data`:**
```json
{
"practices": [
{
"id": 37,
"parent_kind": "LESSON",
"parent_id": 12,
"title": "Lesson 12 — Table MCQ practice",
"question_set_id": 55,
"publish_status": "DRAFT",
"created_at": "2026-06-04T12:00:00Z"
}
],
"total_count": 1,
"limit": 20,
"offset": 0
}
```
### 14.2 Get practice by id
| | |
|--|--|
| **GET** | `/practices/:id` |
| **Permission** | `practices.get` |
**Success `200` — `data`:** full `Practice` object (includes `question_set_id`).
### 14.3 List questions in set (admin — full dynamic payload)
Use **`question_set_id`** from the practice record (not `practice_id`).
| | |
|--|--|
| **GET** | `/question-sets/:setId/questions` |
| **Permission** | `question_set_items.list` |
**Success `200` — `data`:** array of full questions including `dynamic_payload` and `question_type_definition_id`.
For paginated learner-style listing with filters:
| | |
|--|--|
| **GET** | `/practices/:practiceId/questions?limit=10&offset=0&question_type=DYNAMIC` |
**Note:** This routes path parameter is named `practiceId` in OpenAPI but is implemented against **`question_sets.id`**. For admin, prefer **`GET /question-sets/:setId/questions`** using `practice.question_set_id` from Step 14.2.
**Paginated response shape (`GET /practices/.../questions`):**
```json
{
"questions": [
{
"id": 901,
"set_id": 55,
"question_id": 1001,
"display_order": 1,
"question_type": "DYNAMIC",
"dynamic_payload": { "stimulus": [ ], "response": [ ] },
"points": 2,
"question_status": "PUBLISHED"
}
],
"total_count": 1,
"limit": 10,
"offset": 0
}
```
---
## 15. Optional — Reorder, update, publish
### Reorder question in set
| | |
|--|--|
| **PUT** | `/question-sets/:setId/questions/:questionId/order` |
| **Permission** | `question_set_items.update_order` |
**Request:**
```json
{
"display_order": 2
}
```
**Success `200`:**
```json
{
"message": "Question order updated successfully",
"success": true,
"status_code": 200
}
```
### Publish practice shell
| | |
|--|--|
| **PUT** | `/practices/:id` |
| **Permission** | `practices.update` |
**Request:**
```json
{
"publish_status": "PUBLISHED"
}
```
**Success `200` — `data`:** updated `Practice` with `publish_status: "PUBLISHED"`.
### Update dynamic question content
| | |
|--|--|
| **PUT** | `/questions/:id` |
| **Permission** | `questions.update` |
Send updated `dynamic_payload` (and optional metadata). Do not send `question_text` for `DYNAMIC`.
### Remove question from set
| | |
|--|--|
| **DELETE** | `/question-sets/:setId/questions/:questionId` |
| **Permission** | `question_set_items.remove` |
---
## 16. Worked example — Lesson practice with TABLE + OPTION
**Goal:** Lesson `12` gets one practice with one dynamic TABLE+MCQ question.
| Step | API | Key ids |
|------|-----|---------|
| 1 | `POST /questions/type-definitions` | `definition_id = 42` |
| 2 | `POST /questions` | `question_id = 1001` |
| 3 | `POST /question-sets` (`owner_type: LESSON`, `owner_id: 12`) | `set_id = 55` |
| 4 | `POST /question-sets/55/questions` | links `1001` order `1` |
| 5 | `POST /practices` (`parent_kind: LESSON`, `parent_id: 12`, `question_set_id: 55`) | `practice_id = 37` |
| 6 | `GET /lessons/12/practices` | confirms practice listed |
| 7 | `GET /question-sets/55/questions` | confirms TABLE payload |
| 8 | `PUT /practices/37` `{ "publish_status": "PUBLISHED" }` | go live |
**Admin UI table editor → API:** bind columns/rows UI to stimulus slot `data_table` / kind `TABLE` before Step 2 (`POST /questions`).
---
## 17. Scope-specific quick reference
### Lesson practice
```json
// Question set
{ "owner_type": "LESSON", "owner_id": <lesson_id>, "set_type": "PRACTICE" }
// Practice
{ "parent_kind": "LESSON", "parent_id": <lesson_id>, "question_set_id": <set_id> }
```
**Verify:** `GET /lessons/<lesson_id>/practices`
### Module practice
```json
{ "owner_type": "MODULE", "owner_id": <module_id> }
{ "parent_kind": "MODULE", "parent_id": <module_id> }
```
**Verify:** `GET /modules/<module_id>/practices`
### Course practice
```json
{ "owner_type": "COURSE", "owner_id": <course_id> }
{ "parent_kind": "COURSE", "parent_id": <course_id> }
```
**Verify:** `GET /courses/<course_id>/practices`
---
## 18. API index
| # | Method | Path | Permission |
|---|--------|------|------------|
| 1 | GET | `/questions/component-catalog` | `questions.list` |
| 2 | GET | `/questions/type-definitions` | `questions.list` |
| 3 | POST | `/questions/type-definitions` | `questions.create` |
| 4 | POST | `/questions/validate-question-type-definition` | `questions.create` |
| 5 | POST | `/files/upload` | auth |
| 6 | GET | `/files/url` | auth |
| 7 | POST | `/questions` | `questions.create` |
| 8 | PUT | `/questions/:id` | `questions.update` |
| 9 | POST | `/question-sets` | `question_sets.create` |
| 10 | POST | `/question-sets/:setId/questions` | `question_set_items.add` |
| 11 | GET | `/question-sets/:setId/questions` | `question_set_items.list` |
| 12 | GET | `/question-sets/:setId/question-types` | `question_set_items.list` |
| 13 | PUT | `/question-sets/:setId/questions/:questionId/order` | `question_set_items.update_order` |
| 14 | DELETE | `/question-sets/:setId/questions/:questionId` | `question_set_items.remove` |
| 15 | POST | `/practices` | `practices.create` |
| 16 | GET | `/practices/:id` | `practices.get` |
| 17 | PUT | `/practices/:id` | `practices.update` |
| 18 | DELETE | `/practices/:id` | `practices.delete` |
| 19 | GET | `/lessons/:id/practices` | `practices.list` |
| 20 | GET | `/modules/:id/practices` | `practices.list` |
| 21 | GET | `/courses/:id/practices` | `practices.list` |
| 22 | GET | `/practices/:practiceId/questions` | `question_set_items.list` (see §14.3 note) |
---
## 19. QA checklist
- [ ] Parent course/module/lesson exists (`GET` returns 200)
- [ ] Definition includes `TABLE` (or other) slots used in payload
- [ ] Dynamic question created without `question_text` in request
- [ ] TABLE `value` has `columns` + `rows` aligned
- [ ] Question set `set_type` is `PRACTICE` and `owner_type` matches practice scope
- [ ] All questions added to set with correct `display_order`
- [ ] Practice `question_set_id` matches set id
- [ ] `parent_kind` / `parent_id` match intended scope
- [ ] `GET` list practices under parent shows new practice
- [ ] `GET /question-sets/:id/questions` shows `dynamic_payload`
- [ ] Publish: question `PUBLISHED`, set `PUBLISHED`, practice `publish_status: PUBLISHED` when going live
- [ ] `OPEN_LEARNER` sees unlocked content; `STUDENT` respects practice sequence on same owner scope
---
## Pitfalls
1. **Do not send `question_text`** on dynamic question create/update — use `QUESTION_TEXT` (or `INSTRUCTION`) in `dynamic_payload.stimulus`.
2. **`owner_type` on question set** should match **`parent_kind` on practice** for consistent gating and admin filters.
3. **One practice → one `question_set_id`** in normal authoring; add multiple questions to the **same set**, not multiple sets per practice.
4. **TABLE content is per question** — the definition only declares the slot; each `POST /questions` supplies its own `columns` / `rows`.
5. **`GET /practices/:practiceId/questions`** — use `question_set_id` from practice when calling set-based list endpoints (see §14.3).
6. **Dynamic scoring runtime** — verify learner app supports your definitions response shapes before release.
---
*Last aligned with backend: LMS practices (`COURSE`/`MODULE`/`LESSON`), dynamic questions, `PDF_ATTACHMENT`, `TABLE` stimulus, practice `publish_status`, DYNAMIC `question_text` API omission.*