README update
This commit is contained in:
parent
a9c6966820
commit
1c8d041747
|
|
@ -1,4 +1,4 @@
|
||||||
# Yimaru Backend API
|
# Yimaru Backend
|
||||||
|
|
||||||
Yimaru Backend is the server-side application that powers the Yimaru online learning system. It manages courses, lessons, quizzes, student progress, instructor content, and administrative operations for institutions and users on the platform.
|
Yimaru Backend is the server-side application that powers the Yimaru online learning system. It manages courses, lessons, quizzes, student progress, instructor content, and administrative operations for institutions and users on the platform.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ services:
|
||||||
container_name: yimaru-backend-postgres-1
|
container_name: yimaru-backend-postgres-1
|
||||||
image: postgres:16-alpine
|
image: postgres:16-alpine
|
||||||
ports:
|
ports:
|
||||||
- "5592:5422"
|
- "5692:5422"
|
||||||
environment:
|
environment:
|
||||||
- POSTGRES_PASSWORD=secret
|
- POSTGRES_PASSWORD=secret
|
||||||
- POSTGRES_USER=root
|
- POSTGRES_USER=root
|
||||||
|
|
|
||||||
825
docs/COURSE_MANAGEMENT_POSTMAN_COLLECTION.json
Normal file
825
docs/COURSE_MANAGEMENT_POSTMAN_COLLECTION.json
Normal file
|
|
@ -0,0 +1,825 @@
|
||||||
|
{
|
||||||
|
"info": {
|
||||||
|
"_postman_id": "d8d17a29-5f9c-4f06-95fd-12e9862f97f8",
|
||||||
|
"name": "Yimaru Backend - Course Management APIs",
|
||||||
|
"description": "Fully documented Postman collection for all course-management related endpoints in Yimaru Backend.\n\nAuthentication:\n- All endpoints require `Authorization: Bearer {{accessToken}}`.\n\nBase URL:\n- `{{baseUrl}}/api/{{apiVersion}}`\n\nNotes:\n- IDs in path params must be positive integers.\n- Some update endpoints support partial updates.\n- Endpoint-level permission requirements are documented in each request description.",
|
||||||
|
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||||
|
},
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "baseUrl",
|
||||||
|
"value": "https://api.yimaruacademy.com",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "apiVersion",
|
||||||
|
"value": "v1",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "accessToken",
|
||||||
|
"value": "",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "categoryId",
|
||||||
|
"value": "1",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "subCategoryId",
|
||||||
|
"value": "1",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "courseId",
|
||||||
|
"value": "1",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "levelId",
|
||||||
|
"value": "1",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "moduleId",
|
||||||
|
"value": "1",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "subModuleId",
|
||||||
|
"value": "1",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "videoId",
|
||||||
|
"value": "1",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "questionSetId",
|
||||||
|
"value": "1",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "practiceId",
|
||||||
|
"value": "1",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"auth": {
|
||||||
|
"type": "bearer",
|
||||||
|
"bearer": [
|
||||||
|
{
|
||||||
|
"key": "token",
|
||||||
|
"value": "{{accessToken}}",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "Categories & Courses",
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "List Course Categories",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/api/{{apiVersion}}/course-management/categories",
|
||||||
|
"host": [
|
||||||
|
"{{baseUrl}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"{{apiVersion}}",
|
||||||
|
"course-management",
|
||||||
|
"categories"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "Returns all course categories.\n\nPermission: `learning_tree.get`."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Create Course Category",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"name\": \"Grammar\",\n \"is_active\": true\n}"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/api/{{apiVersion}}/course-management/categories",
|
||||||
|
"host": [
|
||||||
|
"{{baseUrl}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"{{apiVersion}}",
|
||||||
|
"course-management",
|
||||||
|
"categories"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "Creates a course category.\n\nRequired fields:\n- `name` (string)\n\nOptional fields:\n- `is_active` (boolean)\n\nPermission: `course_categories.create`."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Delete Course Category",
|
||||||
|
"request": {
|
||||||
|
"method": "DELETE",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/api/{{apiVersion}}/course-management/categories/{{categoryId}}",
|
||||||
|
"host": [
|
||||||
|
"{{baseUrl}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"{{apiVersion}}",
|
||||||
|
"course-management",
|
||||||
|
"categories",
|
||||||
|
"{{categoryId}}"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "Deletes a category by ID.\n\nPath params:\n- `categoryId` (int, required)\n\nPermission: `course_categories.delete`."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "List Courses By Category",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/api/{{apiVersion}}/course-management/categories/{{categoryId}}/courses?offset=0&limit=50",
|
||||||
|
"host": [
|
||||||
|
"{{baseUrl}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"{{apiVersion}}",
|
||||||
|
"course-management",
|
||||||
|
"categories",
|
||||||
|
"{{categoryId}}",
|
||||||
|
"courses"
|
||||||
|
],
|
||||||
|
"query": [
|
||||||
|
{
|
||||||
|
"key": "offset",
|
||||||
|
"value": "0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "limit",
|
||||||
|
"value": "50"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "Returns all courses under a category.\n\nPath params:\n- `categoryId` (int, required)\n\nQuery params:\n- `offset` (int, optional, default 0)\n- `limit` (int, optional, default 10000)\n\nPermission: `learning_tree.get`."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Create Course",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"category_id\": 1,\n \"sub_category_id\": 1,\n \"title\": \"English Basics\",\n \"description\": \"Beginner-level course\",\n \"thumbnail\": \"https://cdn.example.com/course-thumb.jpg\",\n \"intro_video_url\": \"https://cdn.example.com/intro.mp4\",\n \"is_active\": true\n}"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/api/{{apiVersion}}/course-management/courses",
|
||||||
|
"host": [
|
||||||
|
"{{baseUrl}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"{{apiVersion}}",
|
||||||
|
"course-management",
|
||||||
|
"courses"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "Creates a course.\n\nRequired fields:\n- `category_id` (int)\n- `title` (string)\n\nOptional fields:\n- `sub_category_id` (int)\n- `description` (string)\n- `thumbnail` (string URL)\n- `intro_video_url` (string URL)\n- `is_active` (boolean)\n\nPermission: `courses.create`."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Update Course",
|
||||||
|
"request": {
|
||||||
|
"method": "PUT",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"title\": \"English Basics - Updated\",\n \"description\": \"Updated course description\",\n \"thumbnail\": \"https://cdn.example.com/new-thumb.jpg\",\n \"intro_video_url\": \"https://cdn.example.com/new-intro.mp4\",\n \"is_active\": true\n}"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/api/{{apiVersion}}/course-management/courses/{{courseId}}",
|
||||||
|
"host": [
|
||||||
|
"{{baseUrl}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"{{apiVersion}}",
|
||||||
|
"course-management",
|
||||||
|
"courses",
|
||||||
|
"{{courseId}}"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "Updates an existing course (partial update accepted).\n\nPath params:\n- `courseId` (int, required)\n\nOptional fields:\n- `title`, `description`, `thumbnail`, `intro_video_url`, `is_active`\n\nPermission: `courses.update`."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Delete Course",
|
||||||
|
"request": {
|
||||||
|
"method": "DELETE",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/api/{{apiVersion}}/course-management/courses/{{courseId}}",
|
||||||
|
"host": [
|
||||||
|
"{{baseUrl}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"{{apiVersion}}",
|
||||||
|
"course-management",
|
||||||
|
"courses",
|
||||||
|
"{{courseId}}"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "Deletes a course.\n\nPath params:\n- `courseId` (int, required)\n\nPermission: `courses.delete`."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Update Course Thumbnail",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"thumbnail_url\": \"https://cdn.example.com/new-thumbnail.jpg\"\n}"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/api/{{apiVersion}}/course-management/courses/{{courseId}}/thumbnail",
|
||||||
|
"host": [
|
||||||
|
"{{baseUrl}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"{{apiVersion}}",
|
||||||
|
"course-management",
|
||||||
|
"courses",
|
||||||
|
"{{courseId}}",
|
||||||
|
"thumbnail"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "Updates only the thumbnail of a course.\n\nPath params:\n- `courseId` (int, required)\n\nRequired fields:\n- `thumbnail_url` (string)\n\nPermission: `courses.upload_thumbnail`."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Hierarchy & Learning Path",
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "Get Unified Hierarchy",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/api/{{apiVersion}}/course-management/hierarchy",
|
||||||
|
"host": [
|
||||||
|
"{{baseUrl}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"{{apiVersion}}",
|
||||||
|
"course-management",
|
||||||
|
"hierarchy"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "Returns global hierarchy data.\n\nPermission: `learning_tree.get`."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Get Human Language Hierarchy",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/api/{{apiVersion}}/course-management/human-language/hierarchy",
|
||||||
|
"host": [
|
||||||
|
"{{baseUrl}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"{{apiVersion}}",
|
||||||
|
"course-management",
|
||||||
|
"human-language",
|
||||||
|
"hierarchy"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "Alias endpoint for unified hierarchy under human-language path.\n\nPermission: `learning_tree.get`."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Get Course Hierarchy",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/api/{{apiVersion}}/course-management/courses/{{courseId}}/hierarchy",
|
||||||
|
"host": [
|
||||||
|
"{{baseUrl}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"{{apiVersion}}",
|
||||||
|
"course-management",
|
||||||
|
"courses",
|
||||||
|
"{{courseId}}",
|
||||||
|
"hierarchy"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "Returns hierarchy nodes for one course.\n\nPath params:\n- `courseId` (int, required)\n\nPermission: `learning_tree.get`."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Get Course Learning Path",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/api/{{apiVersion}}/course-management/courses/{{courseId}}/learning-path",
|
||||||
|
"host": [
|
||||||
|
"{{baseUrl}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"{{apiVersion}}",
|
||||||
|
"course-management",
|
||||||
|
"courses",
|
||||||
|
"{{courseId}}",
|
||||||
|
"learning-path"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "Returns learning-path projection for a course including sub-modules, videos, and practices.\n\nPath params:\n- `courseId` (int, required)\n\nPermission: `learning_tree.get`."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Sub-Categories, Levels, Modules, Sub-Modules",
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "Create Course Sub-Category",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"category_id\": 1,\n \"name\": \"Everyday Conversation\",\n \"description\": \"Spoken communication track\",\n \"display_order\": 1,\n \"is_active\": true\n}"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/api/{{apiVersion}}/course-management/sub-categories",
|
||||||
|
"host": [
|
||||||
|
"{{baseUrl}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"{{apiVersion}}",
|
||||||
|
"course-management",
|
||||||
|
"sub-categories"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "Creates a sub-category under a course category.\n\nRequired fields:\n- `category_id` (int)\n- `name` (string)\n\nOptional fields:\n- `description` (string)\n- `display_order` (int)\n- `is_active` (boolean)\n\nPermission: `course_categories.create`."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Delete Course Sub-Category",
|
||||||
|
"request": {
|
||||||
|
"method": "DELETE",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/api/{{apiVersion}}/course-management/sub-categories/{{subCategoryId}}",
|
||||||
|
"host": [
|
||||||
|
"{{baseUrl}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"{{apiVersion}}",
|
||||||
|
"course-management",
|
||||||
|
"sub-categories",
|
||||||
|
"{{subCategoryId}}"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "Deletes a sub-category.\n\nPath params:\n- `subCategoryId` (int, required)\n\nPermission: `course_categories.delete`."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Create Level",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"course_id\": 1,\n \"cefr_level\": \"A1\",\n \"display_order\": 1,\n \"is_active\": true\n}"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/api/{{apiVersion}}/course-management/levels",
|
||||||
|
"host": [
|
||||||
|
"{{baseUrl}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"{{apiVersion}}",
|
||||||
|
"course-management",
|
||||||
|
"levels"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "Creates a CEFR level under a course.\n\nRequired fields:\n- `course_id` (int)\n- `cefr_level` (one of: A1, A2, A3, B1, B2, B3, C1, C2, C3)\n\nOptional fields:\n- `display_order` (int)\n- `is_active` (boolean)\n\nPermission: `subcourses.create`."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Create Module",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"level_id\": 1,\n \"title\": \"Module 1: Introductions\",\n \"description\": \"Core introduction module\",\n \"display_order\": 1,\n \"is_active\": true\n}"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/api/{{apiVersion}}/course-management/modules",
|
||||||
|
"host": [
|
||||||
|
"{{baseUrl}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"{{apiVersion}}",
|
||||||
|
"course-management",
|
||||||
|
"modules"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "Creates a module under a level.\n\nRequired fields:\n- `level_id` (int)\n- `title` (string)\n\nOptional fields:\n- `description`, `display_order`, `is_active`\n\nPermission: `subcourses.create`."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Delete Module",
|
||||||
|
"request": {
|
||||||
|
"method": "DELETE",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/api/{{apiVersion}}/course-management/modules/{{moduleId}}",
|
||||||
|
"host": [
|
||||||
|
"{{baseUrl}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"{{apiVersion}}",
|
||||||
|
"course-management",
|
||||||
|
"modules",
|
||||||
|
"{{moduleId}}"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "Deletes a module.\n\nPath params:\n- `moduleId` (int, required)\n\nPermission: `subcourses.delete`."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Create Sub-Module",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"module_id\": 1,\n \"title\": \"Sub-Module 1: Greetings\",\n \"description\": \"Greetings and polite expressions\",\n \"display_order\": 1,\n \"is_active\": true\n}"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/api/{{apiVersion}}/course-management/sub-modules",
|
||||||
|
"host": [
|
||||||
|
"{{baseUrl}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"{{apiVersion}}",
|
||||||
|
"course-management",
|
||||||
|
"sub-modules"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "Creates a sub-module under a module.\n\nRequired fields:\n- `module_id` (int)\n- `title` (string)\n\nOptional fields:\n- `description`, `display_order`, `is_active`\n\nPermission: `subcourses.create`."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Update Sub-Module",
|
||||||
|
"request": {
|
||||||
|
"method": "PUT",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"title\": \"Sub-Module 1: Greetings (Updated)\",\n \"description\": \"Updated sub-module description\",\n \"is_active\": true\n}"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/api/{{apiVersion}}/course-management/sub-modules/{{subModuleId}}",
|
||||||
|
"host": [
|
||||||
|
"{{baseUrl}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"{{apiVersion}}",
|
||||||
|
"course-management",
|
||||||
|
"sub-modules",
|
||||||
|
"{{subModuleId}}"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "Updates a sub-module.\n\nPath params:\n- `subModuleId` (int, required)\n\nOptional fields:\n- `title`, `description`, `is_active`\n\nPermission: `subcourses.update`."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Delete Sub-Module",
|
||||||
|
"request": {
|
||||||
|
"method": "DELETE",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/api/{{apiVersion}}/course-management/sub-modules/{{subModuleId}}",
|
||||||
|
"host": [
|
||||||
|
"{{baseUrl}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"{{apiVersion}}",
|
||||||
|
"course-management",
|
||||||
|
"sub-modules",
|
||||||
|
"{{subModuleId}}"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "Deletes a sub-module.\n\nPath params:\n- `subModuleId` (int, required)\n\nPermission: `subcourses.delete`."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Sub-Module Videos",
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "Get Sub-Module Videos",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/api/{{apiVersion}}/course-management/sub-modules/{{subModuleId}}/videos",
|
||||||
|
"host": [
|
||||||
|
"{{baseUrl}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"{{apiVersion}}",
|
||||||
|
"course-management",
|
||||||
|
"sub-modules",
|
||||||
|
"{{subModuleId}}",
|
||||||
|
"videos"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "Lists videos for a given sub-module.\n\nPath params:\n- `subModuleId` (int, required)\n\nPermission: `videos.list`."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Create Sub-Module Video",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"sub_module_id\": 1,\n \"title\": \"Greeting Expressions\",\n \"description\": \"Main lesson video\",\n \"video_url\": \"https://cdn.example.com/videos/greetings.mp4\",\n \"duration\": 480,\n \"resolution\": \"1080p\",\n \"visibility\": \"public\",\n \"instructor_id\": \"instructor-123\",\n \"thumbnail\": \"https://cdn.example.com/thumbs/greetings.jpg\",\n \"display_order\": 1,\n \"status\": \"published\"\n}"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/api/{{apiVersion}}/course-management/sub-module-videos",
|
||||||
|
"host": [
|
||||||
|
"{{baseUrl}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"{{apiVersion}}",
|
||||||
|
"course-management",
|
||||||
|
"sub-module-videos"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "Creates a video under a sub-module.\n\nRequired fields:\n- `sub_module_id` (int)\n- `title` (string)\n- `video_url` (string URL)\n\nOptional fields:\n- `description`, `duration`, `resolution`, `visibility`, `instructor_id`, `thumbnail`, `display_order`, `status`\n\nPermission: `videos.create`."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Update Sub-Module Video",
|
||||||
|
"request": {
|
||||||
|
"method": "PUT",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"title\": \"Greeting Expressions - Updated\",\n \"description\": \"Updated video description\",\n \"video_url\": \"https://cdn.example.com/videos/greetings-v2.mp4\"\n}"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/api/{{apiVersion}}/course-management/sub-module-videos/{{videoId}}",
|
||||||
|
"host": [
|
||||||
|
"{{baseUrl}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"{{apiVersion}}",
|
||||||
|
"course-management",
|
||||||
|
"sub-module-videos",
|
||||||
|
"{{videoId}}"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "Updates a sub-module video.\n\nPath params:\n- `videoId` (int, required)\n\nRequired fields:\n- `title` (string)\n- `video_url` (string URL)\n\nOptional fields:\n- `description`\n\nPermission: `videos.update`."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Delete Sub-Module Video",
|
||||||
|
"request": {
|
||||||
|
"method": "DELETE",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/api/{{apiVersion}}/course-management/sub-module-videos/{{videoId}}",
|
||||||
|
"host": [
|
||||||
|
"{{baseUrl}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"{{apiVersion}}",
|
||||||
|
"course-management",
|
||||||
|
"sub-module-videos",
|
||||||
|
"{{videoId}}"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "Deletes a sub-module video.\n\nPath params:\n- `videoId` (int, required)\n\nPermission: `videos.delete`."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Lessons & Practices",
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "Attach Sub-Module Lesson (Question Set)",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"sub_module_id\": 1,\n \"question_set_id\": 1,\n \"intro_video_url\": \"https://cdn.example.com/intro-lesson.mp4\",\n \"display_order\": 1,\n \"is_active\": true\n}"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/api/{{apiVersion}}/course-management/sub-module-lessons",
|
||||||
|
"host": [
|
||||||
|
"{{baseUrl}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"{{apiVersion}}",
|
||||||
|
"course-management",
|
||||||
|
"sub-module-lessons"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "Links a question set lesson to a sub-module.\n\nRequired fields:\n- `sub_module_id` (int)\n- `question_set_id` (int)\n\nOptional fields:\n- `intro_video_url`, `display_order`, `is_active`\n\nPermission: `question_sets.update`."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Create Sub-Module Practice",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"sub_module_id\": 1,\n \"title\": \"Practice: Basic Greetings\",\n \"description\": \"Practice set for greetings\",\n \"thumbnail\": \"https://cdn.example.com/practice-thumb.jpg\",\n \"intro_video_url\": \"https://cdn.example.com/practice-intro.mp4\",\n \"question_set_id\": 1,\n \"display_order\": 1,\n \"is_active\": true\n}"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/api/{{apiVersion}}/course-management/sub-module-practices",
|
||||||
|
"host": [
|
||||||
|
"{{baseUrl}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"{{apiVersion}}",
|
||||||
|
"course-management",
|
||||||
|
"sub-module-practices"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "Creates a practice under a sub-module.\n\nRequired fields:\n- `sub_module_id` (int)\n- `title` (string)\n- `question_set_id` (int)\n\nOptional fields:\n- `description`, `thumbnail`, `intro_video_url`, `display_order`, `is_active`\n\nPermission: `question_sets.update`."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Update Practice",
|
||||||
|
"request": {
|
||||||
|
"method": "PUT",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"title\": \"Practice: Basic Greetings - Updated\",\n \"description\": \"Updated practice details\",\n \"persona\": \"student\",\n \"is_active\": true\n}"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/api/{{apiVersion}}/course-management/practices/{{practiceId}}",
|
||||||
|
"host": [
|
||||||
|
"{{baseUrl}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"{{apiVersion}}",
|
||||||
|
"course-management",
|
||||||
|
"practices",
|
||||||
|
"{{practiceId}}"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "Updates a practice.\n\nPath params:\n- `practiceId` (int, required)\n\nBehavior:\n- If `is_active` is provided, this endpoint updates practice status.\n- Otherwise, `title` is required and metadata update is applied.\n\nPermission: `question_sets.update`."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Delete Practice",
|
||||||
|
"request": {
|
||||||
|
"method": "DELETE",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/api/{{apiVersion}}/course-management/practices/{{practiceId}}",
|
||||||
|
"host": [
|
||||||
|
"{{baseUrl}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"{{apiVersion}}",
|
||||||
|
"course-management",
|
||||||
|
"practices",
|
||||||
|
"{{practiceId}}"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "Deletes a practice.\n\nPath params:\n- `practiceId` (int, required)\n\nPermission: `question_sets.delete`."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -1,292 +0,0 @@
|
||||||
# Human Language Mobile Integration Guide
|
|
||||||
|
|
||||||
This guide explains how to integrate the new **Human Language** feature in the **Yimaru learner mobile app** (not admin).
|
|
||||||
|
|
||||||
It is designed to keep the existing non-language hierarchy intact while introducing a dedicated CEFR-based flow for language learning.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1) Scope and Goals
|
|
||||||
|
|
||||||
### What is new
|
|
||||||
|
|
||||||
- A dedicated backend API namespace for Human Language:
|
|
||||||
- `GET /api/v1/course-management/human-language/courses/:courseId/lessons?cefr_level=A1..C3`
|
|
||||||
- `POST /api/v1/course-management/human-language/lessons` (admin/instructor side)
|
|
||||||
- `PATCH /api/v1/course-management/human-language/lessons/:id` (admin/instructor side)
|
|
||||||
- CEFR levels are fixed to:
|
|
||||||
- `A1, A2, A3, B1, B2, B3, C1, C2, C3`
|
|
||||||
- No custom sub-levels in Human Language flow.
|
|
||||||
|
|
||||||
### What remains unchanged
|
|
||||||
|
|
||||||
- Existing non-human-language content hierarchy and APIs.
|
|
||||||
- Existing course/category/sub-course endpoints for non-language domains (programming, etc.).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2) Target Hierarchy for Human Language
|
|
||||||
|
|
||||||
- Course Category (Human Language)
|
|
||||||
- Course (e.g., English)
|
|
||||||
- CEFR Lesson Unit (A1..C3 only)
|
|
||||||
- Intro/lesson videos
|
|
||||||
- Practices
|
|
||||||
- Audio questions
|
|
||||||
|
|
||||||
Backend implementation stores CEFR lesson units using the existing sub-course model, with CEFR mapped internally and validated strictly.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3) Authentication and Access
|
|
||||||
|
|
||||||
All Human Language endpoints are under `/api/v1` and require bearer token auth.
|
|
||||||
|
|
||||||
- Header:
|
|
||||||
- `Authorization: Bearer <access_token>`
|
|
||||||
|
|
||||||
### Permission notes
|
|
||||||
|
|
||||||
- **Learner mobile app** typically needs only the `GET` endpoint.
|
|
||||||
- `POST` and `PATCH` are content-management endpoints and should generally be used by admin/instructor clients, not learner clients.
|
|
||||||
|
|
||||||
If learner roles do not currently have `learning_tree.get`, coordinate RBAC assignment for read-only access to the `GET` endpoint.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4) Endpoint Contracts
|
|
||||||
|
|
||||||
## 4.1 Fetch lessons by CEFR level (learner-facing)
|
|
||||||
|
|
||||||
### Request
|
|
||||||
|
|
||||||
`GET /api/v1/course-management/human-language/courses/{courseId}/lessons?cefr_level={A1|A2|A3|B1|B2|B3|C1|C2|C3}`
|
|
||||||
|
|
||||||
### Example
|
|
||||||
|
|
||||||
`GET /api/v1/course-management/human-language/courses/12/lessons?cefr_level=B1`
|
|
||||||
|
|
||||||
### Response (success)
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"message": "Human-language lessons retrieved successfully",
|
|
||||||
"data": {
|
|
||||||
"course_id": 12,
|
|
||||||
"course_title": "English",
|
|
||||||
"cefr_level": "B1",
|
|
||||||
"lessons": [
|
|
||||||
{
|
|
||||||
"id": 201,
|
|
||||||
"course_id": 12,
|
|
||||||
"title": "B1 Module 1",
|
|
||||||
"description": "Intermediate conversational patterns",
|
|
||||||
"thumbnail": "https://.../thumb.jpg",
|
|
||||||
"display_order": 1,
|
|
||||||
"level": "B1",
|
|
||||||
"video_count": 3,
|
|
||||||
"practice_count": 2,
|
|
||||||
"videos": [
|
|
||||||
{
|
|
||||||
"id": 9001,
|
|
||||||
"title": "B1 Intro",
|
|
||||||
"description": "Lesson intro",
|
|
||||||
"video_url": "https://...",
|
|
||||||
"duration": 420,
|
|
||||||
"resolution": "1080p",
|
|
||||||
"display_order": 1
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"practices": [
|
|
||||||
{
|
|
||||||
"id": 4401,
|
|
||||||
"title": "B1 Speaking Practice",
|
|
||||||
"status": "PUBLISHED",
|
|
||||||
"question_count": 10
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"success": true,
|
|
||||||
"status_code": 200,
|
|
||||||
"metadata": null
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Validation errors
|
|
||||||
|
|
||||||
- Invalid CEFR level:
|
|
||||||
- `400` with error message: `Use one of: A1,A2,A3,B1,B2,B3,C1,C2,C3`
|
|
||||||
- Course not in human language category:
|
|
||||||
- `400` with error message indicating invalid human-language course.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4.2 Create Human Language lesson unit (admin/instructor)
|
|
||||||
|
|
||||||
### Request
|
|
||||||
|
|
||||||
`POST /api/v1/course-management/human-language/lessons`
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"course_id": 12,
|
|
||||||
"title": "A2 Module 1",
|
|
||||||
"description": "A2 speaking fundamentals",
|
|
||||||
"thumbnail": "https://...",
|
|
||||||
"display_order": 1,
|
|
||||||
"cefr_level": "A2"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Response
|
|
||||||
|
|
||||||
Returns created lesson metadata with CEFR-normalized level behavior.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4.3 Update Human Language lesson unit (admin/instructor)
|
|
||||||
|
|
||||||
### Request
|
|
||||||
|
|
||||||
`PATCH /api/v1/course-management/human-language/lessons/{lessonId}`
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"title": "A2 Module 1 - Updated",
|
|
||||||
"description": "Updated description",
|
|
||||||
"cefr_level": "A3",
|
|
||||||
"is_active": true
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5) Recommended Mobile App Integration Flow
|
|
||||||
|
|
||||||
## Step 1: Discover courses under Human Language category
|
|
||||||
|
|
||||||
Use existing endpoints:
|
|
||||||
|
|
||||||
1. `GET /course-management/categories`
|
|
||||||
2. Pick category where name indicates human language.
|
|
||||||
3. `GET /course-management/categories/:categoryId/courses`
|
|
||||||
|
|
||||||
## Step 2: Select CEFR level in UI
|
|
||||||
|
|
||||||
Use static list in app:
|
|
||||||
|
|
||||||
- `A1, A2, A3, B1, B2, B3, C1, C2, C3`
|
|
||||||
|
|
||||||
Do not allow custom levels in mobile UI.
|
|
||||||
|
|
||||||
## Step 3: Fetch lessons by selected CEFR level
|
|
||||||
|
|
||||||
Call:
|
|
||||||
|
|
||||||
- `GET /course-management/human-language/courses/:courseId/lessons?cefr_level=<selected>`
|
|
||||||
|
|
||||||
Render grouped lessons with nested videos/practices from response.
|
|
||||||
|
|
||||||
## Step 4: Navigate to learning/practice screens
|
|
||||||
|
|
||||||
- Lesson video playback uses returned `videos[]`.
|
|
||||||
- Practice entry uses returned `practices[]` IDs and existing practice-question endpoints.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6) Mobile UI/UX Recommendations
|
|
||||||
|
|
||||||
- Use CEFR tabs/segmented control (`A1`...`C3`) at top.
|
|
||||||
- Cache last selected level per course.
|
|
||||||
- Show empty state per level:
|
|
||||||
- "No lessons available for this level yet."
|
|
||||||
- Sort by `display_order` and maintain backend order.
|
|
||||||
- Show badges:
|
|
||||||
- video count
|
|
||||||
- practice count
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7) Error Handling and Resilience
|
|
||||||
|
|
||||||
- `400` invalid level: reset selection to previous valid CEFR value and show toast/snackbar.
|
|
||||||
- `401/403`: trigger token refresh / re-login flow.
|
|
||||||
- `5xx`: show retry UI with exponential backoff.
|
|
||||||
- If category/course fetch succeeds but level fetch fails, keep course visible and allow manual retry.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8) Data Model Mapping (Mobile DTO)
|
|
||||||
|
|
||||||
Suggested DTO for learner app:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
type CefrLevel = "A1"|"A2"|"A3"|"B1"|"B2"|"B3"|"C1"|"C2"|"C3";
|
|
||||||
|
|
||||||
interface HumanLanguageLessonDTO {
|
|
||||||
id: number;
|
|
||||||
courseId: number;
|
|
||||||
title: string;
|
|
||||||
description?: string;
|
|
||||||
thumbnail?: string;
|
|
||||||
order: number;
|
|
||||||
level: CefrLevel;
|
|
||||||
videoCount: number;
|
|
||||||
practiceCount: number;
|
|
||||||
videos: {
|
|
||||||
id: number;
|
|
||||||
title: string;
|
|
||||||
url: string;
|
|
||||||
duration: number;
|
|
||||||
order: number;
|
|
||||||
}[];
|
|
||||||
practices: {
|
|
||||||
id: number;
|
|
||||||
title: string;
|
|
||||||
status: string;
|
|
||||||
questionCount: number;
|
|
||||||
}[];
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9) Backward Compatibility
|
|
||||||
|
|
||||||
- Existing non-language content (programming and other categories) continues to use current APIs and hierarchy unchanged.
|
|
||||||
- New Human Language endpoint is additive and isolated.
|
|
||||||
- Mobile app can progressively enable the new flow for language categories only.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 10) QA Checklist for Mobile Team
|
|
||||||
|
|
||||||
- [ ] Category discovery correctly identifies Human Language category.
|
|
||||||
- [ ] CEFR selector only allows A1..C3.
|
|
||||||
- [ ] Fetch by CEFR level returns only matching lessons.
|
|
||||||
- [ ] Video/practice counts match rendered lists.
|
|
||||||
- [ ] Empty-level state works.
|
|
||||||
- [ ] Unauthorized/session-expired flow works.
|
|
||||||
- [ ] Non-language courses still load via existing app flow.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 11) Example Call Matrix
|
|
||||||
|
|
||||||
- Load language categories/courses:
|
|
||||||
- `GET /course-management/categories`
|
|
||||||
- `GET /course-management/categories/:id/courses`
|
|
||||||
- Load A1 lessons:
|
|
||||||
- `GET /course-management/human-language/courses/:courseId/lessons?cefr_level=A1`
|
|
||||||
- Load B2 lessons:
|
|
||||||
- `GET /course-management/human-language/courses/:courseId/lessons?cefr_level=B2`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
For backend ownership questions, refer to:
|
|
||||||
|
|
||||||
- `internal/web_server/handlers/course_management.go`
|
|
||||||
- `internal/web_server/routes.go`
|
|
||||||
|
|
||||||
420
docs/docs.go
420
docs/docs.go
|
|
@ -713,6 +713,267 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v1/course-management/categories": {
|
||||||
|
"get": {
|
||||||
|
"description": "Legacy-compatible endpoint for listing course categories",
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"course-management"
|
||||||
|
],
|
||||||
|
"summary": "List course categories",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.Response"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"description": "Legacy-compatible endpoint for creating a course category",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"course-management"
|
||||||
|
],
|
||||||
|
"summary": "Create course category",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Create category payload",
|
||||||
|
"name": "body",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handlers.createCourseCategoryReq"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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/course-management/categories/{categoryId}/courses": {
|
||||||
|
"get": {
|
||||||
|
"description": "Legacy-compatible endpoint that returns courses for one category",
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"course-management"
|
||||||
|
],
|
||||||
|
"summary": "List courses by category",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Category ID",
|
||||||
|
"name": "categoryId",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Offset",
|
||||||
|
"name": "offset",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Limit",
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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/course-management/courses": {
|
||||||
|
"post": {
|
||||||
|
"description": "Legacy-compatible endpoint for creating a course",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"course-management"
|
||||||
|
],
|
||||||
|
"summary": "Create course",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Create course payload",
|
||||||
|
"name": "body",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handlers.createCourseReq"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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/course-management/courses/{courseId}": {
|
||||||
|
"put": {
|
||||||
|
"description": "Legacy-compatible endpoint for updating a course",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"course-management"
|
||||||
|
],
|
||||||
|
"summary": "Update course",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Course ID",
|
||||||
|
"name": "courseId",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Update course payload",
|
||||||
|
"name": "body",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handlers.updateCourseReq"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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": {
|
||||||
|
"description": "Legacy-compatible endpoint for deleting a course",
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"course-management"
|
||||||
|
],
|
||||||
|
"summary": "Delete course",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Course ID",
|
||||||
|
"name": "courseId",
|
||||||
|
"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/course-management/courses/{courseId}/hierarchy": {
|
"/api/v1/course-management/courses/{courseId}/hierarchy": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Returns hierarchy nodes for one course including levels/modules/sub-modules",
|
"description": "Returns hierarchy nodes for one course including levels/modules/sub-modules",
|
||||||
|
|
@ -754,6 +1015,100 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v1/course-management/courses/{courseId}/learning-path": {
|
||||||
|
"get": {
|
||||||
|
"description": "Legacy-compatible endpoint for course learning path",
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"course-management"
|
||||||
|
],
|
||||||
|
"summary": "Get course learning path",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Course ID",
|
||||||
|
"name": "courseId",
|
||||||
|
"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/course-management/courses/{courseId}/thumbnail": {
|
||||||
|
"post": {
|
||||||
|
"description": "Legacy-compatible endpoint for updating course thumbnail",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"course-management"
|
||||||
|
],
|
||||||
|
"summary": "Update course thumbnail",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Course ID",
|
||||||
|
"name": "courseId",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Update course thumbnail payload",
|
||||||
|
"name": "body",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handlers.updateCourseThumbnailReq"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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/course-management/hierarchy": {
|
"/api/v1/course-management/hierarchy": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Returns full hierarchy: category -\u003e sub-category -\u003e course",
|
"description": "Returns full hierarchy: category -\u003e sub-category -\u003e course",
|
||||||
|
|
@ -9057,6 +9412,43 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"handlers.createCourseCategoryReq": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"is_active": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"handlers.createCourseReq": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"category_id": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"intro_video_url": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"is_active": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"sub_category_id": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"thumbnail": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"handlers.createCourseSubCategoryReq": {
|
"handlers.createCourseSubCategoryReq": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
@ -9658,6 +10050,34 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"handlers.updateCourseReq": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"description": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"intro_video_url": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"is_active": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"thumbnail": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"handlers.updateCourseThumbnailReq": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"thumbnail_url": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"handlers.updateIssueStatusReq": {
|
"handlers.updateIssueStatusReq": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
|
|
||||||
|
|
@ -705,6 +705,267 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v1/course-management/categories": {
|
||||||
|
"get": {
|
||||||
|
"description": "Legacy-compatible endpoint for listing course categories",
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"course-management"
|
||||||
|
],
|
||||||
|
"summary": "List course categories",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.Response"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"description": "Legacy-compatible endpoint for creating a course category",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"course-management"
|
||||||
|
],
|
||||||
|
"summary": "Create course category",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Create category payload",
|
||||||
|
"name": "body",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handlers.createCourseCategoryReq"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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/course-management/categories/{categoryId}/courses": {
|
||||||
|
"get": {
|
||||||
|
"description": "Legacy-compatible endpoint that returns courses for one category",
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"course-management"
|
||||||
|
],
|
||||||
|
"summary": "List courses by category",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Category ID",
|
||||||
|
"name": "categoryId",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Offset",
|
||||||
|
"name": "offset",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Limit",
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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/course-management/courses": {
|
||||||
|
"post": {
|
||||||
|
"description": "Legacy-compatible endpoint for creating a course",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"course-management"
|
||||||
|
],
|
||||||
|
"summary": "Create course",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Create course payload",
|
||||||
|
"name": "body",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handlers.createCourseReq"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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/course-management/courses/{courseId}": {
|
||||||
|
"put": {
|
||||||
|
"description": "Legacy-compatible endpoint for updating a course",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"course-management"
|
||||||
|
],
|
||||||
|
"summary": "Update course",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Course ID",
|
||||||
|
"name": "courseId",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Update course payload",
|
||||||
|
"name": "body",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handlers.updateCourseReq"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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": {
|
||||||
|
"description": "Legacy-compatible endpoint for deleting a course",
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"course-management"
|
||||||
|
],
|
||||||
|
"summary": "Delete course",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Course ID",
|
||||||
|
"name": "courseId",
|
||||||
|
"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/course-management/courses/{courseId}/hierarchy": {
|
"/api/v1/course-management/courses/{courseId}/hierarchy": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Returns hierarchy nodes for one course including levels/modules/sub-modules",
|
"description": "Returns hierarchy nodes for one course including levels/modules/sub-modules",
|
||||||
|
|
@ -746,6 +1007,100 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v1/course-management/courses/{courseId}/learning-path": {
|
||||||
|
"get": {
|
||||||
|
"description": "Legacy-compatible endpoint for course learning path",
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"course-management"
|
||||||
|
],
|
||||||
|
"summary": "Get course learning path",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Course ID",
|
||||||
|
"name": "courseId",
|
||||||
|
"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/course-management/courses/{courseId}/thumbnail": {
|
||||||
|
"post": {
|
||||||
|
"description": "Legacy-compatible endpoint for updating course thumbnail",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"course-management"
|
||||||
|
],
|
||||||
|
"summary": "Update course thumbnail",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Course ID",
|
||||||
|
"name": "courseId",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Update course thumbnail payload",
|
||||||
|
"name": "body",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handlers.updateCourseThumbnailReq"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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/course-management/hierarchy": {
|
"/api/v1/course-management/hierarchy": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Returns full hierarchy: category -\u003e sub-category -\u003e course",
|
"description": "Returns full hierarchy: category -\u003e sub-category -\u003e course",
|
||||||
|
|
@ -9049,6 +9404,43 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"handlers.createCourseCategoryReq": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"is_active": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"handlers.createCourseReq": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"category_id": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"intro_video_url": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"is_active": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"sub_category_id": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"thumbnail": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"handlers.createCourseSubCategoryReq": {
|
"handlers.createCourseSubCategoryReq": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
@ -9650,6 +10042,34 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"handlers.updateCourseReq": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"description": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"intro_video_url": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"is_active": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"thumbnail": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"handlers.updateCourseThumbnailReq": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"thumbnail_url": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"handlers.updateIssueStatusReq": {
|
"handlers.updateIssueStatusReq": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
|
|
||||||
|
|
@ -947,6 +947,30 @@ definitions:
|
||||||
confirm:
|
confirm:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
|
handlers.createCourseCategoryReq:
|
||||||
|
properties:
|
||||||
|
is_active:
|
||||||
|
type: boolean
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
handlers.createCourseReq:
|
||||||
|
properties:
|
||||||
|
category_id:
|
||||||
|
type: integer
|
||||||
|
description:
|
||||||
|
type: string
|
||||||
|
intro_video_url:
|
||||||
|
type: string
|
||||||
|
is_active:
|
||||||
|
type: boolean
|
||||||
|
sub_category_id:
|
||||||
|
type: integer
|
||||||
|
thumbnail:
|
||||||
|
type: string
|
||||||
|
title:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
handlers.createCourseSubCategoryReq:
|
handlers.createCourseSubCategoryReq:
|
||||||
properties:
|
properties:
|
||||||
category_id:
|
category_id:
|
||||||
|
|
@ -1354,6 +1378,24 @@ definitions:
|
||||||
example: false
|
example: false
|
||||||
type: boolean
|
type: boolean
|
||||||
type: object
|
type: object
|
||||||
|
handlers.updateCourseReq:
|
||||||
|
properties:
|
||||||
|
description:
|
||||||
|
type: string
|
||||||
|
intro_video_url:
|
||||||
|
type: string
|
||||||
|
is_active:
|
||||||
|
type: boolean
|
||||||
|
thumbnail:
|
||||||
|
type: string
|
||||||
|
title:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
handlers.updateCourseThumbnailReq:
|
||||||
|
properties:
|
||||||
|
thumbnail_url:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
handlers.updateIssueStatusReq:
|
handlers.updateIssueStatusReq:
|
||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
|
|
@ -2323,6 +2365,178 @@ paths:
|
||||||
summary: Refresh token
|
summary: Refresh token
|
||||||
tags:
|
tags:
|
||||||
- auth
|
- auth
|
||||||
|
/api/v1/course-management/categories:
|
||||||
|
get:
|
||||||
|
description: Legacy-compatible endpoint for listing course categories
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/domain.Response'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/domain.ErrorResponse'
|
||||||
|
summary: List course categories
|
||||||
|
tags:
|
||||||
|
- course-management
|
||||||
|
post:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Legacy-compatible endpoint for creating a course category
|
||||||
|
parameters:
|
||||||
|
- description: Create category payload
|
||||||
|
in: body
|
||||||
|
name: body
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/handlers.createCourseCategoryReq'
|
||||||
|
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 course category
|
||||||
|
tags:
|
||||||
|
- course-management
|
||||||
|
/api/v1/course-management/categories/{categoryId}/courses:
|
||||||
|
get:
|
||||||
|
description: Legacy-compatible endpoint that returns courses for one category
|
||||||
|
parameters:
|
||||||
|
- description: Category ID
|
||||||
|
in: path
|
||||||
|
name: categoryId
|
||||||
|
required: true
|
||||||
|
type: integer
|
||||||
|
- description: Offset
|
||||||
|
in: query
|
||||||
|
name: offset
|
||||||
|
type: integer
|
||||||
|
- description: Limit
|
||||||
|
in: query
|
||||||
|
name: limit
|
||||||
|
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: List courses by category
|
||||||
|
tags:
|
||||||
|
- course-management
|
||||||
|
/api/v1/course-management/courses:
|
||||||
|
post:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Legacy-compatible endpoint for creating a course
|
||||||
|
parameters:
|
||||||
|
- description: Create course payload
|
||||||
|
in: body
|
||||||
|
name: body
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/handlers.createCourseReq'
|
||||||
|
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 course
|
||||||
|
tags:
|
||||||
|
- course-management
|
||||||
|
/api/v1/course-management/courses/{courseId}:
|
||||||
|
delete:
|
||||||
|
description: Legacy-compatible endpoint for deleting a course
|
||||||
|
parameters:
|
||||||
|
- description: Course ID
|
||||||
|
in: path
|
||||||
|
name: courseId
|
||||||
|
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 course
|
||||||
|
tags:
|
||||||
|
- course-management
|
||||||
|
put:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Legacy-compatible endpoint for updating a course
|
||||||
|
parameters:
|
||||||
|
- description: Course ID
|
||||||
|
in: path
|
||||||
|
name: courseId
|
||||||
|
required: true
|
||||||
|
type: integer
|
||||||
|
- description: Update course payload
|
||||||
|
in: body
|
||||||
|
name: body
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/handlers.updateCourseReq'
|
||||||
|
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 course
|
||||||
|
tags:
|
||||||
|
- course-management
|
||||||
/api/v1/course-management/courses/{courseId}/hierarchy:
|
/api/v1/course-management/courses/{courseId}/hierarchy:
|
||||||
get:
|
get:
|
||||||
description: Returns hierarchy nodes for one course including levels/modules/sub-modules
|
description: Returns hierarchy nodes for one course including levels/modules/sub-modules
|
||||||
|
|
@ -2350,6 +2564,68 @@ paths:
|
||||||
summary: Get hierarchy for a course
|
summary: Get hierarchy for a course
|
||||||
tags:
|
tags:
|
||||||
- course-management
|
- course-management
|
||||||
|
/api/v1/course-management/courses/{courseId}/learning-path:
|
||||||
|
get:
|
||||||
|
description: Legacy-compatible endpoint for course learning path
|
||||||
|
parameters:
|
||||||
|
- description: Course ID
|
||||||
|
in: path
|
||||||
|
name: courseId
|
||||||
|
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: Get course learning path
|
||||||
|
tags:
|
||||||
|
- course-management
|
||||||
|
/api/v1/course-management/courses/{courseId}/thumbnail:
|
||||||
|
post:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Legacy-compatible endpoint for updating course thumbnail
|
||||||
|
parameters:
|
||||||
|
- description: Course ID
|
||||||
|
in: path
|
||||||
|
name: courseId
|
||||||
|
required: true
|
||||||
|
type: integer
|
||||||
|
- description: Update course thumbnail payload
|
||||||
|
in: body
|
||||||
|
name: body
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/handlers.updateCourseThumbnailReq'
|
||||||
|
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 course thumbnail
|
||||||
|
tags:
|
||||||
|
- course-management
|
||||||
/api/v1/course-management/hierarchy:
|
/api/v1/course-management/hierarchy:
|
||||||
get:
|
get:
|
||||||
description: 'Returns full hierarchy: category -> sub-category -> course'
|
description: 'Returns full hierarchy: category -> sub-category -> course'
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user