Expand course hierarchy read APIs and practice retrieval.
Add list/detail endpoints for courses, levels, modules, submodules, and submodule practices; extend course listing queries; add lesson update support and clean up removed route paths. Made-with: Cursor
This commit is contained in:
parent
343ce470cc
commit
1026354c24
|
|
@ -33,6 +33,57 @@ ORDER BY display_order ASC, id ASC
|
||||||
LIMIT sqlc.narg('limit')::INT
|
LIMIT sqlc.narg('limit')::INT
|
||||||
OFFSET sqlc.narg('offset')::INT;
|
OFFSET sqlc.narg('offset')::INT;
|
||||||
|
|
||||||
|
-- name: GetAllCourses :many
|
||||||
|
SELECT
|
||||||
|
COUNT(*) OVER () AS total_count,
|
||||||
|
c.id,
|
||||||
|
c.category_id,
|
||||||
|
c.sub_category_id,
|
||||||
|
c.title,
|
||||||
|
c.description,
|
||||||
|
c.thumbnail,
|
||||||
|
c.intro_video_url,
|
||||||
|
c.is_active
|
||||||
|
FROM courses c
|
||||||
|
ORDER BY c.display_order ASC, c.id ASC
|
||||||
|
LIMIT sqlc.narg('limit')::INT
|
||||||
|
OFFSET sqlc.narg('offset')::INT;
|
||||||
|
|
||||||
|
-- name: GetHumanLanguageCourses :many
|
||||||
|
SELECT
|
||||||
|
COUNT(*) OVER () AS total_count,
|
||||||
|
c.id,
|
||||||
|
c.category_id,
|
||||||
|
c.sub_category_id,
|
||||||
|
c.title,
|
||||||
|
c.description,
|
||||||
|
c.thumbnail,
|
||||||
|
c.intro_video_url,
|
||||||
|
c.is_active
|
||||||
|
FROM courses c
|
||||||
|
JOIN course_categories cc ON cc.id = c.category_id
|
||||||
|
WHERE lower(trim(cc.name)) = 'human language'
|
||||||
|
ORDER BY c.display_order ASC, c.id ASC
|
||||||
|
LIMIT sqlc.narg('limit')::INT
|
||||||
|
OFFSET sqlc.narg('offset')::INT;
|
||||||
|
|
||||||
|
-- name: GetCoursesBySubCategory :many
|
||||||
|
SELECT
|
||||||
|
COUNT(*) OVER () AS total_count,
|
||||||
|
c.id,
|
||||||
|
c.category_id,
|
||||||
|
c.sub_category_id,
|
||||||
|
c.title,
|
||||||
|
c.description,
|
||||||
|
c.thumbnail,
|
||||||
|
c.intro_video_url,
|
||||||
|
c.is_active
|
||||||
|
FROM courses c
|
||||||
|
WHERE c.sub_category_id = $1
|
||||||
|
ORDER BY c.display_order ASC, c.id ASC
|
||||||
|
LIMIT sqlc.narg('limit')::INT
|
||||||
|
OFFSET sqlc.narg('offset')::INT;
|
||||||
|
|
||||||
|
|
||||||
-- name: UpdateCourse :exec
|
-- name: UpdateCourse :exec
|
||||||
UPDATE courses
|
UPDATE courses
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,20 @@ WHERE course_id = $1
|
||||||
AND is_active = TRUE
|
AND is_active = TRUE
|
||||||
ORDER BY display_order ASC, id ASC;
|
ORDER BY display_order ASC, id ASC;
|
||||||
|
|
||||||
|
-- name: GetAllLevels :many
|
||||||
|
SELECT
|
||||||
|
COUNT(*) OVER () AS total_count,
|
||||||
|
l.*
|
||||||
|
FROM levels l
|
||||||
|
ORDER BY l.display_order ASC, l.id ASC
|
||||||
|
LIMIT sqlc.narg('limit')::INT
|
||||||
|
OFFSET sqlc.narg('offset')::INT;
|
||||||
|
|
||||||
|
-- name: GetLevelByID :one
|
||||||
|
SELECT *
|
||||||
|
FROM levels
|
||||||
|
WHERE id = $1;
|
||||||
|
|
||||||
-- name: GetModulesByLevelID :many
|
-- name: GetModulesByLevelID :many
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM modules
|
FROM modules
|
||||||
|
|
@ -26,6 +40,20 @@ WHERE level_id = $1
|
||||||
AND is_active = TRUE
|
AND is_active = TRUE
|
||||||
ORDER BY display_order ASC, id ASC;
|
ORDER BY display_order ASC, id ASC;
|
||||||
|
|
||||||
|
-- name: GetAllModules :many
|
||||||
|
SELECT
|
||||||
|
COUNT(*) OVER () AS total_count,
|
||||||
|
m.*
|
||||||
|
FROM modules m
|
||||||
|
ORDER BY m.display_order ASC, m.id ASC
|
||||||
|
LIMIT sqlc.narg('limit')::INT
|
||||||
|
OFFSET sqlc.narg('offset')::INT;
|
||||||
|
|
||||||
|
-- name: GetModuleByID :one
|
||||||
|
SELECT *
|
||||||
|
FROM modules
|
||||||
|
WHERE id = $1;
|
||||||
|
|
||||||
-- name: GetSubModulesByModuleID :many
|
-- name: GetSubModulesByModuleID :many
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM sub_modules
|
FROM sub_modules
|
||||||
|
|
@ -33,6 +61,20 @@ WHERE module_id = $1
|
||||||
AND is_active = TRUE
|
AND is_active = TRUE
|
||||||
ORDER BY display_order ASC, id ASC;
|
ORDER BY display_order ASC, id ASC;
|
||||||
|
|
||||||
|
-- name: GetAllSubModules :many
|
||||||
|
SELECT
|
||||||
|
COUNT(*) OVER () AS total_count,
|
||||||
|
sm.*
|
||||||
|
FROM sub_modules sm
|
||||||
|
ORDER BY sm.display_order ASC, sm.id ASC
|
||||||
|
LIMIT sqlc.narg('limit')::INT
|
||||||
|
OFFSET sqlc.narg('offset')::INT;
|
||||||
|
|
||||||
|
-- name: GetSubModuleByID :one
|
||||||
|
SELECT *
|
||||||
|
FROM sub_modules
|
||||||
|
WHERE id = $1;
|
||||||
|
|
||||||
-- name: GetSubModuleVideos :many
|
-- name: GetSubModuleVideos :many
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM sub_module_videos
|
FROM sub_module_videos
|
||||||
|
|
@ -100,6 +142,26 @@ WHERE smp.sub_module_id = $1
|
||||||
AND qs.set_type = 'PRACTICE'
|
AND qs.set_type = 'PRACTICE'
|
||||||
ORDER BY smp.display_order ASC, smp.id ASC;
|
ORDER BY smp.display_order ASC, smp.id ASC;
|
||||||
|
|
||||||
|
-- name: GetSubModulePracticeByID :one
|
||||||
|
SELECT
|
||||||
|
smp.id,
|
||||||
|
smp.sub_module_id,
|
||||||
|
smp.title,
|
||||||
|
smp.description,
|
||||||
|
smp.thumbnail,
|
||||||
|
smp.intro_video_url,
|
||||||
|
smp.question_set_id,
|
||||||
|
smp.display_order,
|
||||||
|
smp.is_active,
|
||||||
|
qs.status,
|
||||||
|
qs.set_type,
|
||||||
|
(SELECT COUNT(*) FROM question_set_items qsi WHERE qsi.set_id = qs.id) AS question_count
|
||||||
|
FROM sub_module_practices 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 = 'PRACTICE';
|
||||||
|
|
||||||
-- name: GetFullHierarchyByCourseID :many
|
-- name: GetFullHierarchyByCourseID :many
|
||||||
SELECT
|
SELECT
|
||||||
c.id AS course_id,
|
c.id AS course_id,
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,70 @@ func (q *Queries) DeleteCourse(ctx context.Context, id int64) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const GetAllCourses = `-- name: GetAllCourses :many
|
||||||
|
SELECT
|
||||||
|
COUNT(*) OVER () AS total_count,
|
||||||
|
c.id,
|
||||||
|
c.category_id,
|
||||||
|
c.sub_category_id,
|
||||||
|
c.title,
|
||||||
|
c.description,
|
||||||
|
c.thumbnail,
|
||||||
|
c.intro_video_url,
|
||||||
|
c.is_active
|
||||||
|
FROM courses c
|
||||||
|
ORDER BY c.display_order ASC, c.id ASC
|
||||||
|
LIMIT $2::INT
|
||||||
|
OFFSET $1::INT
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetAllCoursesParams struct {
|
||||||
|
Offset pgtype.Int4 `json:"offset"`
|
||||||
|
Limit pgtype.Int4 `json:"limit"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetAllCoursesRow struct {
|
||||||
|
TotalCount int64 `json:"total_count"`
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
CategoryID int64 `json:"category_id"`
|
||||||
|
SubCategoryID pgtype.Int8 `json:"sub_category_id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description pgtype.Text `json:"description"`
|
||||||
|
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||||
|
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
||||||
|
IsActive bool `json:"is_active"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetAllCourses(ctx context.Context, arg GetAllCoursesParams) ([]GetAllCoursesRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, GetAllCourses, arg.Offset, arg.Limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []GetAllCoursesRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i GetAllCoursesRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.TotalCount,
|
||||||
|
&i.ID,
|
||||||
|
&i.CategoryID,
|
||||||
|
&i.SubCategoryID,
|
||||||
|
&i.Title,
|
||||||
|
&i.Description,
|
||||||
|
&i.Thumbnail,
|
||||||
|
&i.IntroVideoUrl,
|
||||||
|
&i.IsActive,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
const GetCourseByID = `-- name: GetCourseByID :one
|
const GetCourseByID = `-- name: GetCourseByID :one
|
||||||
SELECT id, category_id, title, description, is_active, thumbnail, intro_video_url, display_order, sub_category_id
|
SELECT id, category_id, title, description, is_active, thumbnail, intro_video_url, display_order, sub_category_id
|
||||||
FROM courses
|
FROM courses
|
||||||
|
|
@ -153,6 +217,138 @@ func (q *Queries) GetCoursesByCategory(ctx context.Context, arg GetCoursesByCate
|
||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const GetCoursesBySubCategory = `-- name: GetCoursesBySubCategory :many
|
||||||
|
SELECT
|
||||||
|
COUNT(*) OVER () AS total_count,
|
||||||
|
c.id,
|
||||||
|
c.category_id,
|
||||||
|
c.sub_category_id,
|
||||||
|
c.title,
|
||||||
|
c.description,
|
||||||
|
c.thumbnail,
|
||||||
|
c.intro_video_url,
|
||||||
|
c.is_active
|
||||||
|
FROM courses c
|
||||||
|
WHERE c.sub_category_id = $1
|
||||||
|
ORDER BY c.display_order ASC, c.id ASC
|
||||||
|
LIMIT $3::INT
|
||||||
|
OFFSET $2::INT
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetCoursesBySubCategoryParams struct {
|
||||||
|
SubCategoryID pgtype.Int8 `json:"sub_category_id"`
|
||||||
|
Offset pgtype.Int4 `json:"offset"`
|
||||||
|
Limit pgtype.Int4 `json:"limit"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetCoursesBySubCategoryRow struct {
|
||||||
|
TotalCount int64 `json:"total_count"`
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
CategoryID int64 `json:"category_id"`
|
||||||
|
SubCategoryID pgtype.Int8 `json:"sub_category_id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description pgtype.Text `json:"description"`
|
||||||
|
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||||
|
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
||||||
|
IsActive bool `json:"is_active"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetCoursesBySubCategory(ctx context.Context, arg GetCoursesBySubCategoryParams) ([]GetCoursesBySubCategoryRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, GetCoursesBySubCategory, arg.SubCategoryID, arg.Offset, arg.Limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []GetCoursesBySubCategoryRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i GetCoursesBySubCategoryRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.TotalCount,
|
||||||
|
&i.ID,
|
||||||
|
&i.CategoryID,
|
||||||
|
&i.SubCategoryID,
|
||||||
|
&i.Title,
|
||||||
|
&i.Description,
|
||||||
|
&i.Thumbnail,
|
||||||
|
&i.IntroVideoUrl,
|
||||||
|
&i.IsActive,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const GetHumanLanguageCourses = `-- name: GetHumanLanguageCourses :many
|
||||||
|
SELECT
|
||||||
|
COUNT(*) OVER () AS total_count,
|
||||||
|
c.id,
|
||||||
|
c.category_id,
|
||||||
|
c.sub_category_id,
|
||||||
|
c.title,
|
||||||
|
c.description,
|
||||||
|
c.thumbnail,
|
||||||
|
c.intro_video_url,
|
||||||
|
c.is_active
|
||||||
|
FROM courses c
|
||||||
|
JOIN course_categories cc ON cc.id = c.category_id
|
||||||
|
WHERE lower(trim(cc.name)) = 'human language'
|
||||||
|
ORDER BY c.display_order ASC, c.id ASC
|
||||||
|
LIMIT $2::INT
|
||||||
|
OFFSET $1::INT
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetHumanLanguageCoursesParams struct {
|
||||||
|
Offset pgtype.Int4 `json:"offset"`
|
||||||
|
Limit pgtype.Int4 `json:"limit"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetHumanLanguageCoursesRow struct {
|
||||||
|
TotalCount int64 `json:"total_count"`
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
CategoryID int64 `json:"category_id"`
|
||||||
|
SubCategoryID pgtype.Int8 `json:"sub_category_id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description pgtype.Text `json:"description"`
|
||||||
|
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||||
|
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
||||||
|
IsActive bool `json:"is_active"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetHumanLanguageCourses(ctx context.Context, arg GetHumanLanguageCoursesParams) ([]GetHumanLanguageCoursesRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, GetHumanLanguageCourses, arg.Offset, arg.Limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []GetHumanLanguageCoursesRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i GetHumanLanguageCoursesRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.TotalCount,
|
||||||
|
&i.ID,
|
||||||
|
&i.CategoryID,
|
||||||
|
&i.SubCategoryID,
|
||||||
|
&i.Title,
|
||||||
|
&i.Description,
|
||||||
|
&i.Thumbnail,
|
||||||
|
&i.IntroVideoUrl,
|
||||||
|
&i.IsActive,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
const ReorderCourses = `-- name: ReorderCourses :exec
|
const ReorderCourses = `-- name: ReorderCourses :exec
|
||||||
UPDATE courses
|
UPDATE courses
|
||||||
SET display_order = bulk.position
|
SET display_order = bulk.position
|
||||||
|
|
|
||||||
|
|
@ -364,6 +364,171 @@ func (q *Queries) CreateSubModuleVideo(ctx context.Context, arg CreateSubModuleV
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const GetAllLevels = `-- name: GetAllLevels :many
|
||||||
|
SELECT
|
||||||
|
COUNT(*) OVER () AS total_count,
|
||||||
|
l.id, l.course_id, l.cefr_level, l.display_order, l.is_active, l.created_at
|
||||||
|
FROM levels l
|
||||||
|
ORDER BY l.display_order ASC, l.id ASC
|
||||||
|
LIMIT $2::INT
|
||||||
|
OFFSET $1::INT
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetAllLevelsParams struct {
|
||||||
|
Offset pgtype.Int4 `json:"offset"`
|
||||||
|
Limit pgtype.Int4 `json:"limit"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetAllLevelsRow struct {
|
||||||
|
TotalCount int64 `json:"total_count"`
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
CourseID int64 `json:"course_id"`
|
||||||
|
CefrLevel string `json:"cefr_level"`
|
||||||
|
DisplayOrder int32 `json:"display_order"`
|
||||||
|
IsActive bool `json:"is_active"`
|
||||||
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetAllLevels(ctx context.Context, arg GetAllLevelsParams) ([]GetAllLevelsRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, GetAllLevels, arg.Offset, arg.Limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []GetAllLevelsRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i GetAllLevelsRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.TotalCount,
|
||||||
|
&i.ID,
|
||||||
|
&i.CourseID,
|
||||||
|
&i.CefrLevel,
|
||||||
|
&i.DisplayOrder,
|
||||||
|
&i.IsActive,
|
||||||
|
&i.CreatedAt,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const GetAllModules = `-- name: GetAllModules :many
|
||||||
|
SELECT
|
||||||
|
COUNT(*) OVER () AS total_count,
|
||||||
|
m.id, m.level_id, m.title, m.description, m.display_order, m.is_active, m.created_at
|
||||||
|
FROM modules m
|
||||||
|
ORDER BY m.display_order ASC, m.id ASC
|
||||||
|
LIMIT $2::INT
|
||||||
|
OFFSET $1::INT
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetAllModulesParams struct {
|
||||||
|
Offset pgtype.Int4 `json:"offset"`
|
||||||
|
Limit pgtype.Int4 `json:"limit"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetAllModulesRow struct {
|
||||||
|
TotalCount int64 `json:"total_count"`
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
LevelID int64 `json:"level_id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description pgtype.Text `json:"description"`
|
||||||
|
DisplayOrder int32 `json:"display_order"`
|
||||||
|
IsActive bool `json:"is_active"`
|
||||||
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetAllModules(ctx context.Context, arg GetAllModulesParams) ([]GetAllModulesRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, GetAllModules, arg.Offset, arg.Limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []GetAllModulesRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i GetAllModulesRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.TotalCount,
|
||||||
|
&i.ID,
|
||||||
|
&i.LevelID,
|
||||||
|
&i.Title,
|
||||||
|
&i.Description,
|
||||||
|
&i.DisplayOrder,
|
||||||
|
&i.IsActive,
|
||||||
|
&i.CreatedAt,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const GetAllSubModules = `-- name: GetAllSubModules :many
|
||||||
|
SELECT
|
||||||
|
COUNT(*) OVER () AS total_count,
|
||||||
|
sm.id, sm.module_id, sm.title, sm.description, sm.display_order, sm.is_active, sm.created_at, sm.legacy_sub_course_id
|
||||||
|
FROM sub_modules sm
|
||||||
|
ORDER BY sm.display_order ASC, sm.id ASC
|
||||||
|
LIMIT $2::INT
|
||||||
|
OFFSET $1::INT
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetAllSubModulesParams struct {
|
||||||
|
Offset pgtype.Int4 `json:"offset"`
|
||||||
|
Limit pgtype.Int4 `json:"limit"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetAllSubModulesRow struct {
|
||||||
|
TotalCount int64 `json:"total_count"`
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
ModuleID int64 `json:"module_id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description pgtype.Text `json:"description"`
|
||||||
|
DisplayOrder int32 `json:"display_order"`
|
||||||
|
IsActive bool `json:"is_active"`
|
||||||
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
|
LegacySubCourseID pgtype.Int8 `json:"legacy_sub_course_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetAllSubModules(ctx context.Context, arg GetAllSubModulesParams) ([]GetAllSubModulesRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, GetAllSubModules, arg.Offset, arg.Limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []GetAllSubModulesRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i GetAllSubModulesRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.TotalCount,
|
||||||
|
&i.ID,
|
||||||
|
&i.ModuleID,
|
||||||
|
&i.Title,
|
||||||
|
&i.Description,
|
||||||
|
&i.DisplayOrder,
|
||||||
|
&i.IsActive,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.LegacySubCourseID,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
const GetCourseSubCategories = `-- name: GetCourseSubCategories :many
|
const GetCourseSubCategories = `-- name: GetCourseSubCategories :many
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(*) OVER () AS total_count,
|
COUNT(*) OVER () AS total_count,
|
||||||
|
|
@ -607,6 +772,26 @@ func (q *Queries) GetHumanLanguageCourseSubCategories(ctx context.Context, arg G
|
||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const GetLevelByID = `-- name: GetLevelByID :one
|
||||||
|
SELECT id, course_id, cefr_level, display_order, is_active, created_at
|
||||||
|
FROM levels
|
||||||
|
WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetLevelByID(ctx context.Context, id int64) (Level, error) {
|
||||||
|
row := q.db.QueryRow(ctx, GetLevelByID, id)
|
||||||
|
var i Level
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.CourseID,
|
||||||
|
&i.CefrLevel,
|
||||||
|
&i.DisplayOrder,
|
||||||
|
&i.IsActive,
|
||||||
|
&i.CreatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
const GetLevelsByCourseID = `-- name: GetLevelsByCourseID :many
|
const GetLevelsByCourseID = `-- name: GetLevelsByCourseID :many
|
||||||
SELECT id, course_id, cefr_level, display_order, is_active, created_at
|
SELECT id, course_id, cefr_level, display_order, is_active, created_at
|
||||||
FROM levels
|
FROM levels
|
||||||
|
|
@ -642,6 +827,27 @@ func (q *Queries) GetLevelsByCourseID(ctx context.Context, courseID int64) ([]Le
|
||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const GetModuleByID = `-- name: GetModuleByID :one
|
||||||
|
SELECT id, level_id, title, description, display_order, is_active, created_at
|
||||||
|
FROM modules
|
||||||
|
WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetModuleByID(ctx context.Context, id int64) (Module, error) {
|
||||||
|
row := q.db.QueryRow(ctx, GetModuleByID, id)
|
||||||
|
var i Module
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.LevelID,
|
||||||
|
&i.Title,
|
||||||
|
&i.Description,
|
||||||
|
&i.DisplayOrder,
|
||||||
|
&i.IsActive,
|
||||||
|
&i.CreatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
const GetModulesByLevelID = `-- name: GetModulesByLevelID :many
|
const GetModulesByLevelID = `-- name: GetModulesByLevelID :many
|
||||||
SELECT id, level_id, title, description, display_order, is_active, created_at
|
SELECT id, level_id, title, description, display_order, is_active, created_at
|
||||||
FROM modules
|
FROM modules
|
||||||
|
|
@ -678,6 +884,28 @@ func (q *Queries) GetModulesByLevelID(ctx context.Context, levelID int64) ([]Mod
|
||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const GetSubModuleByID = `-- name: GetSubModuleByID :one
|
||||||
|
SELECT id, module_id, title, description, display_order, is_active, created_at, legacy_sub_course_id
|
||||||
|
FROM sub_modules
|
||||||
|
WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetSubModuleByID(ctx context.Context, id int64) (SubModule, error) {
|
||||||
|
row := q.db.QueryRow(ctx, GetSubModuleByID, id)
|
||||||
|
var i SubModule
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.ModuleID,
|
||||||
|
&i.Title,
|
||||||
|
&i.Description,
|
||||||
|
&i.DisplayOrder,
|
||||||
|
&i.IsActive,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.LegacySubCourseID,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
const GetSubModuleLessonByID = `-- name: GetSubModuleLessonByID :one
|
const GetSubModuleLessonByID = `-- name: GetSubModuleLessonByID :one
|
||||||
SELECT
|
SELECT
|
||||||
smp.id,
|
smp.id,
|
||||||
|
|
@ -798,6 +1026,62 @@ func (q *Queries) GetSubModuleLessons(ctx context.Context, subModuleID int64) ([
|
||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const GetSubModulePracticeByID = `-- name: GetSubModulePracticeByID :one
|
||||||
|
SELECT
|
||||||
|
smp.id,
|
||||||
|
smp.sub_module_id,
|
||||||
|
smp.title,
|
||||||
|
smp.description,
|
||||||
|
smp.thumbnail,
|
||||||
|
smp.intro_video_url,
|
||||||
|
smp.question_set_id,
|
||||||
|
smp.display_order,
|
||||||
|
smp.is_active,
|
||||||
|
qs.status,
|
||||||
|
qs.set_type,
|
||||||
|
(SELECT COUNT(*) FROM question_set_items qsi WHERE qsi.set_id = qs.id) AS question_count
|
||||||
|
FROM sub_module_practices 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 = 'PRACTICE'
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetSubModulePracticeByIDRow struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
SubModuleID int64 `json:"sub_module_id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description pgtype.Text `json:"description"`
|
||||||
|
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||||
|
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
||||||
|
QuestionSetID int64 `json:"question_set_id"`
|
||||||
|
DisplayOrder int32 `json:"display_order"`
|
||||||
|
IsActive bool `json:"is_active"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
SetType string `json:"set_type"`
|
||||||
|
QuestionCount int64 `json:"question_count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetSubModulePracticeByID(ctx context.Context, id int64) (GetSubModulePracticeByIDRow, error) {
|
||||||
|
row := q.db.QueryRow(ctx, GetSubModulePracticeByID, id)
|
||||||
|
var i GetSubModulePracticeByIDRow
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.SubModuleID,
|
||||||
|
&i.Title,
|
||||||
|
&i.Description,
|
||||||
|
&i.Thumbnail,
|
||||||
|
&i.IntroVideoUrl,
|
||||||
|
&i.QuestionSetID,
|
||||||
|
&i.DisplayOrder,
|
||||||
|
&i.IsActive,
|
||||||
|
&i.Status,
|
||||||
|
&i.SetType,
|
||||||
|
&i.QuestionCount,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
const GetSubModulePractices = `-- name: GetSubModulePractices :many
|
const GetSubModulePractices = `-- name: GetSubModulePractices :many
|
||||||
SELECT
|
SELECT
|
||||||
smp.id,
|
smp.id,
|
||||||
|
|
|
||||||
|
|
@ -269,6 +269,501 @@ func (h *Handler) ListCoursesByCategory(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListAllCourses godoc
|
||||||
|
// @Summary List all courses
|
||||||
|
// @Description Returns all courses with pagination
|
||||||
|
// @Tags course-management
|
||||||
|
// @Produce json
|
||||||
|
// @Param offset query int false "Offset"
|
||||||
|
// @Param limit query int false "Limit"
|
||||||
|
// @Success 200 {object} domain.Response
|
||||||
|
// @Failure 500 {object} domain.ErrorResponse
|
||||||
|
// @Router /api/v1/course-management/courses [get]
|
||||||
|
func (h *Handler) ListAllCourses(c *fiber.Ctx) error {
|
||||||
|
offset := int32(c.QueryInt("offset", 0))
|
||||||
|
if offset < 0 {
|
||||||
|
offset = 0
|
||||||
|
}
|
||||||
|
limit := int32(c.QueryInt("limit", 10000))
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = 10000
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := h.analyticsDB.GetAllCourses(c.Context(), dbgen.GetAllCoursesParams{
|
||||||
|
Offset: pgtype.Int4{Int32: offset, Valid: true},
|
||||||
|
Limit: pgtype.Int4{Int32: limit, Valid: true},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load courses", Error: err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
total := 0
|
||||||
|
if len(rows) > 0 {
|
||||||
|
total = int(rows[0].TotalCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(domain.Response{
|
||||||
|
Message: "Courses retrieved successfully",
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"courses": rows,
|
||||||
|
"total_count": total,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListHumanLanguageCourses godoc
|
||||||
|
// @Summary List Human Language courses
|
||||||
|
// @Description Returns all courses under Human Language category
|
||||||
|
// @Tags course-management
|
||||||
|
// @Produce json
|
||||||
|
// @Param offset query int false "Offset"
|
||||||
|
// @Param limit query int false "Limit"
|
||||||
|
// @Success 200 {object} domain.Response
|
||||||
|
// @Failure 500 {object} domain.ErrorResponse
|
||||||
|
// @Router /api/v1/course-management/human-language/courses [get]
|
||||||
|
func (h *Handler) ListHumanLanguageCourses(c *fiber.Ctx) error {
|
||||||
|
offset := int32(c.QueryInt("offset", 0))
|
||||||
|
if offset < 0 {
|
||||||
|
offset = 0
|
||||||
|
}
|
||||||
|
limit := int32(c.QueryInt("limit", 10000))
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = 10000
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := h.analyticsDB.GetHumanLanguageCourses(c.Context(), dbgen.GetHumanLanguageCoursesParams{
|
||||||
|
Offset: pgtype.Int4{Int32: offset, Valid: true},
|
||||||
|
Limit: pgtype.Int4{Int32: limit, Valid: true},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load Human Language courses", Error: err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
total := 0
|
||||||
|
if len(rows) > 0 {
|
||||||
|
total = int(rows[0].TotalCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(domain.Response{
|
||||||
|
Message: "Human Language courses retrieved successfully",
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"courses": rows,
|
||||||
|
"total_count": total,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListCoursesBySubCategory godoc
|
||||||
|
// @Summary List courses by sub-category
|
||||||
|
// @Description Returns courses for one sub-category
|
||||||
|
// @Tags course-management
|
||||||
|
// @Produce json
|
||||||
|
// @Param subCategoryId path int true "Sub-category ID"
|
||||||
|
// @Param offset query int false "Offset"
|
||||||
|
// @Param limit query int false "Limit"
|
||||||
|
// @Success 200 {object} domain.Response
|
||||||
|
// @Failure 400 {object} domain.ErrorResponse
|
||||||
|
// @Failure 500 {object} domain.ErrorResponse
|
||||||
|
// @Router /api/v1/course-management/sub-categories/{subCategoryId}/courses [get]
|
||||||
|
func (h *Handler) ListCoursesBySubCategory(c *fiber.Ctx) error {
|
||||||
|
subCategoryID, err := strconv.ParseInt(c.Params("subCategoryId"), 10, 64)
|
||||||
|
if err != nil || subCategoryID <= 0 {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid sub-category ID", Error: "subCategoryId must be a positive integer"})
|
||||||
|
}
|
||||||
|
|
||||||
|
offset := int32(c.QueryInt("offset", 0))
|
||||||
|
if offset < 0 {
|
||||||
|
offset = 0
|
||||||
|
}
|
||||||
|
limit := int32(c.QueryInt("limit", 10000))
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = 10000
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := h.analyticsDB.GetCoursesBySubCategory(c.Context(), dbgen.GetCoursesBySubCategoryParams{
|
||||||
|
SubCategoryID: pgtype.Int8{Int64: subCategoryID, Valid: true},
|
||||||
|
Offset: pgtype.Int4{Int32: offset, Valid: true},
|
||||||
|
Limit: pgtype.Int4{Int32: limit, Valid: true},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load courses", Error: err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
total := 0
|
||||||
|
if len(rows) > 0 {
|
||||||
|
total = int(rows[0].TotalCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(domain.Response{
|
||||||
|
Message: "Courses retrieved successfully",
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"courses": rows,
|
||||||
|
"total_count": total,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCourseByID godoc
|
||||||
|
// @Summary Get course detail
|
||||||
|
// @Description Returns one course by ID
|
||||||
|
// @Tags course-management
|
||||||
|
// @Produce json
|
||||||
|
// @Param courseId path int true "Course ID"
|
||||||
|
// @Success 200 {object} domain.Response
|
||||||
|
// @Failure 400 {object} domain.ErrorResponse
|
||||||
|
// @Failure 404 {object} domain.ErrorResponse
|
||||||
|
// @Failure 500 {object} domain.ErrorResponse
|
||||||
|
// @Router /api/v1/course-management/courses/{courseId} [get]
|
||||||
|
func (h *Handler) GetCourseByID(c *fiber.Ctx) error {
|
||||||
|
courseID, err := strconv.ParseInt(c.Params("courseId"), 10, 64)
|
||||||
|
if err != nil || courseID <= 0 {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Invalid course ID",
|
||||||
|
Error: "courseId must be a positive integer",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
course, err := h.analyticsDB.GetCourseByID(c.Context(), courseID)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Course not found",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(domain.Response{
|
||||||
|
Message: "Course retrieved successfully",
|
||||||
|
Data: course,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListAllLevels godoc
|
||||||
|
// @Summary List all levels
|
||||||
|
// @Description Returns all levels with pagination
|
||||||
|
// @Tags course-management
|
||||||
|
// @Produce json
|
||||||
|
// @Param offset query int false "Offset"
|
||||||
|
// @Param limit query int false "Limit"
|
||||||
|
// @Success 200 {object} domain.Response
|
||||||
|
// @Failure 500 {object} domain.ErrorResponse
|
||||||
|
// @Router /api/v1/course-management/levels [get]
|
||||||
|
func (h *Handler) ListAllLevels(c *fiber.Ctx) error {
|
||||||
|
offset := int32(c.QueryInt("offset", 0))
|
||||||
|
if offset < 0 {
|
||||||
|
offset = 0
|
||||||
|
}
|
||||||
|
limit := int32(c.QueryInt("limit", 10000))
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = 10000
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := h.analyticsDB.GetAllLevels(c.Context(), dbgen.GetAllLevelsParams{
|
||||||
|
Offset: pgtype.Int4{Int32: offset, Valid: true},
|
||||||
|
Limit: pgtype.Int4{Int32: limit, Valid: true},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load levels", Error: err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
total := 0
|
||||||
|
if len(rows) > 0 {
|
||||||
|
total = int(rows[0].TotalCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(domain.Response{
|
||||||
|
Message: "Levels retrieved successfully",
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"levels": rows,
|
||||||
|
"total_count": total,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListLevelsByCourse godoc
|
||||||
|
// @Summary List levels by course
|
||||||
|
// @Description Returns all active levels for one course
|
||||||
|
// @Tags course-management
|
||||||
|
// @Produce json
|
||||||
|
// @Param courseId path int true "Course ID"
|
||||||
|
// @Success 200 {object} domain.Response
|
||||||
|
// @Failure 400 {object} domain.ErrorResponse
|
||||||
|
// @Failure 500 {object} domain.ErrorResponse
|
||||||
|
// @Router /api/v1/course-management/courses/{courseId}/levels [get]
|
||||||
|
func (h *Handler) ListLevelsByCourse(c *fiber.Ctx) error {
|
||||||
|
courseID, err := strconv.ParseInt(c.Params("courseId"), 10, 64)
|
||||||
|
if err != nil || courseID <= 0 {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Invalid course ID",
|
||||||
|
Error: "courseId must be a positive integer",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := h.analyticsDB.GetLevelsByCourseID(c.Context(), courseID)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load levels", Error: err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(domain.Response{
|
||||||
|
Message: "Levels retrieved successfully",
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"levels": rows,
|
||||||
|
"total_count": len(rows),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLevelByID godoc
|
||||||
|
// @Summary Get level detail
|
||||||
|
// @Description Returns one level by ID
|
||||||
|
// @Tags course-management
|
||||||
|
// @Produce json
|
||||||
|
// @Param levelId path int true "Level ID"
|
||||||
|
// @Success 200 {object} domain.Response
|
||||||
|
// @Failure 400 {object} domain.ErrorResponse
|
||||||
|
// @Failure 404 {object} domain.ErrorResponse
|
||||||
|
// @Failure 500 {object} domain.ErrorResponse
|
||||||
|
// @Router /api/v1/course-management/levels/{levelId} [get]
|
||||||
|
func (h *Handler) GetLevelByID(c *fiber.Ctx) error {
|
||||||
|
levelID, err := strconv.ParseInt(c.Params("levelId"), 10, 64)
|
||||||
|
if err != nil || levelID <= 0 {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Invalid level ID",
|
||||||
|
Error: "levelId must be a positive integer",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
level, err := h.analyticsDB.GetLevelByID(c.Context(), levelID)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Level not found",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(domain.Response{
|
||||||
|
Message: "Level retrieved successfully",
|
||||||
|
Data: level,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListAllModules godoc
|
||||||
|
// @Summary List all modules
|
||||||
|
// @Description Returns all modules with pagination
|
||||||
|
// @Tags course-management
|
||||||
|
// @Produce json
|
||||||
|
// @Param offset query int false "Offset"
|
||||||
|
// @Param limit query int false "Limit"
|
||||||
|
// @Success 200 {object} domain.Response
|
||||||
|
// @Failure 500 {object} domain.ErrorResponse
|
||||||
|
// @Router /api/v1/course-management/modules [get]
|
||||||
|
func (h *Handler) ListAllModules(c *fiber.Ctx) error {
|
||||||
|
offset := int32(c.QueryInt("offset", 0))
|
||||||
|
if offset < 0 {
|
||||||
|
offset = 0
|
||||||
|
}
|
||||||
|
limit := int32(c.QueryInt("limit", 10000))
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = 10000
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := h.analyticsDB.GetAllModules(c.Context(), dbgen.GetAllModulesParams{
|
||||||
|
Offset: pgtype.Int4{Int32: offset, Valid: true},
|
||||||
|
Limit: pgtype.Int4{Int32: limit, Valid: true},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load modules", Error: err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
total := 0
|
||||||
|
if len(rows) > 0 {
|
||||||
|
total = int(rows[0].TotalCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(domain.Response{
|
||||||
|
Message: "Modules retrieved successfully",
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"modules": rows,
|
||||||
|
"total_count": total,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListModulesByLevel godoc
|
||||||
|
// @Summary List modules by level
|
||||||
|
// @Description Returns all active modules for one level
|
||||||
|
// @Tags course-management
|
||||||
|
// @Produce json
|
||||||
|
// @Param levelId path int true "Level ID"
|
||||||
|
// @Success 200 {object} domain.Response
|
||||||
|
// @Failure 400 {object} domain.ErrorResponse
|
||||||
|
// @Failure 500 {object} domain.ErrorResponse
|
||||||
|
// @Router /api/v1/course-management/levels/{levelId}/modules [get]
|
||||||
|
func (h *Handler) ListModulesByLevel(c *fiber.Ctx) error {
|
||||||
|
levelID, err := strconv.ParseInt(c.Params("levelId"), 10, 64)
|
||||||
|
if err != nil || levelID <= 0 {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Invalid level ID",
|
||||||
|
Error: "levelId must be a positive integer",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := h.analyticsDB.GetModulesByLevelID(c.Context(), levelID)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load modules", Error: err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(domain.Response{
|
||||||
|
Message: "Modules retrieved successfully",
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"modules": rows,
|
||||||
|
"total_count": len(rows),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetModuleByID godoc
|
||||||
|
// @Summary Get module detail
|
||||||
|
// @Description Returns one module by ID
|
||||||
|
// @Tags course-management
|
||||||
|
// @Produce json
|
||||||
|
// @Param moduleId path int true "Module ID"
|
||||||
|
// @Success 200 {object} domain.Response
|
||||||
|
// @Failure 400 {object} domain.ErrorResponse
|
||||||
|
// @Failure 404 {object} domain.ErrorResponse
|
||||||
|
// @Failure 500 {object} domain.ErrorResponse
|
||||||
|
// @Router /api/v1/course-management/modules/{moduleId} [get]
|
||||||
|
func (h *Handler) GetModuleByID(c *fiber.Ctx) error {
|
||||||
|
moduleID, err := strconv.ParseInt(c.Params("moduleId"), 10, 64)
|
||||||
|
if err != nil || moduleID <= 0 {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Invalid module ID",
|
||||||
|
Error: "moduleId must be a positive integer",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
mod, err := h.analyticsDB.GetModuleByID(c.Context(), moduleID)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Module not found",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(domain.Response{
|
||||||
|
Message: "Module retrieved successfully",
|
||||||
|
Data: mod,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListAllSubModules godoc
|
||||||
|
// @Summary List all sub-modules
|
||||||
|
// @Description Returns all sub-modules with pagination
|
||||||
|
// @Tags course-management
|
||||||
|
// @Produce json
|
||||||
|
// @Param offset query int false "Offset"
|
||||||
|
// @Param limit query int false "Limit"
|
||||||
|
// @Success 200 {object} domain.Response
|
||||||
|
// @Failure 500 {object} domain.ErrorResponse
|
||||||
|
// @Router /api/v1/course-management/sub-modules [get]
|
||||||
|
func (h *Handler) ListAllSubModules(c *fiber.Ctx) error {
|
||||||
|
offset := int32(c.QueryInt("offset", 0))
|
||||||
|
if offset < 0 {
|
||||||
|
offset = 0
|
||||||
|
}
|
||||||
|
limit := int32(c.QueryInt("limit", 10000))
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = 10000
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := h.analyticsDB.GetAllSubModules(c.Context(), dbgen.GetAllSubModulesParams{
|
||||||
|
Offset: pgtype.Int4{Int32: offset, Valid: true},
|
||||||
|
Limit: pgtype.Int4{Int32: limit, Valid: true},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load sub-modules", Error: err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
total := 0
|
||||||
|
if len(rows) > 0 {
|
||||||
|
total = int(rows[0].TotalCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(domain.Response{
|
||||||
|
Message: "Sub-modules retrieved successfully",
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"sub_modules": rows,
|
||||||
|
"total_count": total,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListSubModulesByModule godoc
|
||||||
|
// @Summary List sub-modules by module
|
||||||
|
// @Description Returns all active sub-modules for one module
|
||||||
|
// @Tags course-management
|
||||||
|
// @Produce json
|
||||||
|
// @Param moduleId path int true "Module ID"
|
||||||
|
// @Success 200 {object} domain.Response
|
||||||
|
// @Failure 400 {object} domain.ErrorResponse
|
||||||
|
// @Failure 500 {object} domain.ErrorResponse
|
||||||
|
// @Router /api/v1/course-management/modules/{moduleId}/sub-modules [get]
|
||||||
|
func (h *Handler) ListSubModulesByModule(c *fiber.Ctx) error {
|
||||||
|
moduleID, err := strconv.ParseInt(c.Params("moduleId"), 10, 64)
|
||||||
|
if err != nil || moduleID <= 0 {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Invalid module ID",
|
||||||
|
Error: "moduleId must be a positive integer",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := h.analyticsDB.GetSubModulesByModuleID(c.Context(), moduleID)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load sub-modules", Error: err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(domain.Response{
|
||||||
|
Message: "Sub-modules retrieved successfully",
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"sub_modules": rows,
|
||||||
|
"total_count": len(rows),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSubModuleByID godoc
|
||||||
|
// @Summary Get sub-module detail
|
||||||
|
// @Description Returns one sub-module by ID
|
||||||
|
// @Tags course-management
|
||||||
|
// @Produce json
|
||||||
|
// @Param subModuleId path int true "Sub-module ID"
|
||||||
|
// @Success 200 {object} domain.Response
|
||||||
|
// @Failure 400 {object} domain.ErrorResponse
|
||||||
|
// @Failure 404 {object} domain.ErrorResponse
|
||||||
|
// @Failure 500 {object} domain.ErrorResponse
|
||||||
|
// @Router /api/v1/course-management/sub-modules/{subModuleId} [get]
|
||||||
|
func (h *Handler) GetSubModuleByID(c *fiber.Ctx) error {
|
||||||
|
subModuleID, err := strconv.ParseInt(c.Params("subModuleId"), 10, 64)
|
||||||
|
if err != nil || subModuleID <= 0 {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Invalid sub-module ID",
|
||||||
|
Error: "subModuleId must be a positive integer",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
subModule, err := h.analyticsDB.GetSubModuleByID(c.Context(), subModuleID)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Sub-module not found",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(domain.Response{
|
||||||
|
Message: "Sub-module retrieved successfully",
|
||||||
|
Data: subModule,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// ListCourseSubCategories godoc
|
// ListCourseSubCategories godoc
|
||||||
// @Summary List course sub-categories
|
// @Summary List course sub-categories
|
||||||
// @Description Returns all active course sub-categories
|
// @Description Returns all active course sub-categories
|
||||||
|
|
@ -1383,6 +1878,78 @@ func (h *Handler) CreateSubModulePractice(c *fiber.Ctx) error {
|
||||||
return c.Status(fiber.StatusCreated).JSON(domain.Response{Message: "Practice created", Data: created})
|
return c.Status(fiber.StatusCreated).JSON(domain.Response{Message: "Practice created", Data: created})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetSubModulePractices godoc
|
||||||
|
// @Summary Get practices under sub-module
|
||||||
|
// @Description Returns all active practices attached to a sub-module
|
||||||
|
// @Tags course-management
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param subModuleId path int true "Sub-module ID"
|
||||||
|
// @Success 200 {object} domain.Response
|
||||||
|
// @Failure 400 {object} domain.ErrorResponse
|
||||||
|
// @Failure 500 {object} domain.ErrorResponse
|
||||||
|
// @Router /api/v1/course-management/sub-modules/{subModuleId}/practices [get]
|
||||||
|
func (h *Handler) GetSubModulePractices(c *fiber.Ctx) error {
|
||||||
|
subModuleID, err := strconv.ParseInt(c.Params("subModuleId"), 10, 64)
|
||||||
|
if err != nil || subModuleID <= 0 {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Invalid sub-module ID",
|
||||||
|
Error: "subModuleId must be a positive integer",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
practices, err := h.analyticsDB.GetSubModulePractices(c.Context(), subModuleID)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Failed to load sub-module practices",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(domain.Response{
|
||||||
|
Message: "Sub-module practices retrieved successfully",
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"practices": practices,
|
||||||
|
"total_count": len(practices),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSubModulePracticeByID godoc
|
||||||
|
// @Summary Get practice detail
|
||||||
|
// @Description Returns one active practice by practice ID
|
||||||
|
// @Tags course-management
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param practiceId path int true "Practice ID"
|
||||||
|
// @Success 200 {object} domain.Response
|
||||||
|
// @Failure 400 {object} domain.ErrorResponse
|
||||||
|
// @Failure 404 {object} domain.ErrorResponse
|
||||||
|
// @Failure 500 {object} domain.ErrorResponse
|
||||||
|
// @Router /api/v1/course-management/practices/{practiceId} [get]
|
||||||
|
func (h *Handler) GetSubModulePracticeByID(c *fiber.Ctx) error {
|
||||||
|
practiceID, err := strconv.ParseInt(c.Params("practiceId"), 10, 64)
|
||||||
|
if err != nil || practiceID <= 0 {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Invalid practice ID",
|
||||||
|
Error: "practiceId must be a positive integer",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
practice, err := h.analyticsDB.GetSubModulePracticeByID(c.Context(), practiceID)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Practice not found",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(domain.Response{
|
||||||
|
Message: "Practice retrieved successfully",
|
||||||
|
Data: practice,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Handler) GetSubModuleVideos(c *fiber.Ctx) error {
|
func (h *Handler) GetSubModuleVideos(c *fiber.Ctx) error {
|
||||||
subModuleID, err := strconv.ParseInt(c.Params("subModuleId"), 10, 64)
|
subModuleID, err := strconv.ParseInt(c.Params("subModuleId"), 10, 64)
|
||||||
if err != nil || subModuleID <= 0 {
|
if err != nil || subModuleID <= 0 {
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,19 @@ func (a *App) initAppRoutes() {
|
||||||
|
|
||||||
// Unified Course Management (single hierarchy model)
|
// Unified Course Management (single hierarchy model)
|
||||||
groupV1.Get("/course-management/categories", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.ListCourseCategories)
|
groupV1.Get("/course-management/categories", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.ListCourseCategories)
|
||||||
groupV1.Get("/course-management/categories/:categoryId/courses", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.ListCoursesByCategory)
|
groupV1.Get("/course-management/courses", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.ListAllCourses)
|
||||||
|
groupV1.Get("/course-management/human-language/courses", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.ListHumanLanguageCourses)
|
||||||
|
groupV1.Get("/course-management/sub-categories/:subCategoryId/courses", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.ListCoursesBySubCategory)
|
||||||
|
groupV1.Get("/course-management/courses/:courseId", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.GetCourseByID)
|
||||||
|
groupV1.Get("/course-management/levels", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.ListAllLevels)
|
||||||
|
groupV1.Get("/course-management/courses/:courseId/levels", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.ListLevelsByCourse)
|
||||||
|
groupV1.Get("/course-management/levels/:levelId", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.GetLevelByID)
|
||||||
|
groupV1.Get("/course-management/modules", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.ListAllModules)
|
||||||
|
groupV1.Get("/course-management/levels/:levelId/modules", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.ListModulesByLevel)
|
||||||
|
groupV1.Get("/course-management/modules/:moduleId", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.GetModuleByID)
|
||||||
|
groupV1.Get("/course-management/sub-modules", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.ListAllSubModules)
|
||||||
|
groupV1.Get("/course-management/modules/:moduleId/sub-modules", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.ListSubModulesByModule)
|
||||||
|
groupV1.Get("/course-management/sub-modules/:subModuleId", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.GetSubModuleByID)
|
||||||
groupV1.Post("/course-management/categories", a.authMiddleware, a.RequirePermission("course_categories.create"), h.CreateCourseCategory)
|
groupV1.Post("/course-management/categories", a.authMiddleware, a.RequirePermission("course_categories.create"), h.CreateCourseCategory)
|
||||||
groupV1.Delete("/course-management/categories/:categoryId", a.authMiddleware, a.RequirePermission("course_categories.delete"), h.DeleteCourseCategory)
|
groupV1.Delete("/course-management/categories/:categoryId", a.authMiddleware, a.RequirePermission("course_categories.delete"), h.DeleteCourseCategory)
|
||||||
groupV1.Post("/course-management/courses", a.authMiddleware, a.RequirePermission("courses.create"), h.CreateCourse)
|
groupV1.Post("/course-management/courses", a.authMiddleware, a.RequirePermission("courses.create"), h.CreateCourse)
|
||||||
|
|
@ -101,7 +113,6 @@ func (a *App) initAppRoutes() {
|
||||||
groupV1.Post("/course-management/sub-modules", a.authMiddleware, a.RequirePermission("subcourses.create"), h.CreateSubModule)
|
groupV1.Post("/course-management/sub-modules", a.authMiddleware, a.RequirePermission("subcourses.create"), h.CreateSubModule)
|
||||||
groupV1.Put("/course-management/sub-modules/:subModuleId", a.authMiddleware, a.RequirePermission("subcourses.update"), h.UpdateSubModule)
|
groupV1.Put("/course-management/sub-modules/:subModuleId", a.authMiddleware, a.RequirePermission("subcourses.update"), h.UpdateSubModule)
|
||||||
groupV1.Delete("/course-management/sub-modules/:subModuleId", a.authMiddleware, a.RequirePermission("subcourses.delete"), h.DeleteSubModule)
|
groupV1.Delete("/course-management/sub-modules/:subModuleId", a.authMiddleware, a.RequirePermission("subcourses.delete"), h.DeleteSubModule)
|
||||||
groupV1.Get("/course-management/sub-modules/:subModuleId/videos", a.authMiddleware, a.RequirePermission("videos.list"), h.GetSubModuleVideos)
|
|
||||||
groupV1.Post("/course-management/sub-module-videos", a.authMiddleware, a.RequirePermission("videos.create"), h.CreateSubModuleVideo)
|
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.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.Delete("/course-management/sub-module-videos/:videoId", a.authMiddleware, a.RequirePermission("videos.delete"), h.DeleteSubModuleVideo)
|
||||||
|
|
@ -109,6 +120,8 @@ func (a *App) initAppRoutes() {
|
||||||
groupV1.Get("/course-management/sub-module-lessons/:lessonId", a.authMiddleware, a.RequirePermission("question_sets.get"), h.GetSubModuleLessonByID)
|
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.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.Post("/course-management/sub-module-lessons", a.authMiddleware, a.RequirePermission("question_sets.update"), h.AttachSubModuleLesson)
|
||||||
|
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)
|
groupV1.Post("/course-management/sub-module-practices", a.authMiddleware, a.RequirePermission("question_sets.update"), h.CreateSubModulePractice)
|
||||||
groupV1.Put("/course-management/practices/:practiceId", a.authMiddleware, a.RequirePermission("question_sets.update"), h.UpdatePractice)
|
groupV1.Put("/course-management/practices/:practiceId", a.authMiddleware, a.RequirePermission("question_sets.update"), h.UpdatePractice)
|
||||||
groupV1.Delete("/course-management/practices/:practiceId", a.authMiddleware, a.RequirePermission("question_sets.delete"), h.DeletePractice)
|
groupV1.Delete("/course-management/practices/:practiceId", a.authMiddleware, a.RequirePermission("question_sets.delete"), h.DeletePractice)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user