added more structure to lessons
This commit is contained in:
parent
1026354c24
commit
518c3ee751
|
|
@ -0,0 +1,18 @@
|
|||
-- Restores legacy lesson columns. Rows will have NULL question_set_id until repopulated.
|
||||
|
||||
ALTER TABLE sub_module_lessons
|
||||
ADD COLUMN IF NOT EXISTS question_set_id BIGINT REFERENCES question_sets(id) ON DELETE CASCADE,
|
||||
ADD COLUMN IF NOT EXISTS intro_video_url TEXT;
|
||||
|
||||
UPDATE sub_module_lessons
|
||||
SET intro_video_url = teaching_video_url
|
||||
WHERE teaching_video_url IS NOT NULL;
|
||||
|
||||
ALTER TABLE sub_module_lessons
|
||||
DROP COLUMN IF EXISTS title,
|
||||
DROP COLUMN IF EXISTS description,
|
||||
DROP COLUMN IF EXISTS thumbnail,
|
||||
DROP COLUMN IF EXISTS teaching_text,
|
||||
DROP COLUMN IF EXISTS teaching_image_url,
|
||||
DROP COLUMN IF EXISTS teaching_audio_url,
|
||||
DROP COLUMN IF EXISTS teaching_video_url;
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
-- Lessons are teaching content only (text, images, audio, video, thumbnail).
|
||||
-- Question sets remain linked to practices, not lessons.
|
||||
|
||||
ALTER TABLE sub_module_lessons
|
||||
ADD COLUMN IF NOT EXISTS title VARCHAR(255),
|
||||
ADD COLUMN IF NOT EXISTS description TEXT,
|
||||
ADD COLUMN IF NOT EXISTS thumbnail TEXT,
|
||||
ADD COLUMN IF NOT EXISTS teaching_text TEXT,
|
||||
ADD COLUMN IF NOT EXISTS teaching_image_url TEXT,
|
||||
ADD COLUMN IF NOT EXISTS teaching_audio_url TEXT,
|
||||
ADD COLUMN IF NOT EXISTS teaching_video_url TEXT;
|
||||
|
||||
UPDATE sub_module_lessons sml
|
||||
SET
|
||||
title = qs.title,
|
||||
description = qs.description
|
||||
FROM question_sets qs
|
||||
WHERE sml.question_set_id IS NOT NULL
|
||||
AND qs.id = sml.question_set_id;
|
||||
|
||||
UPDATE sub_module_lessons
|
||||
SET title = 'Lesson'
|
||||
WHERE title IS NULL OR trim(title) = '';
|
||||
|
||||
UPDATE sub_module_lessons
|
||||
SET teaching_video_url = intro_video_url
|
||||
WHERE intro_video_url IS NOT NULL;
|
||||
|
||||
ALTER TABLE sub_module_lessons DROP CONSTRAINT IF EXISTS sub_module_lessons_question_set_id_fkey;
|
||||
ALTER TABLE sub_module_lessons DROP CONSTRAINT IF EXISTS sub_module_lessons_question_set_id_key;
|
||||
|
||||
ALTER TABLE sub_module_lessons DROP COLUMN IF EXISTS question_set_id;
|
||||
ALTER TABLE sub_module_lessons DROP COLUMN IF EXISTS intro_video_url;
|
||||
|
||||
ALTER TABLE sub_module_lessons
|
||||
ALTER COLUMN title SET NOT NULL,
|
||||
ALTER COLUMN title SET DEFAULT 'Lesson';
|
||||
|
|
@ -83,43 +83,17 @@ WHERE sub_module_id = $1
|
|||
ORDER BY display_order ASC, id ASC;
|
||||
|
||||
-- name: GetSubModuleLessons :many
|
||||
SELECT
|
||||
smp.id,
|
||||
smp.sub_module_id,
|
||||
smp.question_set_id,
|
||||
smp.intro_video_url,
|
||||
smp.display_order,
|
||||
smp.is_active,
|
||||
qs.title,
|
||||
qs.description,
|
||||
qs.status,
|
||||
qs.set_type,
|
||||
(SELECT COUNT(*) FROM question_set_items qsi WHERE qsi.set_id = qs.id) AS question_count
|
||||
FROM sub_module_lessons smp
|
||||
JOIN question_sets qs ON qs.id = smp.question_set_id
|
||||
WHERE smp.sub_module_id = $1
|
||||
AND smp.is_active = TRUE
|
||||
AND qs.set_type = 'QUIZ'
|
||||
ORDER BY smp.display_order ASC, smp.id ASC;
|
||||
SELECT *
|
||||
FROM sub_module_lessons
|
||||
WHERE sub_module_id = $1
|
||||
AND is_active = TRUE
|
||||
ORDER BY display_order ASC, id ASC;
|
||||
|
||||
-- name: GetSubModuleLessonByID :one
|
||||
SELECT
|
||||
smp.id,
|
||||
smp.sub_module_id,
|
||||
smp.question_set_id,
|
||||
smp.intro_video_url,
|
||||
smp.display_order,
|
||||
smp.is_active,
|
||||
qs.title,
|
||||
qs.description,
|
||||
qs.status,
|
||||
qs.set_type,
|
||||
(SELECT COUNT(*) FROM question_set_items qsi WHERE qsi.set_id = qs.id) AS question_count
|
||||
FROM sub_module_lessons smp
|
||||
JOIN question_sets qs ON qs.id = smp.question_set_id
|
||||
WHERE smp.id = $1
|
||||
AND smp.is_active = TRUE
|
||||
AND qs.set_type = 'QUIZ';
|
||||
SELECT *
|
||||
FROM sub_module_lessons
|
||||
WHERE id = $1
|
||||
AND is_active = TRUE;
|
||||
|
||||
-- name: GetSubModulePractices :many
|
||||
SELECT
|
||||
|
|
@ -289,26 +263,47 @@ VALUES (
|
|||
)
|
||||
RETURNING *;
|
||||
|
||||
-- name: AttachQuestionSetLessonToSubModule :one
|
||||
-- name: CreateSubModuleLesson :one
|
||||
INSERT INTO sub_module_lessons (
|
||||
sub_module_id,
|
||||
question_set_id,
|
||||
intro_video_url,
|
||||
title,
|
||||
description,
|
||||
thumbnail,
|
||||
teaching_text,
|
||||
teaching_image_url,
|
||||
teaching_audio_url,
|
||||
teaching_video_url,
|
||||
display_order,
|
||||
is_active
|
||||
)
|
||||
VALUES ($1, $2, $3, COALESCE($4, 0), COALESCE($5, TRUE))
|
||||
VALUES (
|
||||
$1,
|
||||
$2,
|
||||
$3,
|
||||
$4,
|
||||
$5,
|
||||
$6,
|
||||
$7,
|
||||
$8,
|
||||
COALESCE($9, 0),
|
||||
COALESCE($10, TRUE)
|
||||
)
|
||||
RETURNING *;
|
||||
|
||||
-- name: UpdateSubModuleLesson :one
|
||||
UPDATE sub_module_lessons
|
||||
SET
|
||||
sub_module_id = $1,
|
||||
question_set_id = $2,
|
||||
intro_video_url = $3,
|
||||
display_order = $4,
|
||||
is_active = $5
|
||||
WHERE id = $6
|
||||
title = $2,
|
||||
description = $3,
|
||||
thumbnail = $4,
|
||||
teaching_text = $5,
|
||||
teaching_image_url = $6,
|
||||
teaching_audio_url = $7,
|
||||
teaching_video_url = $8,
|
||||
display_order = $9,
|
||||
is_active = $10
|
||||
WHERE id = $11
|
||||
RETURNING *;
|
||||
|
||||
-- name: CreateSubModulePractice :one
|
||||
|
|
|
|||
975
docs/docs.go
975
docs/docs.go
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -911,19 +911,6 @@ definitions:
|
|||
required:
|
||||
- user_id
|
||||
type: object
|
||||
handlers.attachSubModuleLessonReq:
|
||||
properties:
|
||||
display_order:
|
||||
type: integer
|
||||
intro_video_url:
|
||||
type: string
|
||||
is_active:
|
||||
type: boolean
|
||||
question_set_id:
|
||||
type: integer
|
||||
sub_module_id:
|
||||
type: integer
|
||||
type: object
|
||||
handlers.autoRenewReq:
|
||||
properties:
|
||||
auto_renew:
|
||||
|
|
@ -1133,6 +1120,29 @@ definitions:
|
|||
- set_type
|
||||
- title
|
||||
type: object
|
||||
handlers.createSubModuleLessonReq:
|
||||
properties:
|
||||
description:
|
||||
type: string
|
||||
display_order:
|
||||
type: integer
|
||||
is_active:
|
||||
type: boolean
|
||||
sub_module_id:
|
||||
type: integer
|
||||
teaching_audio_url:
|
||||
type: string
|
||||
teaching_image_url:
|
||||
type: string
|
||||
teaching_text:
|
||||
type: string
|
||||
teaching_video_url:
|
||||
type: string
|
||||
thumbnail:
|
||||
type: string
|
||||
title:
|
||||
type: string
|
||||
type: object
|
||||
handlers.createSubModulePracticeReq:
|
||||
properties:
|
||||
description:
|
||||
|
|
@ -1488,6 +1498,29 @@ definitions:
|
|||
title:
|
||||
type: string
|
||||
type: object
|
||||
handlers.updateSubModuleLessonReq:
|
||||
properties:
|
||||
description:
|
||||
type: string
|
||||
display_order:
|
||||
type: integer
|
||||
is_active:
|
||||
type: boolean
|
||||
sub_module_id:
|
||||
type: integer
|
||||
teaching_audio_url:
|
||||
type: string
|
||||
teaching_image_url:
|
||||
type: string
|
||||
teaching_text:
|
||||
type: string
|
||||
teaching_video_url:
|
||||
type: string
|
||||
thumbnail:
|
||||
type: string
|
||||
title:
|
||||
type: string
|
||||
type: object
|
||||
handlers.verifyOTPReq:
|
||||
properties:
|
||||
otp:
|
||||
|
|
@ -2447,6 +2480,31 @@ paths:
|
|||
tags:
|
||||
- course-management
|
||||
/api/v1/course-management/courses:
|
||||
get:
|
||||
description: Returns all courses with pagination
|
||||
parameters:
|
||||
- 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'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/domain.ErrorResponse'
|
||||
summary: List all courses
|
||||
tags:
|
||||
- course-management
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
|
|
@ -2503,6 +2561,36 @@ paths:
|
|||
summary: Delete course
|
||||
tags:
|
||||
- course-management
|
||||
get:
|
||||
description: Returns one course by ID
|
||||
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'
|
||||
"404":
|
||||
description: Not Found
|
||||
schema:
|
||||
$ref: '#/definitions/domain.ErrorResponse'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/domain.ErrorResponse'
|
||||
summary: Get course detail
|
||||
tags:
|
||||
- course-management
|
||||
put:
|
||||
consumes:
|
||||
- application/json
|
||||
|
|
@ -2591,6 +2679,33 @@ paths:
|
|||
summary: Get course learning path
|
||||
tags:
|
||||
- course-management
|
||||
/api/v1/course-management/courses/{courseId}/levels:
|
||||
get:
|
||||
description: Returns all active levels for one 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: List levels by course
|
||||
tags:
|
||||
- course-management
|
||||
/api/v1/course-management/courses/{courseId}/thumbnail:
|
||||
post:
|
||||
consumes:
|
||||
|
|
@ -2643,7 +2758,84 @@ paths:
|
|||
summary: Get unified course hierarchy
|
||||
tags:
|
||||
- course-management
|
||||
/api/v1/course-management/human-language/courses:
|
||||
get:
|
||||
description: Returns all courses under Human Language category
|
||||
parameters:
|
||||
- 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'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/domain.ErrorResponse'
|
||||
summary: List Human Language courses
|
||||
tags:
|
||||
- course-management
|
||||
/api/v1/course-management/human-language/sub-categories:
|
||||
get:
|
||||
description: Returns active sub-categories under Human Language category
|
||||
parameters:
|
||||
- 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'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/domain.ErrorResponse'
|
||||
summary: List Human Language sub-categories
|
||||
tags:
|
||||
- course-management
|
||||
/api/v1/course-management/levels:
|
||||
get:
|
||||
description: Returns all levels with pagination
|
||||
parameters:
|
||||
- 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'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/domain.ErrorResponse'
|
||||
summary: List all levels
|
||||
tags:
|
||||
- course-management
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
|
|
@ -2673,7 +2865,90 @@ paths:
|
|||
summary: Create level
|
||||
tags:
|
||||
- course-management
|
||||
/api/v1/course-management/levels/{levelId}:
|
||||
get:
|
||||
description: Returns one level by ID
|
||||
parameters:
|
||||
- description: Level ID
|
||||
in: path
|
||||
name: levelId
|
||||
required: true
|
||||
type: integer
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/domain.Response'
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/domain.ErrorResponse'
|
||||
"404":
|
||||
description: Not Found
|
||||
schema:
|
||||
$ref: '#/definitions/domain.ErrorResponse'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/domain.ErrorResponse'
|
||||
summary: Get level detail
|
||||
tags:
|
||||
- course-management
|
||||
/api/v1/course-management/levels/{levelId}/modules:
|
||||
get:
|
||||
description: Returns all active modules for one level
|
||||
parameters:
|
||||
- description: Level ID
|
||||
in: path
|
||||
name: levelId
|
||||
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: List modules by level
|
||||
tags:
|
||||
- course-management
|
||||
/api/v1/course-management/modules:
|
||||
get:
|
||||
description: Returns all modules with pagination
|
||||
parameters:
|
||||
- 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'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/domain.ErrorResponse'
|
||||
summary: List all modules
|
||||
tags:
|
||||
- course-management
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
|
|
@ -2703,7 +2978,123 @@ paths:
|
|||
summary: Create module
|
||||
tags:
|
||||
- course-management
|
||||
/api/v1/course-management/modules/{moduleId}:
|
||||
get:
|
||||
description: Returns one module by ID
|
||||
parameters:
|
||||
- description: Module ID
|
||||
in: path
|
||||
name: moduleId
|
||||
required: true
|
||||
type: integer
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/domain.Response'
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/domain.ErrorResponse'
|
||||
"404":
|
||||
description: Not Found
|
||||
schema:
|
||||
$ref: '#/definitions/domain.ErrorResponse'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/domain.ErrorResponse'
|
||||
summary: Get module detail
|
||||
tags:
|
||||
- course-management
|
||||
/api/v1/course-management/modules/{moduleId}/sub-modules:
|
||||
get:
|
||||
description: Returns all active sub-modules for one module
|
||||
parameters:
|
||||
- description: Module ID
|
||||
in: path
|
||||
name: moduleId
|
||||
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: List sub-modules by module
|
||||
tags:
|
||||
- course-management
|
||||
/api/v1/course-management/practices/{practiceId}:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Returns one active practice by practice ID
|
||||
parameters:
|
||||
- description: Practice ID
|
||||
in: path
|
||||
name: practiceId
|
||||
required: true
|
||||
type: integer
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/domain.Response'
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/domain.ErrorResponse'
|
||||
"404":
|
||||
description: Not Found
|
||||
schema:
|
||||
$ref: '#/definitions/domain.ErrorResponse'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/domain.ErrorResponse'
|
||||
summary: Get practice detail
|
||||
tags:
|
||||
- course-management
|
||||
/api/v1/course-management/sub-categories:
|
||||
get:
|
||||
description: Returns all active course sub-categories
|
||||
parameters:
|
||||
- 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'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/domain.ErrorResponse'
|
||||
summary: List course sub-categories
|
||||
tags:
|
||||
- course-management
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
|
|
@ -2733,18 +3124,54 @@ paths:
|
|||
summary: Create course sub-category
|
||||
tags:
|
||||
- course-management
|
||||
/api/v1/course-management/sub-categories/{subCategoryId}/courses:
|
||||
get:
|
||||
description: Returns courses for one sub-category
|
||||
parameters:
|
||||
- description: Sub-category ID
|
||||
in: path
|
||||
name: subCategoryId
|
||||
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 sub-category
|
||||
tags:
|
||||
- course-management
|
||||
/api/v1/course-management/sub-module-lessons:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Links a question set lesson to a sub-module
|
||||
description: Creates a sub-module lesson with teaching content (text, image,
|
||||
audio, video URLs) and optional thumbnail
|
||||
parameters:
|
||||
- description: Attach lesson payload
|
||||
- description: Create lesson payload
|
||||
in: body
|
||||
name: body
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/handlers.attachSubModuleLessonReq'
|
||||
$ref: '#/definitions/handlers.createSubModuleLessonReq'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
|
|
@ -2760,7 +3187,79 @@ paths:
|
|||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/domain.ErrorResponse'
|
||||
summary: Attach lesson to sub-module
|
||||
summary: Create lesson under sub-module
|
||||
tags:
|
||||
- course-management
|
||||
/api/v1/course-management/sub-module-lessons/{lessonId}:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Returns one active lesson detail by lesson ID
|
||||
parameters:
|
||||
- description: Lesson ID
|
||||
in: path
|
||||
name: lessonId
|
||||
required: true
|
||||
type: integer
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/domain.Response'
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/domain.ErrorResponse'
|
||||
"404":
|
||||
description: Not Found
|
||||
schema:
|
||||
$ref: '#/definitions/domain.ErrorResponse'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/domain.ErrorResponse'
|
||||
summary: Get lesson detail
|
||||
tags:
|
||||
- course-management
|
||||
put:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Updates lesson teaching content, thumbnail, ordering, and active
|
||||
flag
|
||||
parameters:
|
||||
- description: Lesson ID
|
||||
in: path
|
||||
name: lessonId
|
||||
required: true
|
||||
type: integer
|
||||
- description: Update lesson payload
|
||||
in: body
|
||||
name: body
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/handlers.updateSubModuleLessonReq'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/domain.Response'
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/domain.ErrorResponse'
|
||||
"404":
|
||||
description: Not Found
|
||||
schema:
|
||||
$ref: '#/definitions/domain.ErrorResponse'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/domain.ErrorResponse'
|
||||
summary: Update lesson detail
|
||||
tags:
|
||||
- course-management
|
||||
/api/v1/course-management/sub-module-practices:
|
||||
|
|
@ -2825,6 +3324,31 @@ paths:
|
|||
tags:
|
||||
- course-management
|
||||
/api/v1/course-management/sub-modules:
|
||||
get:
|
||||
description: Returns all sub-modules with pagination
|
||||
parameters:
|
||||
- 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'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/domain.ErrorResponse'
|
||||
summary: List all sub-modules
|
||||
tags:
|
||||
- course-management
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
|
|
@ -2854,6 +3378,95 @@ paths:
|
|||
summary: Create sub-module
|
||||
tags:
|
||||
- course-management
|
||||
/api/v1/course-management/sub-modules/{subModuleId}:
|
||||
get:
|
||||
description: Returns one sub-module by ID
|
||||
parameters:
|
||||
- description: Sub-module ID
|
||||
in: path
|
||||
name: subModuleId
|
||||
required: true
|
||||
type: integer
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/domain.Response'
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/domain.ErrorResponse'
|
||||
"404":
|
||||
description: Not Found
|
||||
schema:
|
||||
$ref: '#/definitions/domain.ErrorResponse'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/domain.ErrorResponse'
|
||||
summary: Get sub-module detail
|
||||
tags:
|
||||
- course-management
|
||||
/api/v1/course-management/sub-modules/{subModuleId}/lessons:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Returns all active lessons for a sub-module (teaching content metadata)
|
||||
parameters:
|
||||
- description: Sub-module ID
|
||||
in: path
|
||||
name: subModuleId
|
||||
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 lessons under sub-module
|
||||
tags:
|
||||
- course-management
|
||||
/api/v1/course-management/sub-modules/{subModuleId}/practices:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Returns all active practices attached to a sub-module
|
||||
parameters:
|
||||
- description: Sub-module ID
|
||||
in: path
|
||||
name: subModuleId
|
||||
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 practices under sub-module
|
||||
tags:
|
||||
- course-management
|
||||
/api/v1/files/audio:
|
||||
post:
|
||||
consumes:
|
||||
|
|
|
|||
|
|
@ -11,47 +11,6 @@ import (
|
|||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const AttachQuestionSetLessonToSubModule = `-- name: AttachQuestionSetLessonToSubModule :one
|
||||
INSERT INTO sub_module_lessons (
|
||||
sub_module_id,
|
||||
question_set_id,
|
||||
intro_video_url,
|
||||
display_order,
|
||||
is_active
|
||||
)
|
||||
VALUES ($1, $2, $3, COALESCE($4, 0), COALESCE($5, TRUE))
|
||||
RETURNING id, sub_module_id, question_set_id, intro_video_url, display_order, is_active, created_at
|
||||
`
|
||||
|
||||
type AttachQuestionSetLessonToSubModuleParams struct {
|
||||
SubModuleID int64 `json:"sub_module_id"`
|
||||
QuestionSetID int64 `json:"question_set_id"`
|
||||
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
||||
Column4 interface{} `json:"column_4"`
|
||||
Column5 interface{} `json:"column_5"`
|
||||
}
|
||||
|
||||
func (q *Queries) AttachQuestionSetLessonToSubModule(ctx context.Context, arg AttachQuestionSetLessonToSubModuleParams) (SubModuleLesson, error) {
|
||||
row := q.db.QueryRow(ctx, AttachQuestionSetLessonToSubModule,
|
||||
arg.SubModuleID,
|
||||
arg.QuestionSetID,
|
||||
arg.IntroVideoUrl,
|
||||
arg.Column4,
|
||||
arg.Column5,
|
||||
)
|
||||
var i SubModuleLesson
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.SubModuleID,
|
||||
&i.QuestionSetID,
|
||||
&i.IntroVideoUrl,
|
||||
&i.DisplayOrder,
|
||||
&i.IsActive,
|
||||
&i.CreatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const CreateCourseSubCategory = `-- name: CreateCourseSubCategory :one
|
||||
INSERT INTO course_sub_categories (
|
||||
category_id,
|
||||
|
|
@ -213,6 +172,78 @@ func (q *Queries) CreateSubModule(ctx context.Context, arg CreateSubModuleParams
|
|||
return i, err
|
||||
}
|
||||
|
||||
const CreateSubModuleLesson = `-- name: CreateSubModuleLesson :one
|
||||
INSERT INTO sub_module_lessons (
|
||||
sub_module_id,
|
||||
title,
|
||||
description,
|
||||
thumbnail,
|
||||
teaching_text,
|
||||
teaching_image_url,
|
||||
teaching_audio_url,
|
||||
teaching_video_url,
|
||||
display_order,
|
||||
is_active
|
||||
)
|
||||
VALUES (
|
||||
$1,
|
||||
$2,
|
||||
$3,
|
||||
$4,
|
||||
$5,
|
||||
$6,
|
||||
$7,
|
||||
$8,
|
||||
COALESCE($9, 0),
|
||||
COALESCE($10, TRUE)
|
||||
)
|
||||
RETURNING id, sub_module_id, display_order, is_active, created_at, title, description, thumbnail, teaching_text, teaching_image_url, teaching_audio_url, teaching_video_url
|
||||
`
|
||||
|
||||
type CreateSubModuleLessonParams struct {
|
||||
SubModuleID int64 `json:"sub_module_id"`
|
||||
Title string `json:"title"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||
TeachingText pgtype.Text `json:"teaching_text"`
|
||||
TeachingImageUrl pgtype.Text `json:"teaching_image_url"`
|
||||
TeachingAudioUrl pgtype.Text `json:"teaching_audio_url"`
|
||||
TeachingVideoUrl pgtype.Text `json:"teaching_video_url"`
|
||||
Column9 interface{} `json:"column_9"`
|
||||
Column10 interface{} `json:"column_10"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateSubModuleLesson(ctx context.Context, arg CreateSubModuleLessonParams) (SubModuleLesson, error) {
|
||||
row := q.db.QueryRow(ctx, CreateSubModuleLesson,
|
||||
arg.SubModuleID,
|
||||
arg.Title,
|
||||
arg.Description,
|
||||
arg.Thumbnail,
|
||||
arg.TeachingText,
|
||||
arg.TeachingImageUrl,
|
||||
arg.TeachingAudioUrl,
|
||||
arg.TeachingVideoUrl,
|
||||
arg.Column9,
|
||||
arg.Column10,
|
||||
)
|
||||
var i SubModuleLesson
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.SubModuleID,
|
||||
&i.DisplayOrder,
|
||||
&i.IsActive,
|
||||
&i.CreatedAt,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.Thumbnail,
|
||||
&i.TeachingText,
|
||||
&i.TeachingImageUrl,
|
||||
&i.TeachingAudioUrl,
|
||||
&i.TeachingVideoUrl,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const CreateSubModulePractice = `-- name: CreateSubModulePractice :one
|
||||
INSERT INTO sub_module_practices (
|
||||
sub_module_id,
|
||||
|
|
@ -907,114 +938,62 @@ func (q *Queries) GetSubModuleByID(ctx context.Context, id int64) (SubModule, er
|
|||
}
|
||||
|
||||
const GetSubModuleLessonByID = `-- name: GetSubModuleLessonByID :one
|
||||
SELECT
|
||||
smp.id,
|
||||
smp.sub_module_id,
|
||||
smp.question_set_id,
|
||||
smp.intro_video_url,
|
||||
smp.display_order,
|
||||
smp.is_active,
|
||||
qs.title,
|
||||
qs.description,
|
||||
qs.status,
|
||||
qs.set_type,
|
||||
(SELECT COUNT(*) FROM question_set_items qsi WHERE qsi.set_id = qs.id) AS question_count
|
||||
FROM sub_module_lessons smp
|
||||
JOIN question_sets qs ON qs.id = smp.question_set_id
|
||||
WHERE smp.id = $1
|
||||
AND smp.is_active = TRUE
|
||||
AND qs.set_type = 'QUIZ'
|
||||
SELECT id, sub_module_id, display_order, is_active, created_at, title, description, thumbnail, teaching_text, teaching_image_url, teaching_audio_url, teaching_video_url
|
||||
FROM sub_module_lessons
|
||||
WHERE id = $1
|
||||
AND is_active = TRUE
|
||||
`
|
||||
|
||||
type GetSubModuleLessonByIDRow struct {
|
||||
ID int64 `json:"id"`
|
||||
SubModuleID int64 `json:"sub_module_id"`
|
||||
QuestionSetID int64 `json:"question_set_id"`
|
||||
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
||||
DisplayOrder int32 `json:"display_order"`
|
||||
IsActive bool `json:"is_active"`
|
||||
Title string `json:"title"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
Status string `json:"status"`
|
||||
SetType string `json:"set_type"`
|
||||
QuestionCount int64 `json:"question_count"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetSubModuleLessonByID(ctx context.Context, id int64) (GetSubModuleLessonByIDRow, error) {
|
||||
func (q *Queries) GetSubModuleLessonByID(ctx context.Context, id int64) (SubModuleLesson, error) {
|
||||
row := q.db.QueryRow(ctx, GetSubModuleLessonByID, id)
|
||||
var i GetSubModuleLessonByIDRow
|
||||
var i SubModuleLesson
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.SubModuleID,
|
||||
&i.QuestionSetID,
|
||||
&i.IntroVideoUrl,
|
||||
&i.DisplayOrder,
|
||||
&i.IsActive,
|
||||
&i.CreatedAt,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.Status,
|
||||
&i.SetType,
|
||||
&i.QuestionCount,
|
||||
&i.Thumbnail,
|
||||
&i.TeachingText,
|
||||
&i.TeachingImageUrl,
|
||||
&i.TeachingAudioUrl,
|
||||
&i.TeachingVideoUrl,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const GetSubModuleLessons = `-- name: GetSubModuleLessons :many
|
||||
SELECT
|
||||
smp.id,
|
||||
smp.sub_module_id,
|
||||
smp.question_set_id,
|
||||
smp.intro_video_url,
|
||||
smp.display_order,
|
||||
smp.is_active,
|
||||
qs.title,
|
||||
qs.description,
|
||||
qs.status,
|
||||
qs.set_type,
|
||||
(SELECT COUNT(*) FROM question_set_items qsi WHERE qsi.set_id = qs.id) AS question_count
|
||||
FROM sub_module_lessons smp
|
||||
JOIN question_sets qs ON qs.id = smp.question_set_id
|
||||
WHERE smp.sub_module_id = $1
|
||||
AND smp.is_active = TRUE
|
||||
AND qs.set_type = 'QUIZ'
|
||||
ORDER BY smp.display_order ASC, smp.id ASC
|
||||
SELECT id, sub_module_id, display_order, is_active, created_at, title, description, thumbnail, teaching_text, teaching_image_url, teaching_audio_url, teaching_video_url
|
||||
FROM sub_module_lessons
|
||||
WHERE sub_module_id = $1
|
||||
AND is_active = TRUE
|
||||
ORDER BY display_order ASC, id ASC
|
||||
`
|
||||
|
||||
type GetSubModuleLessonsRow struct {
|
||||
ID int64 `json:"id"`
|
||||
SubModuleID int64 `json:"sub_module_id"`
|
||||
QuestionSetID int64 `json:"question_set_id"`
|
||||
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
||||
DisplayOrder int32 `json:"display_order"`
|
||||
IsActive bool `json:"is_active"`
|
||||
Title string `json:"title"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
Status string `json:"status"`
|
||||
SetType string `json:"set_type"`
|
||||
QuestionCount int64 `json:"question_count"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetSubModuleLessons(ctx context.Context, subModuleID int64) ([]GetSubModuleLessonsRow, error) {
|
||||
func (q *Queries) GetSubModuleLessons(ctx context.Context, subModuleID int64) ([]SubModuleLesson, error) {
|
||||
rows, err := q.db.Query(ctx, GetSubModuleLessons, subModuleID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetSubModuleLessonsRow
|
||||
var items []SubModuleLesson
|
||||
for rows.Next() {
|
||||
var i GetSubModuleLessonsRow
|
||||
var i SubModuleLesson
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.SubModuleID,
|
||||
&i.QuestionSetID,
|
||||
&i.IntroVideoUrl,
|
||||
&i.DisplayOrder,
|
||||
&i.IsActive,
|
||||
&i.CreatedAt,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.Status,
|
||||
&i.SetType,
|
||||
&i.QuestionCount,
|
||||
&i.Thumbnail,
|
||||
&i.TeachingText,
|
||||
&i.TeachingImageUrl,
|
||||
&i.TeachingAudioUrl,
|
||||
&i.TeachingVideoUrl,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -1242,28 +1221,43 @@ const UpdateSubModuleLesson = `-- name: UpdateSubModuleLesson :one
|
|||
UPDATE sub_module_lessons
|
||||
SET
|
||||
sub_module_id = $1,
|
||||
question_set_id = $2,
|
||||
intro_video_url = $3,
|
||||
display_order = $4,
|
||||
is_active = $5
|
||||
WHERE id = $6
|
||||
RETURNING id, sub_module_id, question_set_id, intro_video_url, display_order, is_active, created_at
|
||||
title = $2,
|
||||
description = $3,
|
||||
thumbnail = $4,
|
||||
teaching_text = $5,
|
||||
teaching_image_url = $6,
|
||||
teaching_audio_url = $7,
|
||||
teaching_video_url = $8,
|
||||
display_order = $9,
|
||||
is_active = $10
|
||||
WHERE id = $11
|
||||
RETURNING id, sub_module_id, display_order, is_active, created_at, title, description, thumbnail, teaching_text, teaching_image_url, teaching_audio_url, teaching_video_url
|
||||
`
|
||||
|
||||
type UpdateSubModuleLessonParams struct {
|
||||
SubModuleID int64 `json:"sub_module_id"`
|
||||
QuestionSetID int64 `json:"question_set_id"`
|
||||
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
||||
DisplayOrder int32 `json:"display_order"`
|
||||
IsActive bool `json:"is_active"`
|
||||
ID int64 `json:"id"`
|
||||
SubModuleID int64 `json:"sub_module_id"`
|
||||
Title string `json:"title"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||
TeachingText pgtype.Text `json:"teaching_text"`
|
||||
TeachingImageUrl pgtype.Text `json:"teaching_image_url"`
|
||||
TeachingAudioUrl pgtype.Text `json:"teaching_audio_url"`
|
||||
TeachingVideoUrl pgtype.Text `json:"teaching_video_url"`
|
||||
DisplayOrder int32 `json:"display_order"`
|
||||
IsActive bool `json:"is_active"`
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateSubModuleLesson(ctx context.Context, arg UpdateSubModuleLessonParams) (SubModuleLesson, error) {
|
||||
row := q.db.QueryRow(ctx, UpdateSubModuleLesson,
|
||||
arg.SubModuleID,
|
||||
arg.QuestionSetID,
|
||||
arg.IntroVideoUrl,
|
||||
arg.Title,
|
||||
arg.Description,
|
||||
arg.Thumbnail,
|
||||
arg.TeachingText,
|
||||
arg.TeachingImageUrl,
|
||||
arg.TeachingAudioUrl,
|
||||
arg.TeachingVideoUrl,
|
||||
arg.DisplayOrder,
|
||||
arg.IsActive,
|
||||
arg.ID,
|
||||
|
|
@ -1272,11 +1266,16 @@ func (q *Queries) UpdateSubModuleLesson(ctx context.Context, arg UpdateSubModule
|
|||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.SubModuleID,
|
||||
&i.QuestionSetID,
|
||||
&i.IntroVideoUrl,
|
||||
&i.DisplayOrder,
|
||||
&i.IsActive,
|
||||
&i.CreatedAt,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.Thumbnail,
|
||||
&i.TeachingText,
|
||||
&i.TeachingImageUrl,
|
||||
&i.TeachingAudioUrl,
|
||||
&i.TeachingVideoUrl,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -356,13 +356,18 @@ type SubModule struct {
|
|||
}
|
||||
|
||||
type SubModuleLesson struct {
|
||||
ID int64 `json:"id"`
|
||||
SubModuleID int64 `json:"sub_module_id"`
|
||||
QuestionSetID int64 `json:"question_set_id"`
|
||||
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
||||
DisplayOrder int32 `json:"display_order"`
|
||||
IsActive bool `json:"is_active"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
ID int64 `json:"id"`
|
||||
SubModuleID int64 `json:"sub_module_id"`
|
||||
DisplayOrder int32 `json:"display_order"`
|
||||
IsActive bool `json:"is_active"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
Title string `json:"title"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||
TeachingText pgtype.Text `json:"teaching_text"`
|
||||
TeachingImageUrl pgtype.Text `json:"teaching_image_url"`
|
||||
TeachingAudioUrl pgtype.Text `json:"teaching_audio_url"`
|
||||
TeachingVideoUrl pgtype.Text `json:"teaching_video_url"`
|
||||
}
|
||||
|
||||
type SubModulePractice struct {
|
||||
|
|
|
|||
|
|
@ -101,35 +101,30 @@ type updatePracticeReq struct {
|
|||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
type attachSubModuleLessonReq struct {
|
||||
SubModuleID int64 `json:"sub_module_id"`
|
||||
QuestionSetID int64 `json:"question_set_id"`
|
||||
IntroVideoURL *string `json:"intro_video_url"`
|
||||
DisplayOrder *int32 `json:"display_order"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
type updateLessonQuestionReq struct {
|
||||
QuestionID int64 `json:"question_id"`
|
||||
DisplayOrder *int32 `json:"display_order"`
|
||||
type createSubModuleLessonReq struct {
|
||||
SubModuleID int64 `json:"sub_module_id"`
|
||||
Title string `json:"title"`
|
||||
Description *string `json:"description"`
|
||||
Thumbnail *string `json:"thumbnail"`
|
||||
TeachingText *string `json:"teaching_text"`
|
||||
TeachingImageURL *string `json:"teaching_image_url"`
|
||||
TeachingAudioURL *string `json:"teaching_audio_url"`
|
||||
TeachingVideoURL *string `json:"teaching_video_url"`
|
||||
DisplayOrder *int32 `json:"display_order"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
type updateSubModuleLessonReq struct {
|
||||
SubModuleID *int64 `json:"sub_module_id"`
|
||||
QuestionSetID *int64 `json:"question_set_id"`
|
||||
IntroVideoURL *string `json:"intro_video_url"`
|
||||
DisplayOrder *int32 `json:"display_order"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
Title *string `json:"title"`
|
||||
Description *string `json:"description"`
|
||||
BannerImage *string `json:"banner_image"`
|
||||
Persona *string `json:"persona"`
|
||||
TimeLimitMinutes *int32 `json:"time_limit_minutes"`
|
||||
PassingScore *int32 `json:"passing_score"`
|
||||
ShuffleQuestions *bool `json:"shuffle_questions"`
|
||||
Status *string `json:"status"`
|
||||
SubCourseVideoID *int64 `json:"sub_course_video_id"`
|
||||
Questions []updateLessonQuestionReq `json:"questions"`
|
||||
SubModuleID *int64 `json:"sub_module_id"`
|
||||
Title *string `json:"title"`
|
||||
Description *string `json:"description"`
|
||||
Thumbnail *string `json:"thumbnail"`
|
||||
TeachingText *string `json:"teaching_text"`
|
||||
TeachingImageURL *string `json:"teaching_image_url"`
|
||||
TeachingAudioURL *string `json:"teaching_audio_url"`
|
||||
TeachingVideoURL *string `json:"teaching_video_url"`
|
||||
DisplayOrder *int32 `json:"display_order"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
type createSubModulePracticeReq struct {
|
||||
|
|
@ -159,6 +154,16 @@ func toText(v *string) pgtype.Text {
|
|||
return pgtype.Text{String: *v, Valid: true}
|
||||
}
|
||||
|
||||
func mergeTextField(current pgtype.Text, req *string) pgtype.Text {
|
||||
if req == nil {
|
||||
return current
|
||||
}
|
||||
if *req == "" {
|
||||
return pgtype.Text{Valid: false}
|
||||
}
|
||||
return pgtype.Text{String: *req, Valid: true}
|
||||
}
|
||||
|
||||
func toInt4(v *int32) pgtype.Int4 {
|
||||
if v == nil {
|
||||
return pgtype.Int4{Valid: false}
|
||||
|
|
@ -1515,41 +1520,46 @@ func (h *Handler) CreateSubModuleVideo(c *fiber.Ctx) error {
|
|||
return c.Status(fiber.StatusCreated).JSON(domain.Response{Message: "Sub-module video created", Data: created})
|
||||
}
|
||||
|
||||
// AttachSubModuleLesson godoc
|
||||
// @Summary Attach lesson to sub-module
|
||||
// @Description Links a question set lesson to a sub-module
|
||||
// CreateSubModuleLesson godoc
|
||||
// @Summary Create lesson under sub-module
|
||||
// @Description Creates a sub-module lesson with teaching content (text, image, audio, video URLs) and optional thumbnail
|
||||
// @Tags course-management
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body attachSubModuleLessonReq true "Attach lesson payload"
|
||||
// @Param body body createSubModuleLessonReq true "Create lesson payload"
|
||||
// @Success 201 {object} domain.Response
|
||||
// @Failure 400 {object} domain.ErrorResponse
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/course-management/sub-module-lessons [post]
|
||||
func (h *Handler) AttachSubModuleLesson(c *fiber.Ctx) error {
|
||||
var req attachSubModuleLessonReq
|
||||
func (h *Handler) CreateSubModuleLesson(c *fiber.Ctx) error {
|
||||
var req createSubModuleLessonReq
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()})
|
||||
}
|
||||
if req.SubModuleID <= 0 || req.QuestionSetID <= 0 {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "sub_module_id and question_set_id are required"})
|
||||
if req.SubModuleID <= 0 || strings.TrimSpace(req.Title) == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "sub_module_id and title are required"})
|
||||
}
|
||||
attached, err := h.analyticsDB.AttachQuestionSetLessonToSubModule(c.Context(), dbgen.AttachQuestionSetLessonToSubModuleParams{
|
||||
SubModuleID: req.SubModuleID,
|
||||
QuestionSetID: req.QuestionSetID,
|
||||
IntroVideoUrl: toText(req.IntroVideoURL),
|
||||
Column4: intOrNil(req.DisplayOrder),
|
||||
Column5: boolOrNil(req.IsActive),
|
||||
created, err := h.analyticsDB.CreateSubModuleLesson(c.Context(), dbgen.CreateSubModuleLessonParams{
|
||||
SubModuleID: req.SubModuleID,
|
||||
Title: strings.TrimSpace(req.Title),
|
||||
Description: toText(req.Description),
|
||||
Thumbnail: toText(req.Thumbnail),
|
||||
TeachingText: toText(req.TeachingText),
|
||||
TeachingImageUrl: toText(req.TeachingImageURL),
|
||||
TeachingAudioUrl: toText(req.TeachingAudioURL),
|
||||
TeachingVideoUrl: toText(req.TeachingVideoURL),
|
||||
Column9: intOrNil(req.DisplayOrder),
|
||||
Column10: boolOrNil(req.IsActive),
|
||||
})
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to attach lesson", Error: err.Error()})
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to create lesson", Error: err.Error()})
|
||||
}
|
||||
return c.Status(fiber.StatusCreated).JSON(domain.Response{Message: "Lesson attached to sub-module", Data: attached})
|
||||
return c.Status(fiber.StatusCreated).JSON(domain.Response{Message: "Lesson created", Data: created})
|
||||
}
|
||||
|
||||
// GetSubModuleLessons godoc
|
||||
// @Summary Get lessons under sub-module
|
||||
// @Description Returns all active lessons attached to a sub-module with question-set details
|
||||
// @Description Returns all active lessons for a sub-module (teaching content metadata)
|
||||
// @Tags course-management
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
|
|
@ -1618,7 +1628,7 @@ func (h *Handler) GetSubModuleLessonByID(c *fiber.Ctx) error {
|
|||
|
||||
// UpdateSubModuleLesson godoc
|
||||
// @Summary Update lesson detail
|
||||
// @Description Updates lesson metadata, linked question-set metadata, and optionally replaces lesson questions
|
||||
// @Description Updates lesson teaching content, thumbnail, ordering, and active flag
|
||||
// @Tags course-management
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
|
|
@ -1662,18 +1672,21 @@ func (h *Handler) UpdateSubModuleLesson(c *fiber.Ctx) error {
|
|||
targetSubModuleID = *req.SubModuleID
|
||||
}
|
||||
|
||||
targetQuestionSetID := currentLesson.QuestionSetID
|
||||
if req.QuestionSetID != nil {
|
||||
if *req.QuestionSetID <= 0 {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "question_set_id must be a positive integer"})
|
||||
targetTitle := currentLesson.Title
|
||||
if req.Title != nil {
|
||||
t := strings.TrimSpace(*req.Title)
|
||||
if t == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "title cannot be empty"})
|
||||
}
|
||||
targetQuestionSetID = *req.QuestionSetID
|
||||
targetTitle = t
|
||||
}
|
||||
|
||||
targetIntroVideoURL := currentLesson.IntroVideoUrl
|
||||
if req.IntroVideoURL != nil {
|
||||
targetIntroVideoURL = toText(req.IntroVideoURL)
|
||||
}
|
||||
targetDescription := mergeTextField(currentLesson.Description, req.Description)
|
||||
targetThumbnail := mergeTextField(currentLesson.Thumbnail, req.Thumbnail)
|
||||
targetTeachingText := mergeTextField(currentLesson.TeachingText, req.TeachingText)
|
||||
targetTeachingImage := mergeTextField(currentLesson.TeachingImageUrl, req.TeachingImageURL)
|
||||
targetTeachingAudio := mergeTextField(currentLesson.TeachingAudioUrl, req.TeachingAudioURL)
|
||||
targetTeachingVideo := mergeTextField(currentLesson.TeachingVideoUrl, req.TeachingVideoURL)
|
||||
|
||||
targetDisplayOrder := currentLesson.DisplayOrder
|
||||
if req.DisplayOrder != nil {
|
||||
|
|
@ -1686,12 +1699,17 @@ func (h *Handler) UpdateSubModuleLesson(c *fiber.Ctx) error {
|
|||
}
|
||||
|
||||
if _, err := h.analyticsDB.UpdateSubModuleLesson(c.Context(), dbgen.UpdateSubModuleLessonParams{
|
||||
SubModuleID: targetSubModuleID,
|
||||
QuestionSetID: targetQuestionSetID,
|
||||
IntroVideoUrl: targetIntroVideoURL,
|
||||
DisplayOrder: targetDisplayOrder,
|
||||
IsActive: targetIsActive,
|
||||
ID: lessonID,
|
||||
SubModuleID: targetSubModuleID,
|
||||
Title: targetTitle,
|
||||
Description: targetDescription,
|
||||
Thumbnail: targetThumbnail,
|
||||
TeachingText: targetTeachingText,
|
||||
TeachingImageUrl: targetTeachingImage,
|
||||
TeachingAudioUrl: targetTeachingAudio,
|
||||
TeachingVideoUrl: targetTeachingVideo,
|
||||
DisplayOrder: targetDisplayOrder,
|
||||
IsActive: targetIsActive,
|
||||
ID: lessonID,
|
||||
}); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to update lesson",
|
||||
|
|
@ -1699,125 +1717,6 @@ func (h *Handler) UpdateSubModuleLesson(c *fiber.Ctx) error {
|
|||
})
|
||||
}
|
||||
|
||||
currentSet, err := h.questionsSvc.GetQuestionSetByID(c.Context(), targetQuestionSetID)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to load linked question set",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
shouldUpdateSet := req.Title != nil || req.Description != nil || req.BannerImage != nil ||
|
||||
req.Persona != nil || req.TimeLimitMinutes != nil || req.PassingScore != nil ||
|
||||
req.ShuffleQuestions != nil || req.Status != nil || req.SubCourseVideoID != nil
|
||||
|
||||
if shouldUpdateSet {
|
||||
title := currentSet.Title
|
||||
if req.Title != nil {
|
||||
title = *req.Title
|
||||
}
|
||||
|
||||
input := domain.CreateQuestionSetInput{
|
||||
Title: title,
|
||||
Description: currentSet.Description,
|
||||
BannerImage: currentSet.BannerImage,
|
||||
Persona: currentSet.Persona,
|
||||
TimeLimitMinutes: currentSet.TimeLimitMinutes,
|
||||
PassingScore: currentSet.PassingScore,
|
||||
SubCourseVideoID: currentSet.SubCourseVideoID,
|
||||
IntroVideoURL: req.IntroVideoURL,
|
||||
ShuffleQuestions: ¤tSet.ShuffleQuestions,
|
||||
}
|
||||
|
||||
currentStatus := currentSet.Status
|
||||
input.Status = ¤tStatus
|
||||
|
||||
if req.Description != nil {
|
||||
input.Description = req.Description
|
||||
}
|
||||
if req.BannerImage != nil {
|
||||
input.BannerImage = req.BannerImage
|
||||
}
|
||||
if req.Persona != nil {
|
||||
input.Persona = req.Persona
|
||||
}
|
||||
if req.TimeLimitMinutes != nil {
|
||||
input.TimeLimitMinutes = req.TimeLimitMinutes
|
||||
}
|
||||
if req.PassingScore != nil {
|
||||
input.PassingScore = req.PassingScore
|
||||
}
|
||||
if req.ShuffleQuestions != nil {
|
||||
input.ShuffleQuestions = req.ShuffleQuestions
|
||||
}
|
||||
if req.Status != nil {
|
||||
input.Status = req.Status
|
||||
}
|
||||
if req.SubCourseVideoID != nil {
|
||||
input.SubCourseVideoID = req.SubCourseVideoID
|
||||
}
|
||||
|
||||
if err := h.questionsSvc.UpdateQuestionSet(c.Context(), targetQuestionSetID, input); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to update linked question set",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if req.Questions != nil {
|
||||
seen := make(map[int64]struct{}, len(req.Questions))
|
||||
for idx, q := range req.Questions {
|
||||
if q.QuestionID <= 0 {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Each question_id must be a positive integer"})
|
||||
}
|
||||
if _, exists := seen[q.QuestionID]; exists {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Duplicate question_id values are not allowed"})
|
||||
}
|
||||
seen[q.QuestionID] = struct{}{}
|
||||
|
||||
order := q.DisplayOrder
|
||||
if order == nil {
|
||||
defaultOrder := int32(idx)
|
||||
order = &defaultOrder
|
||||
}
|
||||
|
||||
if _, err := h.questionsSvc.GetQuestionByID(c.Context(), q.QuestionID); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid question_id in questions payload",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
if _, err := h.questionsSvc.AddQuestionToSet(c.Context(), targetQuestionSetID, q.QuestionID, order); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to upsert lesson question",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
existingItems, err := h.questionsSvc.GetQuestionSetItems(c.Context(), targetQuestionSetID)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to load existing lesson questions",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
for _, item := range existingItems {
|
||||
if _, keep := seen[item.QuestionID]; keep {
|
||||
continue
|
||||
}
|
||||
if err := h.questionsSvc.RemoveQuestionFromSet(c.Context(), targetQuestionSetID, item.QuestionID); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to remove question from lesson",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updatedLesson, err := h.analyticsDB.GetSubModuleLessonByID(c.Context(), lessonID)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
|
|
@ -1826,20 +1725,9 @@ func (h *Handler) UpdateSubModuleLesson(c *fiber.Ctx) error {
|
|||
})
|
||||
}
|
||||
|
||||
updatedQuestions, err := h.questionsSvc.GetQuestionSetItems(c.Context(), updatedLesson.QuestionSetID)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Lesson updated but failed to fetch latest questions",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).JSON(domain.Response{
|
||||
Message: "Lesson updated successfully",
|
||||
Data: map[string]interface{}{
|
||||
"lesson": updatedLesson,
|
||||
"questions": updatedQuestions,
|
||||
},
|
||||
Data: updatedLesson,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -116,10 +116,10 @@ func (a *App) initAppRoutes() {
|
|||
groupV1.Post("/course-management/sub-module-videos", a.authMiddleware, a.RequirePermission("videos.create"), h.CreateSubModuleVideo)
|
||||
groupV1.Put("/course-management/sub-module-videos/:videoId", a.authMiddleware, a.RequirePermission("videos.update"), h.UpdateSubModuleVideo)
|
||||
groupV1.Delete("/course-management/sub-module-videos/:videoId", a.authMiddleware, a.RequirePermission("videos.delete"), h.DeleteSubModuleVideo)
|
||||
groupV1.Get("/course-management/sub-modules/:subModuleId/lessons", a.authMiddleware, a.RequirePermission("question_sets.list"), h.GetSubModuleLessons)
|
||||
groupV1.Get("/course-management/sub-module-lessons/:lessonId", a.authMiddleware, a.RequirePermission("question_sets.get"), h.GetSubModuleLessonByID)
|
||||
groupV1.Put("/course-management/sub-module-lessons/:lessonId", a.authMiddleware, a.RequirePermission("question_sets.update"), h.UpdateSubModuleLesson)
|
||||
groupV1.Post("/course-management/sub-module-lessons", a.authMiddleware, a.RequirePermission("question_sets.update"), h.AttachSubModuleLesson)
|
||||
groupV1.Get("/course-management/sub-modules/:subModuleId/lessons", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.GetSubModuleLessons)
|
||||
groupV1.Get("/course-management/sub-module-lessons/:lessonId", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.GetSubModuleLessonByID)
|
||||
groupV1.Put("/course-management/sub-module-lessons/:lessonId", a.authMiddleware, a.RequirePermission("subcourses.update"), h.UpdateSubModuleLesson)
|
||||
groupV1.Post("/course-management/sub-module-lessons", a.authMiddleware, a.RequirePermission("subcourses.create"), h.CreateSubModuleLesson)
|
||||
groupV1.Get("/course-management/sub-modules/:subModuleId/practices", a.authMiddleware, a.RequirePermission("question_sets.list"), h.GetSubModulePractices)
|
||||
groupV1.Get("/course-management/practices/:practiceId", a.authMiddleware, a.RequirePermission("question_sets.get"), h.GetSubModulePracticeByID)
|
||||
groupV1.Post("/course-management/sub-module-practices", a.authMiddleware, a.RequirePermission("question_sets.update"), h.CreateSubModulePractice)
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user