new course management hierarchy
This commit is contained in:
parent
7ecfdd9cc8
commit
7613eb583a
25
db/migrations/000030_unified_hierarchy.down.sql
Normal file
25
db/migrations/000030_unified_hierarchy.down.sql
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
UPDATE question_sets qs
|
||||
SET owner_type = 'SUB_COURSE',
|
||||
owner_id = sm.legacy_sub_course_id
|
||||
FROM sub_modules sm
|
||||
WHERE qs.owner_type = 'SUB_MODULE'
|
||||
AND qs.owner_id = sm.id
|
||||
AND qs.set_type = 'PRACTICE'
|
||||
AND sm.legacy_sub_course_id IS NOT NULL;
|
||||
|
||||
DROP TABLE IF EXISTS sub_module_practices CASCADE;
|
||||
DROP TABLE IF EXISTS sub_module_videos CASCADE;
|
||||
DROP TABLE IF EXISTS sub_modules CASCADE;
|
||||
DROP TABLE IF EXISTS modules CASCADE;
|
||||
DROP TABLE IF EXISTS levels CASCADE;
|
||||
|
||||
ALTER TABLE courses DROP COLUMN IF EXISTS sub_category_id;
|
||||
DROP TABLE IF EXISTS course_sub_categories CASCADE;
|
||||
|
||||
-- Best-effort rollback to old expectation.
|
||||
UPDATE user_practice_progress
|
||||
SET sub_course_id = 1
|
||||
WHERE sub_course_id IS NULL;
|
||||
ALTER TABLE user_practice_progress
|
||||
ALTER COLUMN sub_course_id SET NOT NULL;
|
||||
|
||||
228
db/migrations/000030_unified_hierarchy.up.sql
Normal file
228
db/migrations/000030_unified_hierarchy.up.sql
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
-- Unified hierarchy
|
||||
-- Course Category -> Course Sub-category -> Course -> Level -> Module -> Sub-Module
|
||||
-- -> Sub-Module Videos
|
||||
-- -> Sub-Module Practices (question sets)
|
||||
|
||||
CREATE TABLE IF NOT EXISTS course_sub_categories (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
category_id BIGINT NOT NULL REFERENCES course_categories(id) ON DELETE CASCADE,
|
||||
name VARCHAR(150) NOT NULL,
|
||||
description TEXT,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
display_order INT NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(category_id, name)
|
||||
);
|
||||
|
||||
ALTER TABLE courses
|
||||
ADD COLUMN IF NOT EXISTS sub_category_id BIGINT REFERENCES course_sub_categories(id) ON DELETE SET NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_courses_sub_category_id ON courses(sub_category_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS levels (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
course_id BIGINT NOT NULL REFERENCES courses(id) ON DELETE CASCADE,
|
||||
cefr_level VARCHAR(2) NOT NULL,
|
||||
display_order INT NOT NULL DEFAULT 0,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(course_id, cefr_level),
|
||||
CHECK (cefr_level IN ('A1', 'A2', 'A3', 'B1', 'B2', 'B3', 'C1', 'C2', 'C3'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_levels_course_id ON levels(course_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS modules (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
level_id BIGINT NOT NULL REFERENCES levels(id) ON DELETE CASCADE,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
display_order INT NOT NULL DEFAULT 0,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_modules_level_id ON modules(level_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sub_modules (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
module_id BIGINT NOT NULL REFERENCES modules(id) ON DELETE CASCADE,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
display_order INT NOT NULL DEFAULT 0,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
legacy_sub_course_id BIGINT UNIQUE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_sub_modules_module_id ON sub_modules(module_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sub_module_videos (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
sub_module_id BIGINT NOT NULL REFERENCES sub_modules(id) ON DELETE CASCADE,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
video_url TEXT NOT NULL,
|
||||
duration INT,
|
||||
resolution VARCHAR(20),
|
||||
is_published BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
publish_date TIMESTAMPTZ,
|
||||
visibility VARCHAR(50),
|
||||
instructor_id VARCHAR(100),
|
||||
thumbnail TEXT,
|
||||
display_order INT NOT NULL DEFAULT 0,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'DRAFT',
|
||||
vimeo_id TEXT,
|
||||
vimeo_embed_url TEXT,
|
||||
vimeo_player_html TEXT,
|
||||
vimeo_status VARCHAR(50),
|
||||
video_host_provider VARCHAR(20),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_sub_module_videos_sub_module_id ON sub_module_videos(sub_module_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sub_module_practices (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
sub_module_id BIGINT NOT NULL REFERENCES sub_modules(id) ON DELETE CASCADE,
|
||||
question_set_id BIGINT NOT NULL REFERENCES question_sets(id) ON DELETE CASCADE,
|
||||
intro_video_url TEXT,
|
||||
display_order INT NOT NULL DEFAULT 0,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(question_set_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_sub_module_practices_sub_module_id ON sub_module_practices(sub_module_id);
|
||||
|
||||
-- Practice progress now supports sub-module owned practices where no legacy sub_course exists.
|
||||
ALTER TABLE user_practice_progress
|
||||
ALTER COLUMN sub_course_id DROP NOT NULL;
|
||||
|
||||
-- Backfill from existing structure
|
||||
INSERT INTO course_sub_categories (category_id, name, description, display_order, is_active)
|
||||
SELECT cc.id, c.title || ' Group', 'Auto-generated from existing course structure', 0, TRUE
|
||||
FROM courses c
|
||||
JOIN course_categories cc ON cc.id = c.category_id
|
||||
LEFT JOIN course_sub_categories csc
|
||||
ON csc.category_id = cc.id AND csc.name = c.title || ' Group'
|
||||
WHERE csc.id IS NULL;
|
||||
|
||||
UPDATE courses c
|
||||
SET sub_category_id = csc.id
|
||||
FROM course_sub_categories csc
|
||||
WHERE csc.category_id = c.category_id
|
||||
AND csc.name = c.title || ' Group'
|
||||
AND c.sub_category_id IS NULL;
|
||||
|
||||
INSERT INTO levels (course_id, cefr_level, display_order, is_active)
|
||||
SELECT
|
||||
sc.course_id,
|
||||
sc.sub_level,
|
||||
MIN(sc.display_order),
|
||||
BOOL_AND(sc.is_active)
|
||||
FROM sub_courses sc
|
||||
WHERE sc.sub_level IN ('A1', 'A2', 'A3', 'B1', 'B2', 'B3', 'C1', 'C2', 'C3')
|
||||
GROUP BY sc.course_id, sc.sub_level
|
||||
ON CONFLICT (course_id, cefr_level) DO NOTHING;
|
||||
|
||||
INSERT INTO modules (level_id, title, description, display_order, is_active)
|
||||
SELECT
|
||||
l.id,
|
||||
l.cefr_level || ' Module 1',
|
||||
'Auto-generated default module for ' || l.cefr_level,
|
||||
1,
|
||||
l.is_active
|
||||
FROM levels l
|
||||
LEFT JOIN modules m ON m.level_id = l.id AND m.display_order = 1
|
||||
WHERE m.id IS NULL;
|
||||
|
||||
INSERT INTO sub_modules (module_id, title, description, display_order, is_active, legacy_sub_course_id)
|
||||
SELECT
|
||||
m.id,
|
||||
sc.title,
|
||||
sc.description,
|
||||
sc.display_order,
|
||||
sc.is_active,
|
||||
sc.id
|
||||
FROM sub_courses sc
|
||||
JOIN levels l
|
||||
ON l.course_id = sc.course_id
|
||||
AND l.cefr_level = sc.sub_level
|
||||
JOIN modules m
|
||||
ON m.level_id = l.id
|
||||
AND m.display_order = 1
|
||||
LEFT JOIN sub_modules sm ON sm.legacy_sub_course_id = sc.id
|
||||
WHERE sm.id IS NULL;
|
||||
|
||||
INSERT INTO sub_module_videos (
|
||||
sub_module_id,
|
||||
title,
|
||||
description,
|
||||
video_url,
|
||||
duration,
|
||||
resolution,
|
||||
is_published,
|
||||
publish_date,
|
||||
visibility,
|
||||
instructor_id,
|
||||
thumbnail,
|
||||
display_order,
|
||||
status,
|
||||
vimeo_id,
|
||||
vimeo_embed_url,
|
||||
vimeo_player_html,
|
||||
vimeo_status,
|
||||
video_host_provider
|
||||
)
|
||||
SELECT
|
||||
sm.id,
|
||||
scv.title,
|
||||
scv.description,
|
||||
scv.video_url,
|
||||
scv.duration,
|
||||
scv.resolution,
|
||||
scv.is_published,
|
||||
scv.publish_date,
|
||||
scv.visibility,
|
||||
scv.instructor_id,
|
||||
scv.thumbnail,
|
||||
scv.display_order,
|
||||
scv.status,
|
||||
scv.vimeo_id,
|
||||
scv.vimeo_embed_url,
|
||||
scv.vimeo_player_html,
|
||||
scv.vimeo_status,
|
||||
scv.video_host_provider
|
||||
FROM sub_course_videos scv
|
||||
JOIN sub_modules sm ON sm.legacy_sub_course_id = scv.sub_course_id
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM sub_module_videos smv
|
||||
WHERE smv.sub_module_id = sm.id
|
||||
AND smv.title = scv.title
|
||||
AND COALESCE(smv.video_url, '') = COALESCE(scv.video_url, '')
|
||||
);
|
||||
|
||||
UPDATE question_sets qs
|
||||
SET owner_type = 'SUB_MODULE',
|
||||
owner_id = sm.id
|
||||
FROM sub_modules sm
|
||||
WHERE qs.owner_type = 'SUB_COURSE'
|
||||
AND qs.owner_id = sm.legacy_sub_course_id
|
||||
AND qs.set_type = 'PRACTICE';
|
||||
|
||||
INSERT INTO sub_module_practices (sub_module_id, question_set_id, intro_video_url, display_order, is_active)
|
||||
SELECT
|
||||
sm.id,
|
||||
qs.id,
|
||||
qs.intro_video_url,
|
||||
COALESCE(qs.display_order, 0),
|
||||
(qs.status != 'ARCHIVED')
|
||||
FROM question_sets qs
|
||||
JOIN sub_modules sm
|
||||
ON qs.owner_type = 'SUB_MODULE'
|
||||
AND qs.owner_id = sm.id
|
||||
WHERE qs.set_type = 'PRACTICE'
|
||||
ON CONFLICT (question_set_id) DO NOTHING;
|
||||
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
ALTER TABLE IF EXISTS sub_module_lessons
|
||||
RENAME TO sub_module_practices;
|
||||
|
||||
ALTER INDEX IF EXISTS idx_sub_module_lessons_sub_module_id
|
||||
RENAME TO idx_sub_module_practices_sub_module_id;
|
||||
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
ALTER TABLE IF EXISTS sub_module_practices
|
||||
RENAME TO sub_module_lessons;
|
||||
|
||||
ALTER INDEX IF EXISTS idx_sub_module_practices_sub_module_id
|
||||
RENAME TO idx_sub_module_lessons_sub_module_id;
|
||||
|
||||
4
db/migrations/000032_add_sub_module_practices.down.sql
Normal file
4
db/migrations/000032_add_sub_module_practices.down.sql
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
DROP INDEX IF EXISTS idx_sub_module_practices_sub_module_id;
|
||||
|
||||
DROP TABLE IF EXISTS sub_module_practices;
|
||||
|
||||
17
db/migrations/000032_add_sub_module_practices.up.sql
Normal file
17
db/migrations/000032_add_sub_module_practices.up.sql
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
CREATE TABLE IF NOT EXISTS sub_module_practices (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
sub_module_id BIGINT NOT NULL REFERENCES sub_modules(id) ON DELETE CASCADE,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
thumbnail TEXT,
|
||||
intro_video_url TEXT,
|
||||
question_set_id BIGINT NOT NULL REFERENCES question_sets(id) ON DELETE CASCADE,
|
||||
display_order INT NOT NULL DEFAULT 0,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(question_set_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_sub_module_practices_sub_module_id
|
||||
ON sub_module_practices(sub_module_id);
|
||||
|
||||
195
db/query/hierarchy.sql
Normal file
195
db/query/hierarchy.sql
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
-- name: GetCoursesWithHierarchy :many
|
||||
SELECT
|
||||
cc.id AS category_id,
|
||||
cc.name AS category_name,
|
||||
csc.id AS sub_category_id,
|
||||
csc.name AS sub_category_name,
|
||||
c.id AS course_id,
|
||||
c.title AS course_title
|
||||
FROM course_categories cc
|
||||
LEFT JOIN course_sub_categories csc ON csc.category_id = cc.id AND csc.is_active = TRUE
|
||||
LEFT JOIN courses c ON c.sub_category_id = csc.id AND c.is_active = TRUE
|
||||
WHERE cc.is_active = TRUE
|
||||
ORDER BY cc.id, csc.display_order, csc.id, c.id;
|
||||
|
||||
-- name: GetLevelsByCourseID :many
|
||||
SELECT *
|
||||
FROM levels
|
||||
WHERE course_id = $1
|
||||
AND is_active = TRUE
|
||||
ORDER BY display_order ASC, id ASC;
|
||||
|
||||
-- name: GetModulesByLevelID :many
|
||||
SELECT *
|
||||
FROM modules
|
||||
WHERE level_id = $1
|
||||
AND is_active = TRUE
|
||||
ORDER BY display_order ASC, id ASC;
|
||||
|
||||
-- name: GetSubModulesByModuleID :many
|
||||
SELECT *
|
||||
FROM sub_modules
|
||||
WHERE module_id = $1
|
||||
AND is_active = TRUE
|
||||
ORDER BY display_order ASC, id ASC;
|
||||
|
||||
-- name: GetSubModuleVideos :many
|
||||
SELECT *
|
||||
FROM sub_module_videos
|
||||
WHERE sub_module_id = $1
|
||||
AND status != 'ARCHIVED'
|
||||
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
|
||||
ORDER BY smp.display_order ASC, smp.id ASC;
|
||||
|
||||
-- name: GetSubModulePractices :many
|
||||
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.sub_module_id = $1
|
||||
AND smp.is_active = TRUE
|
||||
ORDER BY smp.display_order ASC, smp.id ASC;
|
||||
|
||||
-- name: GetFullHierarchyByCourseID :many
|
||||
SELECT
|
||||
c.id AS course_id,
|
||||
c.title AS course_title,
|
||||
l.id AS level_id,
|
||||
l.cefr_level,
|
||||
m.id AS module_id,
|
||||
m.title AS module_title,
|
||||
sm.id AS sub_module_id,
|
||||
sm.title AS sub_module_title
|
||||
FROM courses c
|
||||
LEFT JOIN levels l ON l.course_id = c.id AND l.is_active = TRUE
|
||||
LEFT JOIN modules m ON m.level_id = l.id AND m.is_active = TRUE
|
||||
LEFT JOIN sub_modules sm ON sm.module_id = m.id AND sm.is_active = TRUE
|
||||
WHERE c.id = $1
|
||||
ORDER BY l.display_order, l.id, m.display_order, m.id, sm.display_order, sm.id;
|
||||
|
||||
-- name: CreateCourseSubCategory :one
|
||||
INSERT INTO course_sub_categories (
|
||||
category_id,
|
||||
name,
|
||||
description,
|
||||
display_order,
|
||||
is_active
|
||||
)
|
||||
VALUES ($1, $2, $3, COALESCE($4, 0), COALESCE($5, TRUE))
|
||||
RETURNING *;
|
||||
|
||||
-- name: CreateLevel :one
|
||||
INSERT INTO levels (
|
||||
course_id,
|
||||
cefr_level,
|
||||
display_order,
|
||||
is_active
|
||||
)
|
||||
VALUES ($1, $2, COALESCE($3, 0), COALESCE($4, TRUE))
|
||||
RETURNING *;
|
||||
|
||||
-- name: CreateModule :one
|
||||
INSERT INTO modules (
|
||||
level_id,
|
||||
title,
|
||||
description,
|
||||
display_order,
|
||||
is_active
|
||||
)
|
||||
VALUES ($1, $2, $3, COALESCE($4, 0), COALESCE($5, TRUE))
|
||||
RETURNING *;
|
||||
|
||||
-- name: CreateSubModule :one
|
||||
INSERT INTO sub_modules (
|
||||
module_id,
|
||||
title,
|
||||
description,
|
||||
display_order,
|
||||
is_active
|
||||
)
|
||||
VALUES ($1, $2, $3, COALESCE($4, 0), COALESCE($5, TRUE))
|
||||
RETURNING *;
|
||||
|
||||
-- name: CreateSubModuleVideo :one
|
||||
INSERT INTO sub_module_videos (
|
||||
sub_module_id,
|
||||
title,
|
||||
description,
|
||||
video_url,
|
||||
duration,
|
||||
resolution,
|
||||
is_published,
|
||||
publish_date,
|
||||
visibility,
|
||||
instructor_id,
|
||||
thumbnail,
|
||||
display_order,
|
||||
status,
|
||||
vimeo_id,
|
||||
vimeo_embed_url,
|
||||
vimeo_player_html,
|
||||
vimeo_status,
|
||||
video_host_provider
|
||||
)
|
||||
VALUES (
|
||||
$1, $2, $3, $4, $5, $6,
|
||||
COALESCE($7, FALSE), $8, $9, $10, $11,
|
||||
COALESCE($12, 0), COALESCE($13, 'DRAFT'),
|
||||
$14, $15, $16, $17, COALESCE($18, 'DIRECT')
|
||||
)
|
||||
RETURNING *;
|
||||
|
||||
-- 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 *;
|
||||
|
||||
-- name: CreateSubModulePractice :one
|
||||
INSERT INTO sub_module_practices (
|
||||
sub_module_id,
|
||||
title,
|
||||
description,
|
||||
thumbnail,
|
||||
intro_video_url,
|
||||
question_set_id,
|
||||
display_order,
|
||||
is_active
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, COALESCE($7, 0), COALESCE($8, TRUE))
|
||||
RETURNING *;
|
||||
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
-- name: GetFullLearningTree :many
|
||||
SELECT
|
||||
c.id AS course_id,
|
||||
c.title AS course_title,
|
||||
sc.id AS sub_course_id,
|
||||
sc.title AS sub_course_title,
|
||||
sc.level AS sub_course_level,
|
||||
sc.sub_level AS sub_course_sub_level
|
||||
FROM courses c
|
||||
LEFT JOIN sub_courses sc ON sc.course_id = c.id AND sc.is_active = true
|
||||
WHERE c.is_active = true
|
||||
ORDER BY c.id, sc.display_order, sc.id;
|
||||
|
||||
-- name: GetCourseLearningPath :many
|
||||
SELECT
|
||||
c.id AS course_id,
|
||||
c.title AS course_title,
|
||||
c.description AS course_description,
|
||||
c.thumbnail AS course_thumbnail,
|
||||
c.intro_video_url AS course_intro_video_url,
|
||||
cc.id AS category_id,
|
||||
cc.name AS category_name,
|
||||
sc.id AS sub_course_id,
|
||||
sc.title AS sub_course_title,
|
||||
sc.description AS sub_course_description,
|
||||
sc.thumbnail AS sub_course_thumbnail,
|
||||
sc.display_order AS sub_course_display_order,
|
||||
sc.level AS sub_course_level,
|
||||
sc.sub_level AS sub_course_sub_level,
|
||||
(SELECT COUNT(*) FROM sub_course_prerequisites WHERE sub_course_id = sc.id) AS prerequisite_count,
|
||||
(SELECT COUNT(*) FROM sub_course_videos WHERE sub_course_id = sc.id AND status = 'PUBLISHED') AS video_count,
|
||||
(SELECT COUNT(*) FROM question_sets WHERE set_type = 'PRACTICE' AND owner_type = 'SUB_COURSE' AND owner_id = sc.id AND status = 'PUBLISHED') AS practice_count
|
||||
FROM courses c
|
||||
JOIN course_categories cc ON cc.id = c.category_id
|
||||
LEFT JOIN sub_courses sc ON sc.course_id = c.id AND sc.is_active = true
|
||||
WHERE c.id = $1
|
||||
ORDER BY sc.display_order, sc.id;
|
||||
|
||||
-- name: GetSubCourseVideosForLearningPath :many
|
||||
SELECT id, title, description, video_url, duration, resolution, display_order,
|
||||
vimeo_id, vimeo_embed_url, video_host_provider
|
||||
FROM sub_course_videos
|
||||
WHERE sub_course_id = $1 AND status = 'PUBLISHED'
|
||||
ORDER BY display_order, id;
|
||||
|
||||
-- name: GetSubCoursePracticesForLearningPath :many
|
||||
SELECT id, title, description, persona, status, intro_video_url,
|
||||
(SELECT COUNT(*) FROM question_set_items WHERE set_id = qs.id) AS question_count
|
||||
FROM question_sets qs
|
||||
WHERE qs.owner_type = 'SUB_COURSE' AND qs.owner_id = $1
|
||||
AND qs.set_type = 'PRACTICE' AND qs.status = 'PUBLISHED'
|
||||
ORDER BY qs.display_order ASC, qs.created_at;
|
||||
|
||||
-- name: GetSubCoursePrerequisitesForLearningPath :many
|
||||
SELECT p.prerequisite_sub_course_id, sc.title, sc.level, sc.sub_level
|
||||
FROM sub_course_prerequisites p
|
||||
JOIN sub_courses sc ON sc.id = p.prerequisite_sub_course_id
|
||||
WHERE p.sub_course_id = $1
|
||||
ORDER BY sc.display_order;
|
||||
55
db/query/practice_progress.sql
Normal file
55
db/query/practice_progress.sql
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
-- name: GetFirstIncompletePreviousPractice :one
|
||||
WITH target AS (
|
||||
SELECT id, owner_type, owner_id, COALESCE(display_order, 0) AS display_order
|
||||
FROM question_sets
|
||||
WHERE id = @question_set_id::BIGINT
|
||||
AND set_type = 'PRACTICE'
|
||||
AND status = 'PUBLISHED'
|
||||
),
|
||||
candidates AS (
|
||||
SELECT qs.id, qs.title, COALESCE(qs.display_order, 0) AS display_order
|
||||
FROM question_sets qs
|
||||
JOIN target t
|
||||
ON qs.owner_type = t.owner_type
|
||||
AND qs.owner_id = t.owner_id
|
||||
WHERE qs.set_type = 'PRACTICE'
|
||||
AND qs.status = 'PUBLISHED'
|
||||
AND COALESCE(qs.display_order, 0) < t.display_order
|
||||
)
|
||||
SELECT c.id, c.title, c.display_order
|
||||
FROM candidates c
|
||||
LEFT JOIN user_practice_progress upp
|
||||
ON upp.question_set_id = c.id
|
||||
AND upp.user_id = @user_id::BIGINT
|
||||
AND upp.completed_at IS NOT NULL
|
||||
WHERE upp.id IS NULL
|
||||
ORDER BY c.display_order ASC, c.id ASC
|
||||
LIMIT 1;
|
||||
|
||||
-- name: MarkPracticeCompleted :execrows
|
||||
INSERT INTO user_practice_progress (
|
||||
user_id,
|
||||
sub_course_id,
|
||||
question_set_id,
|
||||
completed_at,
|
||||
updated_at
|
||||
)
|
||||
SELECT
|
||||
@user_id::BIGINT,
|
||||
CASE
|
||||
WHEN qs.owner_type = 'SUB_COURSE' THEN qs.owner_id
|
||||
WHEN qs.owner_type = 'SUB_MODULE' THEN sm.legacy_sub_course_id
|
||||
ELSE NULL
|
||||
END,
|
||||
qs.id,
|
||||
CURRENT_TIMESTAMP,
|
||||
CURRENT_TIMESTAMP
|
||||
FROM question_sets qs
|
||||
LEFT JOIN sub_modules sm
|
||||
ON qs.owner_type = 'SUB_MODULE'
|
||||
AND qs.owner_id = sm.id
|
||||
WHERE qs.id = @question_set_id::BIGINT
|
||||
ON CONFLICT (user_id, question_set_id) DO UPDATE
|
||||
SET completed_at = EXCLUDED.completed_at,
|
||||
updated_at = EXCLUDED.updated_at;
|
||||
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
-- name: AddSubCoursePrerequisite :one
|
||||
INSERT INTO sub_course_prerequisites (sub_course_id, prerequisite_sub_course_id)
|
||||
VALUES ($1, $2)
|
||||
RETURNING *;
|
||||
|
||||
-- name: RemoveSubCoursePrerequisite :exec
|
||||
DELETE FROM sub_course_prerequisites
|
||||
WHERE sub_course_id = $1 AND prerequisite_sub_course_id = $2;
|
||||
|
||||
-- name: GetSubCoursePrerequisites :many
|
||||
SELECT
|
||||
p.id,
|
||||
p.sub_course_id,
|
||||
p.prerequisite_sub_course_id,
|
||||
p.created_at,
|
||||
sc.title AS prerequisite_title,
|
||||
sc.level AS prerequisite_level,
|
||||
sc.display_order AS prerequisite_display_order
|
||||
FROM sub_course_prerequisites p
|
||||
JOIN sub_courses sc ON sc.id = p.prerequisite_sub_course_id
|
||||
WHERE p.sub_course_id = $1
|
||||
ORDER BY sc.display_order;
|
||||
|
||||
-- name: GetSubCourseDependents :many
|
||||
SELECT
|
||||
p.id,
|
||||
p.sub_course_id,
|
||||
p.prerequisite_sub_course_id,
|
||||
p.created_at,
|
||||
sc.title AS dependent_title,
|
||||
sc.level AS dependent_level
|
||||
FROM sub_course_prerequisites p
|
||||
JOIN sub_courses sc ON sc.id = p.sub_course_id
|
||||
WHERE p.prerequisite_sub_course_id = $1
|
||||
ORDER BY sc.display_order;
|
||||
|
||||
-- name: CountUnmetPrerequisites :one
|
||||
SELECT COUNT(*)::bigint AS unmet_count
|
||||
FROM sub_course_prerequisites p
|
||||
WHERE p.sub_course_id = $1
|
||||
AND p.prerequisite_sub_course_id NOT IN (
|
||||
SELECT usp.sub_course_id
|
||||
FROM user_sub_course_progress usp
|
||||
WHERE usp.user_id = $2
|
||||
AND usp.status = 'COMPLETED'
|
||||
);
|
||||
|
||||
-- name: DeleteAllPrerequisitesForSubCourse :exec
|
||||
DELETE FROM sub_course_prerequisites
|
||||
WHERE sub_course_id = $1;
|
||||
|
|
@ -1,122 +0,0 @@
|
|||
-- name: CreateSubCourseVideo :one
|
||||
INSERT INTO sub_course_videos (
|
||||
sub_course_id,
|
||||
title,
|
||||
description,
|
||||
video_url,
|
||||
duration,
|
||||
resolution,
|
||||
instructor_id,
|
||||
thumbnail,
|
||||
visibility,
|
||||
display_order,
|
||||
status,
|
||||
vimeo_id,
|
||||
vimeo_embed_url,
|
||||
vimeo_player_html,
|
||||
vimeo_status,
|
||||
video_host_provider
|
||||
)
|
||||
VALUES (
|
||||
$1, $2, $3, $4, $5, $6,
|
||||
$7, $8, $9,
|
||||
COALESCE($10, 0),
|
||||
COALESCE($11, 'DRAFT'),
|
||||
$12, $13, $14,
|
||||
COALESCE($15, 'pending'),
|
||||
COALESCE($16, 'DIRECT')
|
||||
)
|
||||
RETURNING *;
|
||||
|
||||
-- name: GetSubCourseVideoByID :one
|
||||
SELECT *
|
||||
FROM sub_course_videos
|
||||
WHERE id = $1;
|
||||
|
||||
-- name: GetVideosBySubCourse :many
|
||||
SELECT
|
||||
COUNT(*) OVER () AS total_count,
|
||||
id,
|
||||
sub_course_id,
|
||||
title,
|
||||
description,
|
||||
video_url,
|
||||
duration,
|
||||
resolution,
|
||||
is_published,
|
||||
publish_date,
|
||||
visibility,
|
||||
instructor_id,
|
||||
thumbnail,
|
||||
display_order,
|
||||
status,
|
||||
vimeo_id,
|
||||
vimeo_embed_url,
|
||||
vimeo_player_html,
|
||||
vimeo_status,
|
||||
video_host_provider
|
||||
FROM sub_course_videos
|
||||
WHERE sub_course_id = $1
|
||||
AND status != 'ARCHIVED'
|
||||
ORDER BY display_order ASC, id ASC;
|
||||
|
||||
-- name: GetPublishedVideosBySubCourse :many
|
||||
SELECT *
|
||||
FROM sub_course_videos
|
||||
WHERE sub_course_id = $1
|
||||
AND status = 'PUBLISHED'
|
||||
ORDER BY display_order ASC, publish_date ASC;
|
||||
|
||||
-- name: PublishSubCourseVideo :exec
|
||||
UPDATE sub_course_videos
|
||||
SET
|
||||
is_published = true,
|
||||
publish_date = CURRENT_TIMESTAMP,
|
||||
status = 'PUBLISHED'
|
||||
WHERE id = $1;
|
||||
|
||||
-- name: UpdateSubCourseVideo :exec
|
||||
UPDATE sub_course_videos
|
||||
SET
|
||||
title = COALESCE($1, title),
|
||||
description = COALESCE($2, description),
|
||||
video_url = COALESCE($3, video_url),
|
||||
duration = COALESCE($4, duration),
|
||||
resolution = COALESCE($5, resolution),
|
||||
visibility = COALESCE($6, visibility),
|
||||
thumbnail = COALESCE($7, thumbnail),
|
||||
display_order = COALESCE($8, display_order),
|
||||
status = COALESCE($9, status),
|
||||
vimeo_id = COALESCE($10, vimeo_id),
|
||||
vimeo_embed_url = COALESCE($11, vimeo_embed_url),
|
||||
vimeo_player_html = COALESCE($12, vimeo_player_html),
|
||||
vimeo_status = COALESCE($13, vimeo_status),
|
||||
video_host_provider = COALESCE($14, video_host_provider)
|
||||
WHERE id = $15;
|
||||
|
||||
-- name: UpdateVimeoStatus :exec
|
||||
UPDATE sub_course_videos
|
||||
SET
|
||||
vimeo_status = $1
|
||||
WHERE id = $2;
|
||||
|
||||
-- name: GetVideosByVimeoID :one
|
||||
SELECT * FROM sub_course_videos
|
||||
WHERE vimeo_id = $1;
|
||||
|
||||
-- name: ArchiveSubCourseVideo :exec
|
||||
UPDATE sub_course_videos
|
||||
SET status = 'ARCHIVED'
|
||||
WHERE id = $1;
|
||||
|
||||
-- name: DeleteSubCourseVideo :exec
|
||||
DELETE FROM sub_course_videos
|
||||
WHERE id = $1;
|
||||
|
||||
-- name: ReorderSubCourseVideos :exec
|
||||
UPDATE sub_course_videos
|
||||
SET display_order = bulk.position
|
||||
FROM (
|
||||
SELECT unnest(@ids::BIGINT[]) AS id, unnest(@positions::INT[]) AS position
|
||||
) AS bulk
|
||||
WHERE sub_course_videos.id = bulk.id;
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
-- name: CreateSubCourse :one
|
||||
INSERT INTO sub_courses (
|
||||
course_id,
|
||||
title,
|
||||
description,
|
||||
thumbnail,
|
||||
display_order,
|
||||
level,
|
||||
sub_level,
|
||||
is_active
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, COALESCE($5, 0), $6, $7, COALESCE($8, true))
|
||||
RETURNING *;
|
||||
|
||||
-- name: GetSubCourseByID :one
|
||||
SELECT *
|
||||
FROM sub_courses
|
||||
WHERE id = $1;
|
||||
|
||||
-- name: GetSubCoursesByCourse :many
|
||||
SELECT
|
||||
COUNT(*) OVER () AS total_count,
|
||||
id,
|
||||
course_id,
|
||||
title,
|
||||
description,
|
||||
thumbnail,
|
||||
display_order,
|
||||
level,
|
||||
sub_level,
|
||||
is_active
|
||||
FROM sub_courses
|
||||
WHERE course_id = $1
|
||||
ORDER BY display_order ASC, id ASC;
|
||||
|
||||
-- name: ListSubCoursesByCourse :many
|
||||
SELECT
|
||||
id,
|
||||
course_id,
|
||||
title,
|
||||
description,
|
||||
thumbnail,
|
||||
display_order,
|
||||
level,
|
||||
sub_level,
|
||||
is_active
|
||||
FROM sub_courses
|
||||
WHERE course_id = $1
|
||||
AND is_active = TRUE
|
||||
ORDER BY display_order ASC, id ASC;
|
||||
|
||||
-- name: ListActiveSubCourses :many
|
||||
SELECT
|
||||
id,
|
||||
course_id,
|
||||
title,
|
||||
description,
|
||||
thumbnail,
|
||||
display_order,
|
||||
level,
|
||||
sub_level,
|
||||
is_active
|
||||
FROM sub_courses
|
||||
WHERE is_active = TRUE
|
||||
ORDER BY display_order ASC;
|
||||
|
||||
-- name: UpdateSubCourse :exec
|
||||
UPDATE sub_courses
|
||||
SET
|
||||
title = COALESCE($1, title),
|
||||
description = COALESCE($2, description),
|
||||
thumbnail = COALESCE($3, thumbnail),
|
||||
display_order = COALESCE($4, display_order),
|
||||
level = COALESCE($5, level),
|
||||
sub_level = COALESCE($6, sub_level),
|
||||
is_active = COALESCE($7, is_active)
|
||||
WHERE id = $8;
|
||||
|
||||
-- name: DeleteSubCourse :one
|
||||
DELETE FROM sub_courses
|
||||
WHERE id = $1
|
||||
RETURNING *;
|
||||
|
||||
-- name: DeactivateSubCourse :exec
|
||||
UPDATE sub_courses
|
||||
SET is_active = FALSE
|
||||
WHERE id = $1;
|
||||
|
||||
-- name: ReorderSubCourses :exec
|
||||
UPDATE sub_courses
|
||||
SET display_order = bulk.position
|
||||
FROM (
|
||||
SELECT unnest(@ids::BIGINT[]) AS id, unnest(@positions::INT[]) AS position
|
||||
) AS bulk
|
||||
WHERE sub_courses.id = bulk.id;
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
-- name: MarkPracticeCompleted :one
|
||||
INSERT INTO user_practice_progress (
|
||||
user_id,
|
||||
sub_course_id,
|
||||
question_set_id,
|
||||
completed_at,
|
||||
updated_at
|
||||
)
|
||||
SELECT
|
||||
@user_id::BIGINT,
|
||||
qs.owner_id::BIGINT,
|
||||
qs.id,
|
||||
CURRENT_TIMESTAMP,
|
||||
CURRENT_TIMESTAMP
|
||||
FROM question_sets qs
|
||||
WHERE qs.id = @question_set_id::BIGINT
|
||||
AND qs.set_type = 'PRACTICE'
|
||||
AND qs.owner_type = 'SUB_COURSE'
|
||||
AND qs.status = 'PUBLISHED'
|
||||
ON CONFLICT (user_id, question_set_id)
|
||||
DO UPDATE SET
|
||||
completed_at = CURRENT_TIMESTAMP,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
RETURNING *;
|
||||
|
||||
-- name: GetFirstIncompletePreviousPractice :one
|
||||
SELECT
|
||||
p.id,
|
||||
p.title,
|
||||
p.display_order
|
||||
FROM question_sets target
|
||||
JOIN question_sets p
|
||||
ON p.owner_type = 'SUB_COURSE'
|
||||
AND p.owner_id = target.owner_id
|
||||
AND p.set_type = 'PRACTICE'
|
||||
AND p.status = 'PUBLISHED'
|
||||
AND (
|
||||
p.display_order < target.display_order OR
|
||||
(p.display_order = target.display_order AND p.id < target.id)
|
||||
)
|
||||
LEFT JOIN user_practice_progress upp
|
||||
ON upp.question_set_id = p.id
|
||||
AND upp.user_id = @user_id::BIGINT
|
||||
AND upp.completed_at IS NOT NULL
|
||||
WHERE target.id = @question_set_id::BIGINT
|
||||
AND target.set_type = 'PRACTICE'
|
||||
AND target.owner_type = 'SUB_COURSE'
|
||||
AND target.status = 'PUBLISHED'
|
||||
AND upp.question_set_id IS NULL
|
||||
ORDER BY p.display_order ASC, p.id ASC
|
||||
LIMIT 1;
|
||||
|
|
@ -1,78 +0,0 @@
|
|||
-- name: StartSubCourseProgress :one
|
||||
INSERT INTO user_sub_course_progress (user_id, sub_course_id)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT (user_id, sub_course_id) DO NOTHING
|
||||
RETURNING *;
|
||||
|
||||
-- name: UpdateSubCourseProgress :exec
|
||||
UPDATE user_sub_course_progress
|
||||
SET
|
||||
progress_percentage = $1,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE user_id = $2 AND sub_course_id = $3;
|
||||
|
||||
-- name: CompleteSubCourse :exec
|
||||
UPDATE user_sub_course_progress
|
||||
SET
|
||||
status = 'COMPLETED',
|
||||
progress_percentage = 100,
|
||||
completed_at = CURRENT_TIMESTAMP,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE user_id = $1 AND sub_course_id = $2;
|
||||
|
||||
-- name: GetUserSubCourseProgress :one
|
||||
SELECT * FROM user_sub_course_progress
|
||||
WHERE user_id = $1 AND sub_course_id = $2;
|
||||
|
||||
-- name: GetUserCourseProgress :many
|
||||
SELECT
|
||||
usp.id,
|
||||
usp.user_id,
|
||||
usp.sub_course_id,
|
||||
usp.status,
|
||||
usp.progress_percentage,
|
||||
usp.started_at,
|
||||
usp.completed_at,
|
||||
usp.created_at,
|
||||
usp.updated_at,
|
||||
sc.title AS sub_course_title,
|
||||
sc.level AS sub_course_level,
|
||||
sc.display_order AS sub_course_display_order
|
||||
FROM user_sub_course_progress usp
|
||||
JOIN sub_courses sc ON sc.id = usp.sub_course_id
|
||||
WHERE usp.user_id = $1 AND sc.course_id = $2
|
||||
ORDER BY sc.display_order;
|
||||
|
||||
-- name: GetSubCoursesWithProgressByCourse :many
|
||||
SELECT
|
||||
sc.id AS sub_course_id,
|
||||
sc.title,
|
||||
sc.description,
|
||||
sc.thumbnail,
|
||||
sc.display_order,
|
||||
sc.level,
|
||||
sc.is_active,
|
||||
COALESCE(usp.status, 'NOT_STARTED') AS progress_status,
|
||||
COALESCE(usp.progress_percentage, 0)::smallint AS progress_percentage,
|
||||
usp.started_at,
|
||||
usp.completed_at,
|
||||
(SELECT COUNT(*)::bigint
|
||||
FROM sub_course_prerequisites p
|
||||
WHERE p.sub_course_id = sc.id
|
||||
AND p.prerequisite_sub_course_id NOT IN (
|
||||
SELECT usp2.sub_course_id
|
||||
FROM user_sub_course_progress usp2
|
||||
WHERE usp2.user_id = $1
|
||||
AND usp2.status = 'COMPLETED'
|
||||
)
|
||||
) AS unmet_prerequisites_count
|
||||
FROM sub_courses sc
|
||||
LEFT JOIN user_sub_course_progress usp
|
||||
ON usp.sub_course_id = sc.id AND usp.user_id = $1
|
||||
WHERE sc.course_id = $2
|
||||
AND sc.is_active = true
|
||||
ORDER BY sc.display_order;
|
||||
|
||||
-- name: DeleteUserSubCourseProgress :exec
|
||||
DELETE FROM user_sub_course_progress
|
||||
WHERE user_id = $1 AND sub_course_id = $2;
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
-- name: MarkVideoCompleted :one
|
||||
INSERT INTO user_sub_course_video_progress (
|
||||
user_id,
|
||||
sub_course_id,
|
||||
video_id,
|
||||
completed_at,
|
||||
updated_at
|
||||
)
|
||||
SELECT
|
||||
@user_id::BIGINT,
|
||||
v.sub_course_id,
|
||||
v.id,
|
||||
CURRENT_TIMESTAMP,
|
||||
CURRENT_TIMESTAMP
|
||||
FROM sub_course_videos v
|
||||
WHERE v.id = @video_id::BIGINT
|
||||
AND v.status = 'PUBLISHED'
|
||||
ON CONFLICT (user_id, video_id)
|
||||
DO UPDATE SET
|
||||
completed_at = CURRENT_TIMESTAMP,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
RETURNING *;
|
||||
|
||||
-- name: GetFirstIncompletePreviousVideo :one
|
||||
SELECT
|
||||
v.id,
|
||||
v.title,
|
||||
v.display_order
|
||||
FROM sub_course_videos target
|
||||
JOIN sub_course_videos v
|
||||
ON v.sub_course_id = target.sub_course_id
|
||||
AND v.status = 'PUBLISHED'
|
||||
AND (
|
||||
v.display_order < target.display_order OR
|
||||
(v.display_order = target.display_order AND v.id < target.id)
|
||||
)
|
||||
LEFT JOIN user_sub_course_video_progress p
|
||||
ON p.video_id = v.id
|
||||
AND p.user_id = @user_id::BIGINT
|
||||
AND p.completed_at IS NOT NULL
|
||||
WHERE target.id = @video_id::BIGINT
|
||||
AND p.video_id IS NULL
|
||||
ORDER BY v.display_order ASC, v.id ASC
|
||||
LIMIT 1;
|
||||
2642
docs/docs.go
2642
docs/docs.go
File diff suppressed because it is too large
Load Diff
2642
docs/swagger.json
2642
docs/swagger.json
File diff suppressed because it is too large
Load Diff
1851
docs/swagger.yaml
1851
docs/swagger.yaml
File diff suppressed because it is too large
Load Diff
|
|
@ -21,7 +21,7 @@ INSERT INTO courses (
|
|||
is_active
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, COALESCE($6, true))
|
||||
RETURNING id, category_id, title, description, is_active, thumbnail, intro_video_url, display_order
|
||||
RETURNING id, category_id, title, description, is_active, thumbnail, intro_video_url, display_order, sub_category_id
|
||||
`
|
||||
|
||||
type CreateCourseParams struct {
|
||||
|
|
@ -52,6 +52,7 @@ func (q *Queries) CreateCourse(ctx context.Context, arg CreateCourseParams) (Cou
|
|||
&i.Thumbnail,
|
||||
&i.IntroVideoUrl,
|
||||
&i.DisplayOrder,
|
||||
&i.SubCategoryID,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -67,7 +68,7 @@ func (q *Queries) DeleteCourse(ctx context.Context, id int64) error {
|
|||
}
|
||||
|
||||
const GetCourseByID = `-- name: GetCourseByID :one
|
||||
SELECT id, category_id, title, description, is_active, thumbnail, intro_video_url, display_order
|
||||
SELECT id, category_id, title, description, is_active, thumbnail, intro_video_url, display_order, sub_category_id
|
||||
FROM courses
|
||||
WHERE id = $1
|
||||
`
|
||||
|
|
@ -84,6 +85,7 @@ func (q *Queries) GetCourseByID(ctx context.Context, id int64) (Course, error) {
|
|||
&i.Thumbnail,
|
||||
&i.IntroVideoUrl,
|
||||
&i.DisplayOrder,
|
||||
&i.SubCategoryID,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
|
|||
766
gen/db/hierarchy.sql.go
Normal file
766
gen/db/hierarchy.sql.go
Normal file
|
|
@ -0,0 +1,766 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
// source: hierarchy.sql
|
||||
|
||||
package dbgen
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"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,
|
||||
name,
|
||||
description,
|
||||
display_order,
|
||||
is_active
|
||||
)
|
||||
VALUES ($1, $2, $3, COALESCE($4, 0), COALESCE($5, TRUE))
|
||||
RETURNING id, category_id, name, description, is_active, display_order, created_at
|
||||
`
|
||||
|
||||
type CreateCourseSubCategoryParams struct {
|
||||
CategoryID int64 `json:"category_id"`
|
||||
Name string `json:"name"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
Column4 interface{} `json:"column_4"`
|
||||
Column5 interface{} `json:"column_5"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateCourseSubCategory(ctx context.Context, arg CreateCourseSubCategoryParams) (CourseSubCategory, error) {
|
||||
row := q.db.QueryRow(ctx, CreateCourseSubCategory,
|
||||
arg.CategoryID,
|
||||
arg.Name,
|
||||
arg.Description,
|
||||
arg.Column4,
|
||||
arg.Column5,
|
||||
)
|
||||
var i CourseSubCategory
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.CategoryID,
|
||||
&i.Name,
|
||||
&i.Description,
|
||||
&i.IsActive,
|
||||
&i.DisplayOrder,
|
||||
&i.CreatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const CreateLevel = `-- name: CreateLevel :one
|
||||
INSERT INTO levels (
|
||||
course_id,
|
||||
cefr_level,
|
||||
display_order,
|
||||
is_active
|
||||
)
|
||||
VALUES ($1, $2, COALESCE($3, 0), COALESCE($4, TRUE))
|
||||
RETURNING id, course_id, cefr_level, display_order, is_active, created_at
|
||||
`
|
||||
|
||||
type CreateLevelParams struct {
|
||||
CourseID int64 `json:"course_id"`
|
||||
CefrLevel string `json:"cefr_level"`
|
||||
Column3 interface{} `json:"column_3"`
|
||||
Column4 interface{} `json:"column_4"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateLevel(ctx context.Context, arg CreateLevelParams) (Level, error) {
|
||||
row := q.db.QueryRow(ctx, CreateLevel,
|
||||
arg.CourseID,
|
||||
arg.CefrLevel,
|
||||
arg.Column3,
|
||||
arg.Column4,
|
||||
)
|
||||
var i Level
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.CourseID,
|
||||
&i.CefrLevel,
|
||||
&i.DisplayOrder,
|
||||
&i.IsActive,
|
||||
&i.CreatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const CreateModule = `-- name: CreateModule :one
|
||||
INSERT INTO modules (
|
||||
level_id,
|
||||
title,
|
||||
description,
|
||||
display_order,
|
||||
is_active
|
||||
)
|
||||
VALUES ($1, $2, $3, COALESCE($4, 0), COALESCE($5, TRUE))
|
||||
RETURNING id, level_id, title, description, display_order, is_active, created_at
|
||||
`
|
||||
|
||||
type CreateModuleParams struct {
|
||||
LevelID int64 `json:"level_id"`
|
||||
Title string `json:"title"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
Column4 interface{} `json:"column_4"`
|
||||
Column5 interface{} `json:"column_5"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateModule(ctx context.Context, arg CreateModuleParams) (Module, error) {
|
||||
row := q.db.QueryRow(ctx, CreateModule,
|
||||
arg.LevelID,
|
||||
arg.Title,
|
||||
arg.Description,
|
||||
arg.Column4,
|
||||
arg.Column5,
|
||||
)
|
||||
var i Module
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.LevelID,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.DisplayOrder,
|
||||
&i.IsActive,
|
||||
&i.CreatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const CreateSubModule = `-- name: CreateSubModule :one
|
||||
INSERT INTO sub_modules (
|
||||
module_id,
|
||||
title,
|
||||
description,
|
||||
display_order,
|
||||
is_active
|
||||
)
|
||||
VALUES ($1, $2, $3, COALESCE($4, 0), COALESCE($5, TRUE))
|
||||
RETURNING id, module_id, title, description, display_order, is_active, created_at, legacy_sub_course_id
|
||||
`
|
||||
|
||||
type CreateSubModuleParams struct {
|
||||
ModuleID int64 `json:"module_id"`
|
||||
Title string `json:"title"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
Column4 interface{} `json:"column_4"`
|
||||
Column5 interface{} `json:"column_5"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateSubModule(ctx context.Context, arg CreateSubModuleParams) (SubModule, error) {
|
||||
row := q.db.QueryRow(ctx, CreateSubModule,
|
||||
arg.ModuleID,
|
||||
arg.Title,
|
||||
arg.Description,
|
||||
arg.Column4,
|
||||
arg.Column5,
|
||||
)
|
||||
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 CreateSubModulePractice = `-- name: CreateSubModulePractice :one
|
||||
INSERT INTO sub_module_practices (
|
||||
sub_module_id,
|
||||
title,
|
||||
description,
|
||||
thumbnail,
|
||||
intro_video_url,
|
||||
question_set_id,
|
||||
display_order,
|
||||
is_active
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, COALESCE($7, 0), COALESCE($8, TRUE))
|
||||
RETURNING id, sub_module_id, title, description, thumbnail, intro_video_url, question_set_id, display_order, is_active, created_at
|
||||
`
|
||||
|
||||
type CreateSubModulePracticeParams struct {
|
||||
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"`
|
||||
Column7 interface{} `json:"column_7"`
|
||||
Column8 interface{} `json:"column_8"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateSubModulePractice(ctx context.Context, arg CreateSubModulePracticeParams) (SubModulePractice, error) {
|
||||
row := q.db.QueryRow(ctx, CreateSubModulePractice,
|
||||
arg.SubModuleID,
|
||||
arg.Title,
|
||||
arg.Description,
|
||||
arg.Thumbnail,
|
||||
arg.IntroVideoUrl,
|
||||
arg.QuestionSetID,
|
||||
arg.Column7,
|
||||
arg.Column8,
|
||||
)
|
||||
var i SubModulePractice
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.SubModuleID,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.Thumbnail,
|
||||
&i.IntroVideoUrl,
|
||||
&i.QuestionSetID,
|
||||
&i.DisplayOrder,
|
||||
&i.IsActive,
|
||||
&i.CreatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const CreateSubModuleVideo = `-- name: CreateSubModuleVideo :one
|
||||
INSERT INTO sub_module_videos (
|
||||
sub_module_id,
|
||||
title,
|
||||
description,
|
||||
video_url,
|
||||
duration,
|
||||
resolution,
|
||||
is_published,
|
||||
publish_date,
|
||||
visibility,
|
||||
instructor_id,
|
||||
thumbnail,
|
||||
display_order,
|
||||
status,
|
||||
vimeo_id,
|
||||
vimeo_embed_url,
|
||||
vimeo_player_html,
|
||||
vimeo_status,
|
||||
video_host_provider
|
||||
)
|
||||
VALUES (
|
||||
$1, $2, $3, $4, $5, $6,
|
||||
COALESCE($7, FALSE), $8, $9, $10, $11,
|
||||
COALESCE($12, 0), COALESCE($13, 'DRAFT'),
|
||||
$14, $15, $16, $17, COALESCE($18, 'DIRECT')
|
||||
)
|
||||
RETURNING id, sub_module_id, title, description, video_url, duration, resolution, is_published, publish_date, visibility, instructor_id, thumbnail, display_order, status, vimeo_id, vimeo_embed_url, vimeo_player_html, vimeo_status, video_host_provider, created_at
|
||||
`
|
||||
|
||||
type CreateSubModuleVideoParams struct {
|
||||
SubModuleID int64 `json:"sub_module_id"`
|
||||
Title string `json:"title"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
VideoUrl string `json:"video_url"`
|
||||
Duration pgtype.Int4 `json:"duration"`
|
||||
Resolution pgtype.Text `json:"resolution"`
|
||||
Column7 interface{} `json:"column_7"`
|
||||
PublishDate pgtype.Timestamptz `json:"publish_date"`
|
||||
Visibility pgtype.Text `json:"visibility"`
|
||||
InstructorID pgtype.Text `json:"instructor_id"`
|
||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||
Column12 interface{} `json:"column_12"`
|
||||
Column13 interface{} `json:"column_13"`
|
||||
VimeoID pgtype.Text `json:"vimeo_id"`
|
||||
VimeoEmbedUrl pgtype.Text `json:"vimeo_embed_url"`
|
||||
VimeoPlayerHtml pgtype.Text `json:"vimeo_player_html"`
|
||||
VimeoStatus pgtype.Text `json:"vimeo_status"`
|
||||
Column18 interface{} `json:"column_18"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateSubModuleVideo(ctx context.Context, arg CreateSubModuleVideoParams) (SubModuleVideo, error) {
|
||||
row := q.db.QueryRow(ctx, CreateSubModuleVideo,
|
||||
arg.SubModuleID,
|
||||
arg.Title,
|
||||
arg.Description,
|
||||
arg.VideoUrl,
|
||||
arg.Duration,
|
||||
arg.Resolution,
|
||||
arg.Column7,
|
||||
arg.PublishDate,
|
||||
arg.Visibility,
|
||||
arg.InstructorID,
|
||||
arg.Thumbnail,
|
||||
arg.Column12,
|
||||
arg.Column13,
|
||||
arg.VimeoID,
|
||||
arg.VimeoEmbedUrl,
|
||||
arg.VimeoPlayerHtml,
|
||||
arg.VimeoStatus,
|
||||
arg.Column18,
|
||||
)
|
||||
var i SubModuleVideo
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.SubModuleID,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.VideoUrl,
|
||||
&i.Duration,
|
||||
&i.Resolution,
|
||||
&i.IsPublished,
|
||||
&i.PublishDate,
|
||||
&i.Visibility,
|
||||
&i.InstructorID,
|
||||
&i.Thumbnail,
|
||||
&i.DisplayOrder,
|
||||
&i.Status,
|
||||
&i.VimeoID,
|
||||
&i.VimeoEmbedUrl,
|
||||
&i.VimeoPlayerHtml,
|
||||
&i.VimeoStatus,
|
||||
&i.VideoHostProvider,
|
||||
&i.CreatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const GetCoursesWithHierarchy = `-- name: GetCoursesWithHierarchy :many
|
||||
SELECT
|
||||
cc.id AS category_id,
|
||||
cc.name AS category_name,
|
||||
csc.id AS sub_category_id,
|
||||
csc.name AS sub_category_name,
|
||||
c.id AS course_id,
|
||||
c.title AS course_title
|
||||
FROM course_categories cc
|
||||
LEFT JOIN course_sub_categories csc ON csc.category_id = cc.id AND csc.is_active = TRUE
|
||||
LEFT JOIN courses c ON c.sub_category_id = csc.id AND c.is_active = TRUE
|
||||
WHERE cc.is_active = TRUE
|
||||
ORDER BY cc.id, csc.display_order, csc.id, c.id
|
||||
`
|
||||
|
||||
type GetCoursesWithHierarchyRow struct {
|
||||
CategoryID int64 `json:"category_id"`
|
||||
CategoryName string `json:"category_name"`
|
||||
SubCategoryID pgtype.Int8 `json:"sub_category_id"`
|
||||
SubCategoryName pgtype.Text `json:"sub_category_name"`
|
||||
CourseID pgtype.Int8 `json:"course_id"`
|
||||
CourseTitle pgtype.Text `json:"course_title"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetCoursesWithHierarchy(ctx context.Context) ([]GetCoursesWithHierarchyRow, error) {
|
||||
rows, err := q.db.Query(ctx, GetCoursesWithHierarchy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetCoursesWithHierarchyRow
|
||||
for rows.Next() {
|
||||
var i GetCoursesWithHierarchyRow
|
||||
if err := rows.Scan(
|
||||
&i.CategoryID,
|
||||
&i.CategoryName,
|
||||
&i.SubCategoryID,
|
||||
&i.SubCategoryName,
|
||||
&i.CourseID,
|
||||
&i.CourseTitle,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const GetFullHierarchyByCourseID = `-- name: GetFullHierarchyByCourseID :many
|
||||
SELECT
|
||||
c.id AS course_id,
|
||||
c.title AS course_title,
|
||||
l.id AS level_id,
|
||||
l.cefr_level,
|
||||
m.id AS module_id,
|
||||
m.title AS module_title,
|
||||
sm.id AS sub_module_id,
|
||||
sm.title AS sub_module_title
|
||||
FROM courses c
|
||||
LEFT JOIN levels l ON l.course_id = c.id AND l.is_active = TRUE
|
||||
LEFT JOIN modules m ON m.level_id = l.id AND m.is_active = TRUE
|
||||
LEFT JOIN sub_modules sm ON sm.module_id = m.id AND sm.is_active = TRUE
|
||||
WHERE c.id = $1
|
||||
ORDER BY l.display_order, l.id, m.display_order, m.id, sm.display_order, sm.id
|
||||
`
|
||||
|
||||
type GetFullHierarchyByCourseIDRow struct {
|
||||
CourseID int64 `json:"course_id"`
|
||||
CourseTitle string `json:"course_title"`
|
||||
LevelID pgtype.Int8 `json:"level_id"`
|
||||
CefrLevel pgtype.Text `json:"cefr_level"`
|
||||
ModuleID pgtype.Int8 `json:"module_id"`
|
||||
ModuleTitle pgtype.Text `json:"module_title"`
|
||||
SubModuleID pgtype.Int8 `json:"sub_module_id"`
|
||||
SubModuleTitle pgtype.Text `json:"sub_module_title"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetFullHierarchyByCourseID(ctx context.Context, id int64) ([]GetFullHierarchyByCourseIDRow, error) {
|
||||
rows, err := q.db.Query(ctx, GetFullHierarchyByCourseID, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetFullHierarchyByCourseIDRow
|
||||
for rows.Next() {
|
||||
var i GetFullHierarchyByCourseIDRow
|
||||
if err := rows.Scan(
|
||||
&i.CourseID,
|
||||
&i.CourseTitle,
|
||||
&i.LevelID,
|
||||
&i.CefrLevel,
|
||||
&i.ModuleID,
|
||||
&i.ModuleTitle,
|
||||
&i.SubModuleID,
|
||||
&i.SubModuleTitle,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const GetLevelsByCourseID = `-- name: GetLevelsByCourseID :many
|
||||
SELECT id, course_id, cefr_level, display_order, is_active, created_at
|
||||
FROM levels
|
||||
WHERE course_id = $1
|
||||
AND is_active = TRUE
|
||||
ORDER BY display_order ASC, id ASC
|
||||
`
|
||||
|
||||
func (q *Queries) GetLevelsByCourseID(ctx context.Context, courseID int64) ([]Level, error) {
|
||||
rows, err := q.db.Query(ctx, GetLevelsByCourseID, courseID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []Level
|
||||
for rows.Next() {
|
||||
var i Level
|
||||
if err := rows.Scan(
|
||||
&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 GetModulesByLevelID = `-- name: GetModulesByLevelID :many
|
||||
SELECT id, level_id, title, description, display_order, is_active, created_at
|
||||
FROM modules
|
||||
WHERE level_id = $1
|
||||
AND is_active = TRUE
|
||||
ORDER BY display_order ASC, id ASC
|
||||
`
|
||||
|
||||
func (q *Queries) GetModulesByLevelID(ctx context.Context, levelID int64) ([]Module, error) {
|
||||
rows, err := q.db.Query(ctx, GetModulesByLevelID, levelID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []Module
|
||||
for rows.Next() {
|
||||
var i Module
|
||||
if err := rows.Scan(
|
||||
&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 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
|
||||
ORDER BY smp.display_order ASC, smp.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) {
|
||||
rows, err := q.db.Query(ctx, GetSubModuleLessons, subModuleID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetSubModuleLessonsRow
|
||||
for rows.Next() {
|
||||
var i GetSubModuleLessonsRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.SubModuleID,
|
||||
&i.QuestionSetID,
|
||||
&i.IntroVideoUrl,
|
||||
&i.DisplayOrder,
|
||||
&i.IsActive,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.Status,
|
||||
&i.SetType,
|
||||
&i.QuestionCount,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const GetSubModulePractices = `-- name: GetSubModulePractices :many
|
||||
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.sub_module_id = $1
|
||||
AND smp.is_active = TRUE
|
||||
ORDER BY smp.display_order ASC, smp.id ASC
|
||||
`
|
||||
|
||||
type GetSubModulePracticesRow 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) GetSubModulePractices(ctx context.Context, subModuleID int64) ([]GetSubModulePracticesRow, error) {
|
||||
rows, err := q.db.Query(ctx, GetSubModulePractices, subModuleID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetSubModulePracticesRow
|
||||
for rows.Next() {
|
||||
var i GetSubModulePracticesRow
|
||||
if err := rows.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,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const GetSubModuleVideos = `-- name: GetSubModuleVideos :many
|
||||
SELECT id, sub_module_id, title, description, video_url, duration, resolution, is_published, publish_date, visibility, instructor_id, thumbnail, display_order, status, vimeo_id, vimeo_embed_url, vimeo_player_html, vimeo_status, video_host_provider, created_at
|
||||
FROM sub_module_videos
|
||||
WHERE sub_module_id = $1
|
||||
AND status != 'ARCHIVED'
|
||||
ORDER BY display_order ASC, id ASC
|
||||
`
|
||||
|
||||
func (q *Queries) GetSubModuleVideos(ctx context.Context, subModuleID int64) ([]SubModuleVideo, error) {
|
||||
rows, err := q.db.Query(ctx, GetSubModuleVideos, subModuleID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []SubModuleVideo
|
||||
for rows.Next() {
|
||||
var i SubModuleVideo
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.SubModuleID,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.VideoUrl,
|
||||
&i.Duration,
|
||||
&i.Resolution,
|
||||
&i.IsPublished,
|
||||
&i.PublishDate,
|
||||
&i.Visibility,
|
||||
&i.InstructorID,
|
||||
&i.Thumbnail,
|
||||
&i.DisplayOrder,
|
||||
&i.Status,
|
||||
&i.VimeoID,
|
||||
&i.VimeoEmbedUrl,
|
||||
&i.VimeoPlayerHtml,
|
||||
&i.VimeoStatus,
|
||||
&i.VideoHostProvider,
|
||||
&i.CreatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const GetSubModulesByModuleID = `-- name: GetSubModulesByModuleID :many
|
||||
SELECT id, module_id, title, description, display_order, is_active, created_at, legacy_sub_course_id
|
||||
FROM sub_modules
|
||||
WHERE module_id = $1
|
||||
AND is_active = TRUE
|
||||
ORDER BY display_order ASC, id ASC
|
||||
`
|
||||
|
||||
func (q *Queries) GetSubModulesByModuleID(ctx context.Context, moduleID int64) ([]SubModule, error) {
|
||||
rows, err := q.db.Query(ctx, GetSubModulesByModuleID, moduleID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []SubModule
|
||||
for rows.Next() {
|
||||
var i SubModule
|
||||
if err := rows.Scan(
|
||||
&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
|
||||
}
|
||||
|
|
@ -1,285 +0,0 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
// source: learning_tree.sql
|
||||
|
||||
package dbgen
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const GetCourseLearningPath = `-- name: GetCourseLearningPath :many
|
||||
SELECT
|
||||
c.id AS course_id,
|
||||
c.title AS course_title,
|
||||
c.description AS course_description,
|
||||
c.thumbnail AS course_thumbnail,
|
||||
c.intro_video_url AS course_intro_video_url,
|
||||
cc.id AS category_id,
|
||||
cc.name AS category_name,
|
||||
sc.id AS sub_course_id,
|
||||
sc.title AS sub_course_title,
|
||||
sc.description AS sub_course_description,
|
||||
sc.thumbnail AS sub_course_thumbnail,
|
||||
sc.display_order AS sub_course_display_order,
|
||||
sc.level AS sub_course_level,
|
||||
sc.sub_level AS sub_course_sub_level,
|
||||
(SELECT COUNT(*) FROM sub_course_prerequisites WHERE sub_course_id = sc.id) AS prerequisite_count,
|
||||
(SELECT COUNT(*) FROM sub_course_videos WHERE sub_course_id = sc.id AND status = 'PUBLISHED') AS video_count,
|
||||
(SELECT COUNT(*) FROM question_sets WHERE set_type = 'PRACTICE' AND owner_type = 'SUB_COURSE' AND owner_id = sc.id AND status = 'PUBLISHED') AS practice_count
|
||||
FROM courses c
|
||||
JOIN course_categories cc ON cc.id = c.category_id
|
||||
LEFT JOIN sub_courses sc ON sc.course_id = c.id AND sc.is_active = true
|
||||
WHERE c.id = $1
|
||||
ORDER BY sc.display_order, sc.id
|
||||
`
|
||||
|
||||
type GetCourseLearningPathRow struct {
|
||||
CourseID int64 `json:"course_id"`
|
||||
CourseTitle string `json:"course_title"`
|
||||
CourseDescription pgtype.Text `json:"course_description"`
|
||||
CourseThumbnail pgtype.Text `json:"course_thumbnail"`
|
||||
CourseIntroVideoUrl pgtype.Text `json:"course_intro_video_url"`
|
||||
CategoryID int64 `json:"category_id"`
|
||||
CategoryName string `json:"category_name"`
|
||||
SubCourseID pgtype.Int8 `json:"sub_course_id"`
|
||||
SubCourseTitle pgtype.Text `json:"sub_course_title"`
|
||||
SubCourseDescription pgtype.Text `json:"sub_course_description"`
|
||||
SubCourseThumbnail pgtype.Text `json:"sub_course_thumbnail"`
|
||||
SubCourseDisplayOrder pgtype.Int4 `json:"sub_course_display_order"`
|
||||
SubCourseLevel pgtype.Text `json:"sub_course_level"`
|
||||
SubCourseSubLevel pgtype.Text `json:"sub_course_sub_level"`
|
||||
PrerequisiteCount int64 `json:"prerequisite_count"`
|
||||
VideoCount int64 `json:"video_count"`
|
||||
PracticeCount int64 `json:"practice_count"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetCourseLearningPath(ctx context.Context, id int64) ([]GetCourseLearningPathRow, error) {
|
||||
rows, err := q.db.Query(ctx, GetCourseLearningPath, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetCourseLearningPathRow
|
||||
for rows.Next() {
|
||||
var i GetCourseLearningPathRow
|
||||
if err := rows.Scan(
|
||||
&i.CourseID,
|
||||
&i.CourseTitle,
|
||||
&i.CourseDescription,
|
||||
&i.CourseThumbnail,
|
||||
&i.CourseIntroVideoUrl,
|
||||
&i.CategoryID,
|
||||
&i.CategoryName,
|
||||
&i.SubCourseID,
|
||||
&i.SubCourseTitle,
|
||||
&i.SubCourseDescription,
|
||||
&i.SubCourseThumbnail,
|
||||
&i.SubCourseDisplayOrder,
|
||||
&i.SubCourseLevel,
|
||||
&i.SubCourseSubLevel,
|
||||
&i.PrerequisiteCount,
|
||||
&i.VideoCount,
|
||||
&i.PracticeCount,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const GetFullLearningTree = `-- name: GetFullLearningTree :many
|
||||
SELECT
|
||||
c.id AS course_id,
|
||||
c.title AS course_title,
|
||||
sc.id AS sub_course_id,
|
||||
sc.title AS sub_course_title,
|
||||
sc.level AS sub_course_level,
|
||||
sc.sub_level AS sub_course_sub_level
|
||||
FROM courses c
|
||||
LEFT JOIN sub_courses sc ON sc.course_id = c.id AND sc.is_active = true
|
||||
WHERE c.is_active = true
|
||||
ORDER BY c.id, sc.display_order, sc.id
|
||||
`
|
||||
|
||||
type GetFullLearningTreeRow struct {
|
||||
CourseID int64 `json:"course_id"`
|
||||
CourseTitle string `json:"course_title"`
|
||||
SubCourseID pgtype.Int8 `json:"sub_course_id"`
|
||||
SubCourseTitle pgtype.Text `json:"sub_course_title"`
|
||||
SubCourseLevel pgtype.Text `json:"sub_course_level"`
|
||||
SubCourseSubLevel pgtype.Text `json:"sub_course_sub_level"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetFullLearningTree(ctx context.Context) ([]GetFullLearningTreeRow, error) {
|
||||
rows, err := q.db.Query(ctx, GetFullLearningTree)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetFullLearningTreeRow
|
||||
for rows.Next() {
|
||||
var i GetFullLearningTreeRow
|
||||
if err := rows.Scan(
|
||||
&i.CourseID,
|
||||
&i.CourseTitle,
|
||||
&i.SubCourseID,
|
||||
&i.SubCourseTitle,
|
||||
&i.SubCourseLevel,
|
||||
&i.SubCourseSubLevel,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const GetSubCoursePracticesForLearningPath = `-- name: GetSubCoursePracticesForLearningPath :many
|
||||
SELECT id, title, description, persona, status, intro_video_url,
|
||||
(SELECT COUNT(*) FROM question_set_items WHERE set_id = qs.id) AS question_count
|
||||
FROM question_sets qs
|
||||
WHERE qs.owner_type = 'SUB_COURSE' AND qs.owner_id = $1
|
||||
AND qs.set_type = 'PRACTICE' AND qs.status = 'PUBLISHED'
|
||||
ORDER BY qs.display_order ASC, qs.created_at
|
||||
`
|
||||
|
||||
type GetSubCoursePracticesForLearningPathRow struct {
|
||||
ID int64 `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
Persona pgtype.Text `json:"persona"`
|
||||
Status string `json:"status"`
|
||||
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
||||
QuestionCount int64 `json:"question_count"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetSubCoursePracticesForLearningPath(ctx context.Context, ownerID pgtype.Int8) ([]GetSubCoursePracticesForLearningPathRow, error) {
|
||||
rows, err := q.db.Query(ctx, GetSubCoursePracticesForLearningPath, ownerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetSubCoursePracticesForLearningPathRow
|
||||
for rows.Next() {
|
||||
var i GetSubCoursePracticesForLearningPathRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.Persona,
|
||||
&i.Status,
|
||||
&i.IntroVideoUrl,
|
||||
&i.QuestionCount,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const GetSubCoursePrerequisitesForLearningPath = `-- name: GetSubCoursePrerequisitesForLearningPath :many
|
||||
SELECT p.prerequisite_sub_course_id, sc.title, sc.level, sc.sub_level
|
||||
FROM sub_course_prerequisites p
|
||||
JOIN sub_courses sc ON sc.id = p.prerequisite_sub_course_id
|
||||
WHERE p.sub_course_id = $1
|
||||
ORDER BY sc.display_order
|
||||
`
|
||||
|
||||
type GetSubCoursePrerequisitesForLearningPathRow struct {
|
||||
PrerequisiteSubCourseID int64 `json:"prerequisite_sub_course_id"`
|
||||
Title string `json:"title"`
|
||||
Level string `json:"level"`
|
||||
SubLevel string `json:"sub_level"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetSubCoursePrerequisitesForLearningPath(ctx context.Context, subCourseID int64) ([]GetSubCoursePrerequisitesForLearningPathRow, error) {
|
||||
rows, err := q.db.Query(ctx, GetSubCoursePrerequisitesForLearningPath, subCourseID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetSubCoursePrerequisitesForLearningPathRow
|
||||
for rows.Next() {
|
||||
var i GetSubCoursePrerequisitesForLearningPathRow
|
||||
if err := rows.Scan(
|
||||
&i.PrerequisiteSubCourseID,
|
||||
&i.Title,
|
||||
&i.Level,
|
||||
&i.SubLevel,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const GetSubCourseVideosForLearningPath = `-- name: GetSubCourseVideosForLearningPath :many
|
||||
SELECT id, title, description, video_url, duration, resolution, display_order,
|
||||
vimeo_id, vimeo_embed_url, video_host_provider
|
||||
FROM sub_course_videos
|
||||
WHERE sub_course_id = $1 AND status = 'PUBLISHED'
|
||||
ORDER BY display_order, id
|
||||
`
|
||||
|
||||
type GetSubCourseVideosForLearningPathRow struct {
|
||||
ID int64 `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
VideoUrl string `json:"video_url"`
|
||||
Duration int32 `json:"duration"`
|
||||
Resolution pgtype.Text `json:"resolution"`
|
||||
DisplayOrder int32 `json:"display_order"`
|
||||
VimeoID pgtype.Text `json:"vimeo_id"`
|
||||
VimeoEmbedUrl pgtype.Text `json:"vimeo_embed_url"`
|
||||
VideoHostProvider pgtype.Text `json:"video_host_provider"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetSubCourseVideosForLearningPath(ctx context.Context, subCourseID int64) ([]GetSubCourseVideosForLearningPathRow, error) {
|
||||
rows, err := q.db.Query(ctx, GetSubCourseVideosForLearningPath, subCourseID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetSubCourseVideosForLearningPathRow
|
||||
for rows.Next() {
|
||||
var i GetSubCourseVideosForLearningPathRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.VideoUrl,
|
||||
&i.Duration,
|
||||
&i.Resolution,
|
||||
&i.DisplayOrder,
|
||||
&i.VimeoID,
|
||||
&i.VimeoEmbedUrl,
|
||||
&i.VideoHostProvider,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
|
@ -31,6 +31,7 @@ type Course struct {
|
|||
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
||||
DisplayOrder int32 `json:"display_order"`
|
||||
SubCategoryID pgtype.Int8 `json:"sub_category_id"`
|
||||
}
|
||||
|
||||
type CourseCategory struct {
|
||||
|
|
@ -41,6 +42,16 @@ type CourseCategory struct {
|
|||
DisplayOrder int32 `json:"display_order"`
|
||||
}
|
||||
|
||||
type CourseSubCategory struct {
|
||||
ID int64 `json:"id"`
|
||||
CategoryID int64 `json:"category_id"`
|
||||
Name string `json:"name"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
IsActive bool `json:"is_active"`
|
||||
DisplayOrder int32 `json:"display_order"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
}
|
||||
|
||||
type Device struct {
|
||||
ID int64 `json:"id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
|
|
@ -58,11 +69,30 @@ type GlobalSetting struct {
|
|||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
type Level struct {
|
||||
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"`
|
||||
}
|
||||
|
||||
type LevelToSubCourse struct {
|
||||
LevelID int64 `json:"level_id"`
|
||||
SubCourseID int64 `json:"sub_course_id"`
|
||||
}
|
||||
|
||||
type Module struct {
|
||||
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"`
|
||||
}
|
||||
|
||||
type ModuleToSubCourse struct {
|
||||
ModuleID int64 `json:"module_id"`
|
||||
SubCourseID int64 `json:"sub_course_id"`
|
||||
|
|
@ -314,6 +344,63 @@ type SubCourseVideo struct {
|
|||
VideoHostProvider pgtype.Text `json:"video_host_provider"`
|
||||
}
|
||||
|
||||
type SubModule struct {
|
||||
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"`
|
||||
}
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
type SubModulePractice 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"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
}
|
||||
|
||||
type SubModuleVideo struct {
|
||||
ID int64 `json:"id"`
|
||||
SubModuleID int64 `json:"sub_module_id"`
|
||||
Title string `json:"title"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
VideoUrl string `json:"video_url"`
|
||||
Duration pgtype.Int4 `json:"duration"`
|
||||
Resolution pgtype.Text `json:"resolution"`
|
||||
IsPublished bool `json:"is_published"`
|
||||
PublishDate pgtype.Timestamptz `json:"publish_date"`
|
||||
Visibility pgtype.Text `json:"visibility"`
|
||||
InstructorID pgtype.Text `json:"instructor_id"`
|
||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||
DisplayOrder int32 `json:"display_order"`
|
||||
Status string `json:"status"`
|
||||
VimeoID pgtype.Text `json:"vimeo_id"`
|
||||
VimeoEmbedUrl pgtype.Text `json:"vimeo_embed_url"`
|
||||
VimeoPlayerHtml pgtype.Text `json:"vimeo_player_html"`
|
||||
VimeoStatus pgtype.Text `json:"vimeo_status"`
|
||||
VideoHostProvider pgtype.Text `json:"video_host_provider"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
}
|
||||
|
||||
type SubscriptionPlan struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
|
|
@ -404,7 +491,7 @@ type UserAudioResponse struct {
|
|||
type UserPracticeProgress struct {
|
||||
ID int64 `json:"id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
SubCourseID int64 `json:"sub_course_id"`
|
||||
SubCourseID pgtype.Int8 `json:"sub_course_id"`
|
||||
QuestionSetID int64 `json:"question_set_id"`
|
||||
CompletedAt pgtype.Timestamp `json:"completed_at"`
|
||||
CreatedAt pgtype.Timestamp `json:"created_at"`
|
||||
|
|
|
|||
98
gen/db/practice_progress.sql.go
Normal file
98
gen/db/practice_progress.sql.go
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
// source: practice_progress.sql
|
||||
|
||||
package dbgen
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
const GetFirstIncompletePreviousPractice = `-- name: GetFirstIncompletePreviousPractice :one
|
||||
WITH target AS (
|
||||
SELECT id, owner_type, owner_id, COALESCE(display_order, 0) AS display_order
|
||||
FROM question_sets
|
||||
WHERE id = $2::BIGINT
|
||||
AND set_type = 'PRACTICE'
|
||||
AND status = 'PUBLISHED'
|
||||
),
|
||||
candidates AS (
|
||||
SELECT qs.id, qs.title, COALESCE(qs.display_order, 0) AS display_order
|
||||
FROM question_sets qs
|
||||
JOIN target t
|
||||
ON qs.owner_type = t.owner_type
|
||||
AND qs.owner_id = t.owner_id
|
||||
WHERE qs.set_type = 'PRACTICE'
|
||||
AND qs.status = 'PUBLISHED'
|
||||
AND COALESCE(qs.display_order, 0) < t.display_order
|
||||
)
|
||||
SELECT c.id, c.title, c.display_order
|
||||
FROM candidates c
|
||||
LEFT JOIN user_practice_progress upp
|
||||
ON upp.question_set_id = c.id
|
||||
AND upp.user_id = $1::BIGINT
|
||||
AND upp.completed_at IS NOT NULL
|
||||
WHERE upp.id IS NULL
|
||||
ORDER BY c.display_order ASC, c.id ASC
|
||||
LIMIT 1
|
||||
`
|
||||
|
||||
type GetFirstIncompletePreviousPracticeParams struct {
|
||||
UserID int64 `json:"user_id"`
|
||||
QuestionSetID int64 `json:"question_set_id"`
|
||||
}
|
||||
|
||||
type GetFirstIncompletePreviousPracticeRow struct {
|
||||
ID int64 `json:"id"`
|
||||
Title string `json:"title"`
|
||||
DisplayOrder int32 `json:"display_order"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetFirstIncompletePreviousPractice(ctx context.Context, arg GetFirstIncompletePreviousPracticeParams) (GetFirstIncompletePreviousPracticeRow, error) {
|
||||
row := q.db.QueryRow(ctx, GetFirstIncompletePreviousPractice, arg.UserID, arg.QuestionSetID)
|
||||
var i GetFirstIncompletePreviousPracticeRow
|
||||
err := row.Scan(&i.ID, &i.Title, &i.DisplayOrder)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const MarkPracticeCompleted = `-- name: MarkPracticeCompleted :execrows
|
||||
INSERT INTO user_practice_progress (
|
||||
user_id,
|
||||
sub_course_id,
|
||||
question_set_id,
|
||||
completed_at,
|
||||
updated_at
|
||||
)
|
||||
SELECT
|
||||
$1::BIGINT,
|
||||
CASE
|
||||
WHEN qs.owner_type = 'SUB_COURSE' THEN qs.owner_id
|
||||
WHEN qs.owner_type = 'SUB_MODULE' THEN sm.legacy_sub_course_id
|
||||
ELSE NULL
|
||||
END,
|
||||
qs.id,
|
||||
CURRENT_TIMESTAMP,
|
||||
CURRENT_TIMESTAMP
|
||||
FROM question_sets qs
|
||||
LEFT JOIN sub_modules sm
|
||||
ON qs.owner_type = 'SUB_MODULE'
|
||||
AND qs.owner_id = sm.id
|
||||
WHERE qs.id = $2::BIGINT
|
||||
ON CONFLICT (user_id, question_set_id) DO UPDATE
|
||||
SET completed_at = EXCLUDED.completed_at,
|
||||
updated_at = EXCLUDED.updated_at
|
||||
`
|
||||
|
||||
type MarkPracticeCompletedParams struct {
|
||||
UserID int64 `json:"user_id"`
|
||||
QuestionSetID int64 `json:"question_set_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) MarkPracticeCompleted(ctx context.Context, arg MarkPracticeCompletedParams) (int64, error) {
|
||||
result, err := q.db.Exec(ctx, MarkPracticeCompleted, arg.UserID, arg.QuestionSetID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return result.RowsAffected(), nil
|
||||
}
|
||||
|
|
@ -1,187 +0,0 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
// source: sub_course_prerequisites.sql
|
||||
|
||||
package dbgen
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const AddSubCoursePrerequisite = `-- name: AddSubCoursePrerequisite :one
|
||||
INSERT INTO sub_course_prerequisites (sub_course_id, prerequisite_sub_course_id)
|
||||
VALUES ($1, $2)
|
||||
RETURNING id, sub_course_id, prerequisite_sub_course_id, created_at
|
||||
`
|
||||
|
||||
type AddSubCoursePrerequisiteParams struct {
|
||||
SubCourseID int64 `json:"sub_course_id"`
|
||||
PrerequisiteSubCourseID int64 `json:"prerequisite_sub_course_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) AddSubCoursePrerequisite(ctx context.Context, arg AddSubCoursePrerequisiteParams) (SubCoursePrerequisite, error) {
|
||||
row := q.db.QueryRow(ctx, AddSubCoursePrerequisite, arg.SubCourseID, arg.PrerequisiteSubCourseID)
|
||||
var i SubCoursePrerequisite
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.SubCourseID,
|
||||
&i.PrerequisiteSubCourseID,
|
||||
&i.CreatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const CountUnmetPrerequisites = `-- name: CountUnmetPrerequisites :one
|
||||
SELECT COUNT(*)::bigint AS unmet_count
|
||||
FROM sub_course_prerequisites p
|
||||
WHERE p.sub_course_id = $1
|
||||
AND p.prerequisite_sub_course_id NOT IN (
|
||||
SELECT usp.sub_course_id
|
||||
FROM user_sub_course_progress usp
|
||||
WHERE usp.user_id = $2
|
||||
AND usp.status = 'COMPLETED'
|
||||
)
|
||||
`
|
||||
|
||||
type CountUnmetPrerequisitesParams struct {
|
||||
SubCourseID int64 `json:"sub_course_id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) CountUnmetPrerequisites(ctx context.Context, arg CountUnmetPrerequisitesParams) (int64, error) {
|
||||
row := q.db.QueryRow(ctx, CountUnmetPrerequisites, arg.SubCourseID, arg.UserID)
|
||||
var unmet_count int64
|
||||
err := row.Scan(&unmet_count)
|
||||
return unmet_count, err
|
||||
}
|
||||
|
||||
const DeleteAllPrerequisitesForSubCourse = `-- name: DeleteAllPrerequisitesForSubCourse :exec
|
||||
DELETE FROM sub_course_prerequisites
|
||||
WHERE sub_course_id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteAllPrerequisitesForSubCourse(ctx context.Context, subCourseID int64) error {
|
||||
_, err := q.db.Exec(ctx, DeleteAllPrerequisitesForSubCourse, subCourseID)
|
||||
return err
|
||||
}
|
||||
|
||||
const GetSubCourseDependents = `-- name: GetSubCourseDependents :many
|
||||
SELECT
|
||||
p.id,
|
||||
p.sub_course_id,
|
||||
p.prerequisite_sub_course_id,
|
||||
p.created_at,
|
||||
sc.title AS dependent_title,
|
||||
sc.level AS dependent_level
|
||||
FROM sub_course_prerequisites p
|
||||
JOIN sub_courses sc ON sc.id = p.sub_course_id
|
||||
WHERE p.prerequisite_sub_course_id = $1
|
||||
ORDER BY sc.display_order
|
||||
`
|
||||
|
||||
type GetSubCourseDependentsRow struct {
|
||||
ID int64 `json:"id"`
|
||||
SubCourseID int64 `json:"sub_course_id"`
|
||||
PrerequisiteSubCourseID int64 `json:"prerequisite_sub_course_id"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
DependentTitle string `json:"dependent_title"`
|
||||
DependentLevel string `json:"dependent_level"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetSubCourseDependents(ctx context.Context, prerequisiteSubCourseID int64) ([]GetSubCourseDependentsRow, error) {
|
||||
rows, err := q.db.Query(ctx, GetSubCourseDependents, prerequisiteSubCourseID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetSubCourseDependentsRow
|
||||
for rows.Next() {
|
||||
var i GetSubCourseDependentsRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.SubCourseID,
|
||||
&i.PrerequisiteSubCourseID,
|
||||
&i.CreatedAt,
|
||||
&i.DependentTitle,
|
||||
&i.DependentLevel,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const GetSubCoursePrerequisites = `-- name: GetSubCoursePrerequisites :many
|
||||
SELECT
|
||||
p.id,
|
||||
p.sub_course_id,
|
||||
p.prerequisite_sub_course_id,
|
||||
p.created_at,
|
||||
sc.title AS prerequisite_title,
|
||||
sc.level AS prerequisite_level,
|
||||
sc.display_order AS prerequisite_display_order
|
||||
FROM sub_course_prerequisites p
|
||||
JOIN sub_courses sc ON sc.id = p.prerequisite_sub_course_id
|
||||
WHERE p.sub_course_id = $1
|
||||
ORDER BY sc.display_order
|
||||
`
|
||||
|
||||
type GetSubCoursePrerequisitesRow struct {
|
||||
ID int64 `json:"id"`
|
||||
SubCourseID int64 `json:"sub_course_id"`
|
||||
PrerequisiteSubCourseID int64 `json:"prerequisite_sub_course_id"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
PrerequisiteTitle string `json:"prerequisite_title"`
|
||||
PrerequisiteLevel string `json:"prerequisite_level"`
|
||||
PrerequisiteDisplayOrder int32 `json:"prerequisite_display_order"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetSubCoursePrerequisites(ctx context.Context, subCourseID int64) ([]GetSubCoursePrerequisitesRow, error) {
|
||||
rows, err := q.db.Query(ctx, GetSubCoursePrerequisites, subCourseID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetSubCoursePrerequisitesRow
|
||||
for rows.Next() {
|
||||
var i GetSubCoursePrerequisitesRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.SubCourseID,
|
||||
&i.PrerequisiteSubCourseID,
|
||||
&i.CreatedAt,
|
||||
&i.PrerequisiteTitle,
|
||||
&i.PrerequisiteLevel,
|
||||
&i.PrerequisiteDisplayOrder,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const RemoveSubCoursePrerequisite = `-- name: RemoveSubCoursePrerequisite :exec
|
||||
DELETE FROM sub_course_prerequisites
|
||||
WHERE sub_course_id = $1 AND prerequisite_sub_course_id = $2
|
||||
`
|
||||
|
||||
type RemoveSubCoursePrerequisiteParams struct {
|
||||
SubCourseID int64 `json:"sub_course_id"`
|
||||
PrerequisiteSubCourseID int64 `json:"prerequisite_sub_course_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) RemoveSubCoursePrerequisite(ctx context.Context, arg RemoveSubCoursePrerequisiteParams) error {
|
||||
_, err := q.db.Exec(ctx, RemoveSubCoursePrerequisite, arg.SubCourseID, arg.PrerequisiteSubCourseID)
|
||||
return err
|
||||
}
|
||||
|
|
@ -1,441 +0,0 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
// source: sub_course_videos.sql
|
||||
|
||||
package dbgen
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const ArchiveSubCourseVideo = `-- name: ArchiveSubCourseVideo :exec
|
||||
UPDATE sub_course_videos
|
||||
SET status = 'ARCHIVED'
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) ArchiveSubCourseVideo(ctx context.Context, id int64) error {
|
||||
_, err := q.db.Exec(ctx, ArchiveSubCourseVideo, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const CreateSubCourseVideo = `-- name: CreateSubCourseVideo :one
|
||||
INSERT INTO sub_course_videos (
|
||||
sub_course_id,
|
||||
title,
|
||||
description,
|
||||
video_url,
|
||||
duration,
|
||||
resolution,
|
||||
instructor_id,
|
||||
thumbnail,
|
||||
visibility,
|
||||
display_order,
|
||||
status,
|
||||
vimeo_id,
|
||||
vimeo_embed_url,
|
||||
vimeo_player_html,
|
||||
vimeo_status,
|
||||
video_host_provider
|
||||
)
|
||||
VALUES (
|
||||
$1, $2, $3, $4, $5, $6,
|
||||
$7, $8, $9,
|
||||
COALESCE($10, 0),
|
||||
COALESCE($11, 'DRAFT'),
|
||||
$12, $13, $14,
|
||||
COALESCE($15, 'pending'),
|
||||
COALESCE($16, 'DIRECT')
|
||||
)
|
||||
RETURNING id, sub_course_id, title, description, video_url, duration, resolution, is_published, publish_date, visibility, instructor_id, thumbnail, display_order, status, vimeo_id, vimeo_embed_url, vimeo_player_html, vimeo_status, video_host_provider
|
||||
`
|
||||
|
||||
type CreateSubCourseVideoParams struct {
|
||||
SubCourseID int64 `json:"sub_course_id"`
|
||||
Title string `json:"title"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
VideoUrl string `json:"video_url"`
|
||||
Duration int32 `json:"duration"`
|
||||
Resolution pgtype.Text `json:"resolution"`
|
||||
InstructorID pgtype.Text `json:"instructor_id"`
|
||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||
Visibility pgtype.Text `json:"visibility"`
|
||||
Column10 interface{} `json:"column_10"`
|
||||
Column11 interface{} `json:"column_11"`
|
||||
VimeoID pgtype.Text `json:"vimeo_id"`
|
||||
VimeoEmbedUrl pgtype.Text `json:"vimeo_embed_url"`
|
||||
VimeoPlayerHtml pgtype.Text `json:"vimeo_player_html"`
|
||||
Column15 interface{} `json:"column_15"`
|
||||
Column16 interface{} `json:"column_16"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateSubCourseVideo(ctx context.Context, arg CreateSubCourseVideoParams) (SubCourseVideo, error) {
|
||||
row := q.db.QueryRow(ctx, CreateSubCourseVideo,
|
||||
arg.SubCourseID,
|
||||
arg.Title,
|
||||
arg.Description,
|
||||
arg.VideoUrl,
|
||||
arg.Duration,
|
||||
arg.Resolution,
|
||||
arg.InstructorID,
|
||||
arg.Thumbnail,
|
||||
arg.Visibility,
|
||||
arg.Column10,
|
||||
arg.Column11,
|
||||
arg.VimeoID,
|
||||
arg.VimeoEmbedUrl,
|
||||
arg.VimeoPlayerHtml,
|
||||
arg.Column15,
|
||||
arg.Column16,
|
||||
)
|
||||
var i SubCourseVideo
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.SubCourseID,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.VideoUrl,
|
||||
&i.Duration,
|
||||
&i.Resolution,
|
||||
&i.IsPublished,
|
||||
&i.PublishDate,
|
||||
&i.Visibility,
|
||||
&i.InstructorID,
|
||||
&i.Thumbnail,
|
||||
&i.DisplayOrder,
|
||||
&i.Status,
|
||||
&i.VimeoID,
|
||||
&i.VimeoEmbedUrl,
|
||||
&i.VimeoPlayerHtml,
|
||||
&i.VimeoStatus,
|
||||
&i.VideoHostProvider,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const DeleteSubCourseVideo = `-- name: DeleteSubCourseVideo :exec
|
||||
DELETE FROM sub_course_videos
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteSubCourseVideo(ctx context.Context, id int64) error {
|
||||
_, err := q.db.Exec(ctx, DeleteSubCourseVideo, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const GetPublishedVideosBySubCourse = `-- name: GetPublishedVideosBySubCourse :many
|
||||
SELECT id, sub_course_id, title, description, video_url, duration, resolution, is_published, publish_date, visibility, instructor_id, thumbnail, display_order, status, vimeo_id, vimeo_embed_url, vimeo_player_html, vimeo_status, video_host_provider
|
||||
FROM sub_course_videos
|
||||
WHERE sub_course_id = $1
|
||||
AND status = 'PUBLISHED'
|
||||
ORDER BY display_order ASC, publish_date ASC
|
||||
`
|
||||
|
||||
func (q *Queries) GetPublishedVideosBySubCourse(ctx context.Context, subCourseID int64) ([]SubCourseVideo, error) {
|
||||
rows, err := q.db.Query(ctx, GetPublishedVideosBySubCourse, subCourseID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []SubCourseVideo
|
||||
for rows.Next() {
|
||||
var i SubCourseVideo
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.SubCourseID,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.VideoUrl,
|
||||
&i.Duration,
|
||||
&i.Resolution,
|
||||
&i.IsPublished,
|
||||
&i.PublishDate,
|
||||
&i.Visibility,
|
||||
&i.InstructorID,
|
||||
&i.Thumbnail,
|
||||
&i.DisplayOrder,
|
||||
&i.Status,
|
||||
&i.VimeoID,
|
||||
&i.VimeoEmbedUrl,
|
||||
&i.VimeoPlayerHtml,
|
||||
&i.VimeoStatus,
|
||||
&i.VideoHostProvider,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const GetSubCourseVideoByID = `-- name: GetSubCourseVideoByID :one
|
||||
SELECT id, sub_course_id, title, description, video_url, duration, resolution, is_published, publish_date, visibility, instructor_id, thumbnail, display_order, status, vimeo_id, vimeo_embed_url, vimeo_player_html, vimeo_status, video_host_provider
|
||||
FROM sub_course_videos
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetSubCourseVideoByID(ctx context.Context, id int64) (SubCourseVideo, error) {
|
||||
row := q.db.QueryRow(ctx, GetSubCourseVideoByID, id)
|
||||
var i SubCourseVideo
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.SubCourseID,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.VideoUrl,
|
||||
&i.Duration,
|
||||
&i.Resolution,
|
||||
&i.IsPublished,
|
||||
&i.PublishDate,
|
||||
&i.Visibility,
|
||||
&i.InstructorID,
|
||||
&i.Thumbnail,
|
||||
&i.DisplayOrder,
|
||||
&i.Status,
|
||||
&i.VimeoID,
|
||||
&i.VimeoEmbedUrl,
|
||||
&i.VimeoPlayerHtml,
|
||||
&i.VimeoStatus,
|
||||
&i.VideoHostProvider,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const GetVideosBySubCourse = `-- name: GetVideosBySubCourse :many
|
||||
SELECT
|
||||
COUNT(*) OVER () AS total_count,
|
||||
id,
|
||||
sub_course_id,
|
||||
title,
|
||||
description,
|
||||
video_url,
|
||||
duration,
|
||||
resolution,
|
||||
is_published,
|
||||
publish_date,
|
||||
visibility,
|
||||
instructor_id,
|
||||
thumbnail,
|
||||
display_order,
|
||||
status,
|
||||
vimeo_id,
|
||||
vimeo_embed_url,
|
||||
vimeo_player_html,
|
||||
vimeo_status,
|
||||
video_host_provider
|
||||
FROM sub_course_videos
|
||||
WHERE sub_course_id = $1
|
||||
AND status != 'ARCHIVED'
|
||||
ORDER BY display_order ASC, id ASC
|
||||
`
|
||||
|
||||
type GetVideosBySubCourseRow struct {
|
||||
TotalCount int64 `json:"total_count"`
|
||||
ID int64 `json:"id"`
|
||||
SubCourseID int64 `json:"sub_course_id"`
|
||||
Title string `json:"title"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
VideoUrl string `json:"video_url"`
|
||||
Duration int32 `json:"duration"`
|
||||
Resolution pgtype.Text `json:"resolution"`
|
||||
IsPublished bool `json:"is_published"`
|
||||
PublishDate pgtype.Timestamptz `json:"publish_date"`
|
||||
Visibility pgtype.Text `json:"visibility"`
|
||||
InstructorID pgtype.Text `json:"instructor_id"`
|
||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||
DisplayOrder int32 `json:"display_order"`
|
||||
Status string `json:"status"`
|
||||
VimeoID pgtype.Text `json:"vimeo_id"`
|
||||
VimeoEmbedUrl pgtype.Text `json:"vimeo_embed_url"`
|
||||
VimeoPlayerHtml pgtype.Text `json:"vimeo_player_html"`
|
||||
VimeoStatus pgtype.Text `json:"vimeo_status"`
|
||||
VideoHostProvider pgtype.Text `json:"video_host_provider"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetVideosBySubCourse(ctx context.Context, subCourseID int64) ([]GetVideosBySubCourseRow, error) {
|
||||
rows, err := q.db.Query(ctx, GetVideosBySubCourse, subCourseID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetVideosBySubCourseRow
|
||||
for rows.Next() {
|
||||
var i GetVideosBySubCourseRow
|
||||
if err := rows.Scan(
|
||||
&i.TotalCount,
|
||||
&i.ID,
|
||||
&i.SubCourseID,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.VideoUrl,
|
||||
&i.Duration,
|
||||
&i.Resolution,
|
||||
&i.IsPublished,
|
||||
&i.PublishDate,
|
||||
&i.Visibility,
|
||||
&i.InstructorID,
|
||||
&i.Thumbnail,
|
||||
&i.DisplayOrder,
|
||||
&i.Status,
|
||||
&i.VimeoID,
|
||||
&i.VimeoEmbedUrl,
|
||||
&i.VimeoPlayerHtml,
|
||||
&i.VimeoStatus,
|
||||
&i.VideoHostProvider,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const GetVideosByVimeoID = `-- name: GetVideosByVimeoID :one
|
||||
SELECT id, sub_course_id, title, description, video_url, duration, resolution, is_published, publish_date, visibility, instructor_id, thumbnail, display_order, status, vimeo_id, vimeo_embed_url, vimeo_player_html, vimeo_status, video_host_provider FROM sub_course_videos
|
||||
WHERE vimeo_id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetVideosByVimeoID(ctx context.Context, vimeoID pgtype.Text) (SubCourseVideo, error) {
|
||||
row := q.db.QueryRow(ctx, GetVideosByVimeoID, vimeoID)
|
||||
var i SubCourseVideo
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.SubCourseID,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.VideoUrl,
|
||||
&i.Duration,
|
||||
&i.Resolution,
|
||||
&i.IsPublished,
|
||||
&i.PublishDate,
|
||||
&i.Visibility,
|
||||
&i.InstructorID,
|
||||
&i.Thumbnail,
|
||||
&i.DisplayOrder,
|
||||
&i.Status,
|
||||
&i.VimeoID,
|
||||
&i.VimeoEmbedUrl,
|
||||
&i.VimeoPlayerHtml,
|
||||
&i.VimeoStatus,
|
||||
&i.VideoHostProvider,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const PublishSubCourseVideo = `-- name: PublishSubCourseVideo :exec
|
||||
UPDATE sub_course_videos
|
||||
SET
|
||||
is_published = true,
|
||||
publish_date = CURRENT_TIMESTAMP,
|
||||
status = 'PUBLISHED'
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) PublishSubCourseVideo(ctx context.Context, id int64) error {
|
||||
_, err := q.db.Exec(ctx, PublishSubCourseVideo, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const ReorderSubCourseVideos = `-- name: ReorderSubCourseVideos :exec
|
||||
UPDATE sub_course_videos
|
||||
SET display_order = bulk.position
|
||||
FROM (
|
||||
SELECT unnest($1::BIGINT[]) AS id, unnest($2::INT[]) AS position
|
||||
) AS bulk
|
||||
WHERE sub_course_videos.id = bulk.id
|
||||
`
|
||||
|
||||
type ReorderSubCourseVideosParams struct {
|
||||
Ids []int64 `json:"ids"`
|
||||
Positions []int32 `json:"positions"`
|
||||
}
|
||||
|
||||
func (q *Queries) ReorderSubCourseVideos(ctx context.Context, arg ReorderSubCourseVideosParams) error {
|
||||
_, err := q.db.Exec(ctx, ReorderSubCourseVideos, arg.Ids, arg.Positions)
|
||||
return err
|
||||
}
|
||||
|
||||
const UpdateSubCourseVideo = `-- name: UpdateSubCourseVideo :exec
|
||||
UPDATE sub_course_videos
|
||||
SET
|
||||
title = COALESCE($1, title),
|
||||
description = COALESCE($2, description),
|
||||
video_url = COALESCE($3, video_url),
|
||||
duration = COALESCE($4, duration),
|
||||
resolution = COALESCE($5, resolution),
|
||||
visibility = COALESCE($6, visibility),
|
||||
thumbnail = COALESCE($7, thumbnail),
|
||||
display_order = COALESCE($8, display_order),
|
||||
status = COALESCE($9, status),
|
||||
vimeo_id = COALESCE($10, vimeo_id),
|
||||
vimeo_embed_url = COALESCE($11, vimeo_embed_url),
|
||||
vimeo_player_html = COALESCE($12, vimeo_player_html),
|
||||
vimeo_status = COALESCE($13, vimeo_status),
|
||||
video_host_provider = COALESCE($14, video_host_provider)
|
||||
WHERE id = $15
|
||||
`
|
||||
|
||||
type UpdateSubCourseVideoParams struct {
|
||||
Title string `json:"title"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
VideoUrl string `json:"video_url"`
|
||||
Duration int32 `json:"duration"`
|
||||
Resolution pgtype.Text `json:"resolution"`
|
||||
Visibility pgtype.Text `json:"visibility"`
|
||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||
DisplayOrder int32 `json:"display_order"`
|
||||
Status string `json:"status"`
|
||||
VimeoID pgtype.Text `json:"vimeo_id"`
|
||||
VimeoEmbedUrl pgtype.Text `json:"vimeo_embed_url"`
|
||||
VimeoPlayerHtml pgtype.Text `json:"vimeo_player_html"`
|
||||
VimeoStatus pgtype.Text `json:"vimeo_status"`
|
||||
VideoHostProvider pgtype.Text `json:"video_host_provider"`
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateSubCourseVideo(ctx context.Context, arg UpdateSubCourseVideoParams) error {
|
||||
_, err := q.db.Exec(ctx, UpdateSubCourseVideo,
|
||||
arg.Title,
|
||||
arg.Description,
|
||||
arg.VideoUrl,
|
||||
arg.Duration,
|
||||
arg.Resolution,
|
||||
arg.Visibility,
|
||||
arg.Thumbnail,
|
||||
arg.DisplayOrder,
|
||||
arg.Status,
|
||||
arg.VimeoID,
|
||||
arg.VimeoEmbedUrl,
|
||||
arg.VimeoPlayerHtml,
|
||||
arg.VimeoStatus,
|
||||
arg.VideoHostProvider,
|
||||
arg.ID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
const UpdateVimeoStatus = `-- name: UpdateVimeoStatus :exec
|
||||
UPDATE sub_course_videos
|
||||
SET
|
||||
vimeo_status = $1
|
||||
WHERE id = $2
|
||||
`
|
||||
|
||||
type UpdateVimeoStatusParams struct {
|
||||
VimeoStatus pgtype.Text `json:"vimeo_status"`
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateVimeoStatus(ctx context.Context, arg UpdateVimeoStatusParams) error {
|
||||
_, err := q.db.Exec(ctx, UpdateVimeoStatus, arg.VimeoStatus, arg.ID)
|
||||
return err
|
||||
}
|
||||
|
|
@ -1,356 +0,0 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
// source: sub_courses.sql
|
||||
|
||||
package dbgen
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const CreateSubCourse = `-- name: CreateSubCourse :one
|
||||
INSERT INTO sub_courses (
|
||||
course_id,
|
||||
title,
|
||||
description,
|
||||
thumbnail,
|
||||
display_order,
|
||||
level,
|
||||
sub_level,
|
||||
is_active
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, COALESCE($5, 0), $6, $7, COALESCE($8, true))
|
||||
RETURNING id, course_id, title, description, thumbnail, display_order, level, is_active, sub_level
|
||||
`
|
||||
|
||||
type CreateSubCourseParams struct {
|
||||
CourseID int64 `json:"course_id"`
|
||||
Title string `json:"title"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||
Column5 interface{} `json:"column_5"`
|
||||
Level string `json:"level"`
|
||||
SubLevel string `json:"sub_level"`
|
||||
Column8 interface{} `json:"column_8"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateSubCourse(ctx context.Context, arg CreateSubCourseParams) (SubCourse, error) {
|
||||
row := q.db.QueryRow(ctx, CreateSubCourse,
|
||||
arg.CourseID,
|
||||
arg.Title,
|
||||
arg.Description,
|
||||
arg.Thumbnail,
|
||||
arg.Column5,
|
||||
arg.Level,
|
||||
arg.SubLevel,
|
||||
arg.Column8,
|
||||
)
|
||||
var i SubCourse
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.CourseID,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.Thumbnail,
|
||||
&i.DisplayOrder,
|
||||
&i.Level,
|
||||
&i.IsActive,
|
||||
&i.SubLevel,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const DeactivateSubCourse = `-- name: DeactivateSubCourse :exec
|
||||
UPDATE sub_courses
|
||||
SET is_active = FALSE
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) DeactivateSubCourse(ctx context.Context, id int64) error {
|
||||
_, err := q.db.Exec(ctx, DeactivateSubCourse, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const DeleteSubCourse = `-- name: DeleteSubCourse :one
|
||||
DELETE FROM sub_courses
|
||||
WHERE id = $1
|
||||
RETURNING id, course_id, title, description, thumbnail, display_order, level, is_active, sub_level
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteSubCourse(ctx context.Context, id int64) (SubCourse, error) {
|
||||
row := q.db.QueryRow(ctx, DeleteSubCourse, id)
|
||||
var i SubCourse
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.CourseID,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.Thumbnail,
|
||||
&i.DisplayOrder,
|
||||
&i.Level,
|
||||
&i.IsActive,
|
||||
&i.SubLevel,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const GetSubCourseByID = `-- name: GetSubCourseByID :one
|
||||
SELECT id, course_id, title, description, thumbnail, display_order, level, is_active, sub_level
|
||||
FROM sub_courses
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetSubCourseByID(ctx context.Context, id int64) (SubCourse, error) {
|
||||
row := q.db.QueryRow(ctx, GetSubCourseByID, id)
|
||||
var i SubCourse
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.CourseID,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.Thumbnail,
|
||||
&i.DisplayOrder,
|
||||
&i.Level,
|
||||
&i.IsActive,
|
||||
&i.SubLevel,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const GetSubCoursesByCourse = `-- name: GetSubCoursesByCourse :many
|
||||
SELECT
|
||||
COUNT(*) OVER () AS total_count,
|
||||
id,
|
||||
course_id,
|
||||
title,
|
||||
description,
|
||||
thumbnail,
|
||||
display_order,
|
||||
level,
|
||||
sub_level,
|
||||
is_active
|
||||
FROM sub_courses
|
||||
WHERE course_id = $1
|
||||
ORDER BY display_order ASC, id ASC
|
||||
`
|
||||
|
||||
type GetSubCoursesByCourseRow struct {
|
||||
TotalCount int64 `json:"total_count"`
|
||||
ID int64 `json:"id"`
|
||||
CourseID int64 `json:"course_id"`
|
||||
Title string `json:"title"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||
DisplayOrder int32 `json:"display_order"`
|
||||
Level string `json:"level"`
|
||||
SubLevel string `json:"sub_level"`
|
||||
IsActive bool `json:"is_active"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetSubCoursesByCourse(ctx context.Context, courseID int64) ([]GetSubCoursesByCourseRow, error) {
|
||||
rows, err := q.db.Query(ctx, GetSubCoursesByCourse, courseID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetSubCoursesByCourseRow
|
||||
for rows.Next() {
|
||||
var i GetSubCoursesByCourseRow
|
||||
if err := rows.Scan(
|
||||
&i.TotalCount,
|
||||
&i.ID,
|
||||
&i.CourseID,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.Thumbnail,
|
||||
&i.DisplayOrder,
|
||||
&i.Level,
|
||||
&i.SubLevel,
|
||||
&i.IsActive,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const ListActiveSubCourses = `-- name: ListActiveSubCourses :many
|
||||
SELECT
|
||||
id,
|
||||
course_id,
|
||||
title,
|
||||
description,
|
||||
thumbnail,
|
||||
display_order,
|
||||
level,
|
||||
sub_level,
|
||||
is_active
|
||||
FROM sub_courses
|
||||
WHERE is_active = TRUE
|
||||
ORDER BY display_order ASC
|
||||
`
|
||||
|
||||
type ListActiveSubCoursesRow struct {
|
||||
ID int64 `json:"id"`
|
||||
CourseID int64 `json:"course_id"`
|
||||
Title string `json:"title"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||
DisplayOrder int32 `json:"display_order"`
|
||||
Level string `json:"level"`
|
||||
SubLevel string `json:"sub_level"`
|
||||
IsActive bool `json:"is_active"`
|
||||
}
|
||||
|
||||
func (q *Queries) ListActiveSubCourses(ctx context.Context) ([]ListActiveSubCoursesRow, error) {
|
||||
rows, err := q.db.Query(ctx, ListActiveSubCourses)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []ListActiveSubCoursesRow
|
||||
for rows.Next() {
|
||||
var i ListActiveSubCoursesRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.CourseID,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.Thumbnail,
|
||||
&i.DisplayOrder,
|
||||
&i.Level,
|
||||
&i.SubLevel,
|
||||
&i.IsActive,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const ListSubCoursesByCourse = `-- name: ListSubCoursesByCourse :many
|
||||
SELECT
|
||||
id,
|
||||
course_id,
|
||||
title,
|
||||
description,
|
||||
thumbnail,
|
||||
display_order,
|
||||
level,
|
||||
sub_level,
|
||||
is_active
|
||||
FROM sub_courses
|
||||
WHERE course_id = $1
|
||||
AND is_active = TRUE
|
||||
ORDER BY display_order ASC, id ASC
|
||||
`
|
||||
|
||||
type ListSubCoursesByCourseRow struct {
|
||||
ID int64 `json:"id"`
|
||||
CourseID int64 `json:"course_id"`
|
||||
Title string `json:"title"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||
DisplayOrder int32 `json:"display_order"`
|
||||
Level string `json:"level"`
|
||||
SubLevel string `json:"sub_level"`
|
||||
IsActive bool `json:"is_active"`
|
||||
}
|
||||
|
||||
func (q *Queries) ListSubCoursesByCourse(ctx context.Context, courseID int64) ([]ListSubCoursesByCourseRow, error) {
|
||||
rows, err := q.db.Query(ctx, ListSubCoursesByCourse, courseID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []ListSubCoursesByCourseRow
|
||||
for rows.Next() {
|
||||
var i ListSubCoursesByCourseRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.CourseID,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.Thumbnail,
|
||||
&i.DisplayOrder,
|
||||
&i.Level,
|
||||
&i.SubLevel,
|
||||
&i.IsActive,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const ReorderSubCourses = `-- name: ReorderSubCourses :exec
|
||||
UPDATE sub_courses
|
||||
SET display_order = bulk.position
|
||||
FROM (
|
||||
SELECT unnest($1::BIGINT[]) AS id, unnest($2::INT[]) AS position
|
||||
) AS bulk
|
||||
WHERE sub_courses.id = bulk.id
|
||||
`
|
||||
|
||||
type ReorderSubCoursesParams struct {
|
||||
Ids []int64 `json:"ids"`
|
||||
Positions []int32 `json:"positions"`
|
||||
}
|
||||
|
||||
func (q *Queries) ReorderSubCourses(ctx context.Context, arg ReorderSubCoursesParams) error {
|
||||
_, err := q.db.Exec(ctx, ReorderSubCourses, arg.Ids, arg.Positions)
|
||||
return err
|
||||
}
|
||||
|
||||
const UpdateSubCourse = `-- name: UpdateSubCourse :exec
|
||||
UPDATE sub_courses
|
||||
SET
|
||||
title = COALESCE($1, title),
|
||||
description = COALESCE($2, description),
|
||||
thumbnail = COALESCE($3, thumbnail),
|
||||
display_order = COALESCE($4, display_order),
|
||||
level = COALESCE($5, level),
|
||||
sub_level = COALESCE($6, sub_level),
|
||||
is_active = COALESCE($7, is_active)
|
||||
WHERE id = $8
|
||||
`
|
||||
|
||||
type UpdateSubCourseParams struct {
|
||||
Title string `json:"title"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||
DisplayOrder int32 `json:"display_order"`
|
||||
Level string `json:"level"`
|
||||
SubLevel string `json:"sub_level"`
|
||||
IsActive bool `json:"is_active"`
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateSubCourse(ctx context.Context, arg UpdateSubCourseParams) error {
|
||||
_, err := q.db.Exec(ctx, UpdateSubCourse,
|
||||
arg.Title,
|
||||
arg.Description,
|
||||
arg.Thumbnail,
|
||||
arg.DisplayOrder,
|
||||
arg.Level,
|
||||
arg.SubLevel,
|
||||
arg.IsActive,
|
||||
arg.ID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
|
@ -1,102 +0,0 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
// source: user_practice_progress.sql
|
||||
|
||||
package dbgen
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
const GetFirstIncompletePreviousPractice = `-- name: GetFirstIncompletePreviousPractice :one
|
||||
SELECT
|
||||
p.id,
|
||||
p.title,
|
||||
p.display_order
|
||||
FROM question_sets target
|
||||
JOIN question_sets p
|
||||
ON p.owner_type = 'SUB_COURSE'
|
||||
AND p.owner_id = target.owner_id
|
||||
AND p.set_type = 'PRACTICE'
|
||||
AND p.status = 'PUBLISHED'
|
||||
AND (
|
||||
p.display_order < target.display_order OR
|
||||
(p.display_order = target.display_order AND p.id < target.id)
|
||||
)
|
||||
LEFT JOIN user_practice_progress upp
|
||||
ON upp.question_set_id = p.id
|
||||
AND upp.user_id = $1::BIGINT
|
||||
AND upp.completed_at IS NOT NULL
|
||||
WHERE target.id = $2::BIGINT
|
||||
AND target.set_type = 'PRACTICE'
|
||||
AND target.owner_type = 'SUB_COURSE'
|
||||
AND target.status = 'PUBLISHED'
|
||||
AND upp.question_set_id IS NULL
|
||||
ORDER BY p.display_order ASC, p.id ASC
|
||||
LIMIT 1
|
||||
`
|
||||
|
||||
type GetFirstIncompletePreviousPracticeParams struct {
|
||||
UserID int64 `json:"user_id"`
|
||||
QuestionSetID int64 `json:"question_set_id"`
|
||||
}
|
||||
|
||||
type GetFirstIncompletePreviousPracticeRow struct {
|
||||
ID int64 `json:"id"`
|
||||
Title string `json:"title"`
|
||||
DisplayOrder int32 `json:"display_order"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetFirstIncompletePreviousPractice(ctx context.Context, arg GetFirstIncompletePreviousPracticeParams) (GetFirstIncompletePreviousPracticeRow, error) {
|
||||
row := q.db.QueryRow(ctx, GetFirstIncompletePreviousPractice, arg.UserID, arg.QuestionSetID)
|
||||
var i GetFirstIncompletePreviousPracticeRow
|
||||
err := row.Scan(&i.ID, &i.Title, &i.DisplayOrder)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const MarkPracticeCompleted = `-- name: MarkPracticeCompleted :one
|
||||
INSERT INTO user_practice_progress (
|
||||
user_id,
|
||||
sub_course_id,
|
||||
question_set_id,
|
||||
completed_at,
|
||||
updated_at
|
||||
)
|
||||
SELECT
|
||||
$1::BIGINT,
|
||||
qs.owner_id::BIGINT,
|
||||
qs.id,
|
||||
CURRENT_TIMESTAMP,
|
||||
CURRENT_TIMESTAMP
|
||||
FROM question_sets qs
|
||||
WHERE qs.id = $2::BIGINT
|
||||
AND qs.set_type = 'PRACTICE'
|
||||
AND qs.owner_type = 'SUB_COURSE'
|
||||
AND qs.status = 'PUBLISHED'
|
||||
ON CONFLICT (user_id, question_set_id)
|
||||
DO UPDATE SET
|
||||
completed_at = CURRENT_TIMESTAMP,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
RETURNING id, user_id, sub_course_id, question_set_id, completed_at, created_at, updated_at
|
||||
`
|
||||
|
||||
type MarkPracticeCompletedParams struct {
|
||||
UserID int64 `json:"user_id"`
|
||||
QuestionSetID int64 `json:"question_set_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) MarkPracticeCompleted(ctx context.Context, arg MarkPracticeCompletedParams) (UserPracticeProgress, error) {
|
||||
row := q.db.QueryRow(ctx, MarkPracticeCompleted, arg.UserID, arg.QuestionSetID)
|
||||
var i UserPracticeProgress
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.UserID,
|
||||
&i.SubCourseID,
|
||||
&i.QuestionSetID,
|
||||
&i.CompletedAt,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -1,279 +0,0 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
// source: user_sub_course_progress.sql
|
||||
|
||||
package dbgen
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const CompleteSubCourse = `-- name: CompleteSubCourse :exec
|
||||
UPDATE user_sub_course_progress
|
||||
SET
|
||||
status = 'COMPLETED',
|
||||
progress_percentage = 100,
|
||||
completed_at = CURRENT_TIMESTAMP,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE user_id = $1 AND sub_course_id = $2
|
||||
`
|
||||
|
||||
type CompleteSubCourseParams struct {
|
||||
UserID int64 `json:"user_id"`
|
||||
SubCourseID int64 `json:"sub_course_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) CompleteSubCourse(ctx context.Context, arg CompleteSubCourseParams) error {
|
||||
_, err := q.db.Exec(ctx, CompleteSubCourse, arg.UserID, arg.SubCourseID)
|
||||
return err
|
||||
}
|
||||
|
||||
const DeleteUserSubCourseProgress = `-- name: DeleteUserSubCourseProgress :exec
|
||||
DELETE FROM user_sub_course_progress
|
||||
WHERE user_id = $1 AND sub_course_id = $2
|
||||
`
|
||||
|
||||
type DeleteUserSubCourseProgressParams struct {
|
||||
UserID int64 `json:"user_id"`
|
||||
SubCourseID int64 `json:"sub_course_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) DeleteUserSubCourseProgress(ctx context.Context, arg DeleteUserSubCourseProgressParams) error {
|
||||
_, err := q.db.Exec(ctx, DeleteUserSubCourseProgress, arg.UserID, arg.SubCourseID)
|
||||
return err
|
||||
}
|
||||
|
||||
const GetSubCoursesWithProgressByCourse = `-- name: GetSubCoursesWithProgressByCourse :many
|
||||
SELECT
|
||||
sc.id AS sub_course_id,
|
||||
sc.title,
|
||||
sc.description,
|
||||
sc.thumbnail,
|
||||
sc.display_order,
|
||||
sc.level,
|
||||
sc.is_active,
|
||||
COALESCE(usp.status, 'NOT_STARTED') AS progress_status,
|
||||
COALESCE(usp.progress_percentage, 0)::smallint AS progress_percentage,
|
||||
usp.started_at,
|
||||
usp.completed_at,
|
||||
(SELECT COUNT(*)::bigint
|
||||
FROM sub_course_prerequisites p
|
||||
WHERE p.sub_course_id = sc.id
|
||||
AND p.prerequisite_sub_course_id NOT IN (
|
||||
SELECT usp2.sub_course_id
|
||||
FROM user_sub_course_progress usp2
|
||||
WHERE usp2.user_id = $1
|
||||
AND usp2.status = 'COMPLETED'
|
||||
)
|
||||
) AS unmet_prerequisites_count
|
||||
FROM sub_courses sc
|
||||
LEFT JOIN user_sub_course_progress usp
|
||||
ON usp.sub_course_id = sc.id AND usp.user_id = $1
|
||||
WHERE sc.course_id = $2
|
||||
AND sc.is_active = true
|
||||
ORDER BY sc.display_order
|
||||
`
|
||||
|
||||
type GetSubCoursesWithProgressByCourseParams struct {
|
||||
UserID int64 `json:"user_id"`
|
||||
CourseID int64 `json:"course_id"`
|
||||
}
|
||||
|
||||
type GetSubCoursesWithProgressByCourseRow struct {
|
||||
SubCourseID int64 `json:"sub_course_id"`
|
||||
Title string `json:"title"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||
DisplayOrder int32 `json:"display_order"`
|
||||
Level string `json:"level"`
|
||||
IsActive bool `json:"is_active"`
|
||||
ProgressStatus string `json:"progress_status"`
|
||||
ProgressPercentage int16 `json:"progress_percentage"`
|
||||
StartedAt pgtype.Timestamptz `json:"started_at"`
|
||||
CompletedAt pgtype.Timestamptz `json:"completed_at"`
|
||||
UnmetPrerequisitesCount int64 `json:"unmet_prerequisites_count"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetSubCoursesWithProgressByCourse(ctx context.Context, arg GetSubCoursesWithProgressByCourseParams) ([]GetSubCoursesWithProgressByCourseRow, error) {
|
||||
rows, err := q.db.Query(ctx, GetSubCoursesWithProgressByCourse, arg.UserID, arg.CourseID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetSubCoursesWithProgressByCourseRow
|
||||
for rows.Next() {
|
||||
var i GetSubCoursesWithProgressByCourseRow
|
||||
if err := rows.Scan(
|
||||
&i.SubCourseID,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.Thumbnail,
|
||||
&i.DisplayOrder,
|
||||
&i.Level,
|
||||
&i.IsActive,
|
||||
&i.ProgressStatus,
|
||||
&i.ProgressPercentage,
|
||||
&i.StartedAt,
|
||||
&i.CompletedAt,
|
||||
&i.UnmetPrerequisitesCount,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const GetUserCourseProgress = `-- name: GetUserCourseProgress :many
|
||||
SELECT
|
||||
usp.id,
|
||||
usp.user_id,
|
||||
usp.sub_course_id,
|
||||
usp.status,
|
||||
usp.progress_percentage,
|
||||
usp.started_at,
|
||||
usp.completed_at,
|
||||
usp.created_at,
|
||||
usp.updated_at,
|
||||
sc.title AS sub_course_title,
|
||||
sc.level AS sub_course_level,
|
||||
sc.display_order AS sub_course_display_order
|
||||
FROM user_sub_course_progress usp
|
||||
JOIN sub_courses sc ON sc.id = usp.sub_course_id
|
||||
WHERE usp.user_id = $1 AND sc.course_id = $2
|
||||
ORDER BY sc.display_order
|
||||
`
|
||||
|
||||
type GetUserCourseProgressParams struct {
|
||||
UserID int64 `json:"user_id"`
|
||||
CourseID int64 `json:"course_id"`
|
||||
}
|
||||
|
||||
type GetUserCourseProgressRow struct {
|
||||
ID int64 `json:"id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
SubCourseID int64 `json:"sub_course_id"`
|
||||
Status string `json:"status"`
|
||||
ProgressPercentage int16 `json:"progress_percentage"`
|
||||
StartedAt pgtype.Timestamptz `json:"started_at"`
|
||||
CompletedAt pgtype.Timestamptz `json:"completed_at"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
SubCourseTitle string `json:"sub_course_title"`
|
||||
SubCourseLevel string `json:"sub_course_level"`
|
||||
SubCourseDisplayOrder int32 `json:"sub_course_display_order"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetUserCourseProgress(ctx context.Context, arg GetUserCourseProgressParams) ([]GetUserCourseProgressRow, error) {
|
||||
rows, err := q.db.Query(ctx, GetUserCourseProgress, arg.UserID, arg.CourseID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetUserCourseProgressRow
|
||||
for rows.Next() {
|
||||
var i GetUserCourseProgressRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.UserID,
|
||||
&i.SubCourseID,
|
||||
&i.Status,
|
||||
&i.ProgressPercentage,
|
||||
&i.StartedAt,
|
||||
&i.CompletedAt,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.SubCourseTitle,
|
||||
&i.SubCourseLevel,
|
||||
&i.SubCourseDisplayOrder,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const GetUserSubCourseProgress = `-- name: GetUserSubCourseProgress :one
|
||||
SELECT id, user_id, sub_course_id, status, progress_percentage, started_at, completed_at, created_at, updated_at FROM user_sub_course_progress
|
||||
WHERE user_id = $1 AND sub_course_id = $2
|
||||
`
|
||||
|
||||
type GetUserSubCourseProgressParams struct {
|
||||
UserID int64 `json:"user_id"`
|
||||
SubCourseID int64 `json:"sub_course_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetUserSubCourseProgress(ctx context.Context, arg GetUserSubCourseProgressParams) (UserSubCourseProgress, error) {
|
||||
row := q.db.QueryRow(ctx, GetUserSubCourseProgress, arg.UserID, arg.SubCourseID)
|
||||
var i UserSubCourseProgress
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.UserID,
|
||||
&i.SubCourseID,
|
||||
&i.Status,
|
||||
&i.ProgressPercentage,
|
||||
&i.StartedAt,
|
||||
&i.CompletedAt,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const StartSubCourseProgress = `-- name: StartSubCourseProgress :one
|
||||
INSERT INTO user_sub_course_progress (user_id, sub_course_id)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT (user_id, sub_course_id) DO NOTHING
|
||||
RETURNING id, user_id, sub_course_id, status, progress_percentage, started_at, completed_at, created_at, updated_at
|
||||
`
|
||||
|
||||
type StartSubCourseProgressParams struct {
|
||||
UserID int64 `json:"user_id"`
|
||||
SubCourseID int64 `json:"sub_course_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) StartSubCourseProgress(ctx context.Context, arg StartSubCourseProgressParams) (UserSubCourseProgress, error) {
|
||||
row := q.db.QueryRow(ctx, StartSubCourseProgress, arg.UserID, arg.SubCourseID)
|
||||
var i UserSubCourseProgress
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.UserID,
|
||||
&i.SubCourseID,
|
||||
&i.Status,
|
||||
&i.ProgressPercentage,
|
||||
&i.StartedAt,
|
||||
&i.CompletedAt,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const UpdateSubCourseProgress = `-- name: UpdateSubCourseProgress :exec
|
||||
UPDATE user_sub_course_progress
|
||||
SET
|
||||
progress_percentage = $1,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE user_id = $2 AND sub_course_id = $3
|
||||
`
|
||||
|
||||
type UpdateSubCourseProgressParams struct {
|
||||
ProgressPercentage int16 `json:"progress_percentage"`
|
||||
UserID int64 `json:"user_id"`
|
||||
SubCourseID int64 `json:"sub_course_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateSubCourseProgress(ctx context.Context, arg UpdateSubCourseProgressParams) error {
|
||||
_, err := q.db.Exec(ctx, UpdateSubCourseProgress, arg.ProgressPercentage, arg.UserID, arg.SubCourseID)
|
||||
return err
|
||||
}
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
// source: user_sub_course_video_progress.sql
|
||||
|
||||
package dbgen
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
const GetFirstIncompletePreviousVideo = `-- name: GetFirstIncompletePreviousVideo :one
|
||||
SELECT
|
||||
v.id,
|
||||
v.title,
|
||||
v.display_order
|
||||
FROM sub_course_videos target
|
||||
JOIN sub_course_videos v
|
||||
ON v.sub_course_id = target.sub_course_id
|
||||
AND v.status = 'PUBLISHED'
|
||||
AND (
|
||||
v.display_order < target.display_order OR
|
||||
(v.display_order = target.display_order AND v.id < target.id)
|
||||
)
|
||||
LEFT JOIN user_sub_course_video_progress p
|
||||
ON p.video_id = v.id
|
||||
AND p.user_id = $1::BIGINT
|
||||
AND p.completed_at IS NOT NULL
|
||||
WHERE target.id = $2::BIGINT
|
||||
AND p.video_id IS NULL
|
||||
ORDER BY v.display_order ASC, v.id ASC
|
||||
LIMIT 1
|
||||
`
|
||||
|
||||
type GetFirstIncompletePreviousVideoParams struct {
|
||||
UserID int64 `json:"user_id"`
|
||||
VideoID int64 `json:"video_id"`
|
||||
}
|
||||
|
||||
type GetFirstIncompletePreviousVideoRow struct {
|
||||
ID int64 `json:"id"`
|
||||
Title string `json:"title"`
|
||||
DisplayOrder int32 `json:"display_order"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetFirstIncompletePreviousVideo(ctx context.Context, arg GetFirstIncompletePreviousVideoParams) (GetFirstIncompletePreviousVideoRow, error) {
|
||||
row := q.db.QueryRow(ctx, GetFirstIncompletePreviousVideo, arg.UserID, arg.VideoID)
|
||||
var i GetFirstIncompletePreviousVideoRow
|
||||
err := row.Scan(&i.ID, &i.Title, &i.DisplayOrder)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const MarkVideoCompleted = `-- name: MarkVideoCompleted :one
|
||||
INSERT INTO user_sub_course_video_progress (
|
||||
user_id,
|
||||
sub_course_id,
|
||||
video_id,
|
||||
completed_at,
|
||||
updated_at
|
||||
)
|
||||
SELECT
|
||||
$1::BIGINT,
|
||||
v.sub_course_id,
|
||||
v.id,
|
||||
CURRENT_TIMESTAMP,
|
||||
CURRENT_TIMESTAMP
|
||||
FROM sub_course_videos v
|
||||
WHERE v.id = $2::BIGINT
|
||||
AND v.status = 'PUBLISHED'
|
||||
ON CONFLICT (user_id, video_id)
|
||||
DO UPDATE SET
|
||||
completed_at = CURRENT_TIMESTAMP,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
RETURNING id, user_id, sub_course_id, video_id, completed_at, created_at, updated_at
|
||||
`
|
||||
|
||||
type MarkVideoCompletedParams struct {
|
||||
UserID int64 `json:"user_id"`
|
||||
VideoID int64 `json:"video_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) MarkVideoCompleted(ctx context.Context, arg MarkVideoCompletedParams) (UserSubCourseVideoProgress, error) {
|
||||
row := q.db.QueryRow(ctx, MarkVideoCompleted, arg.UserID, arg.VideoID)
|
||||
var i UserSubCourseVideoProgress
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.UserID,
|
||||
&i.SubCourseID,
|
||||
&i.VideoID,
|
||||
&i.CompletedAt,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -3,13 +3,13 @@ package repository
|
|||
import (
|
||||
dbgen "Yimaru-Backend/gen/db"
|
||||
"Yimaru-Backend/internal/domain"
|
||||
"Yimaru-Backend/internal/ports"
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
func NewCourseStore(s *Store) ports.CourseStore { return s }
|
||||
func NewCourseStore(s *Store) *Store { return s }
|
||||
func NewProgressionStore(s *Store) *Store { return s }
|
||||
|
||||
func (s *Store) CreateCourseCategory(
|
||||
ctx context.Context,
|
||||
|
|
|
|||
|
|
@ -1,162 +0,0 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"Yimaru-Backend/internal/domain"
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
func (s *Store) GetFullLearningTree(ctx context.Context) ([]domain.TreeCourse, error) {
|
||||
rows, err := s.queries.GetFullLearningTree(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
coursesMap := make(map[int64]*domain.TreeCourse)
|
||||
|
||||
for _, row := range rows {
|
||||
course, ok := coursesMap[row.CourseID]
|
||||
if !ok {
|
||||
course = &domain.TreeCourse{
|
||||
ID: row.CourseID,
|
||||
Title: row.CourseTitle,
|
||||
SubCourses: []domain.TreeSubCourse{},
|
||||
}
|
||||
coursesMap[row.CourseID] = course
|
||||
}
|
||||
|
||||
if row.SubCourseID.Valid {
|
||||
subCourse := domain.TreeSubCourse{
|
||||
ID: row.SubCourseID.Int64,
|
||||
Title: row.SubCourseTitle.String,
|
||||
Level: row.SubCourseLevel.String,
|
||||
SubLevel: row.SubCourseSubLevel.String,
|
||||
}
|
||||
course.SubCourses = append(course.SubCourses, subCourse)
|
||||
}
|
||||
}
|
||||
|
||||
courses := make([]domain.TreeCourse, 0, len(coursesMap))
|
||||
for _, course := range coursesMap {
|
||||
courses = append(courses, *course)
|
||||
}
|
||||
|
||||
return courses, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetCourseLearningPath(ctx context.Context, courseID int64) (domain.LearningPath, error) {
|
||||
rows, err := s.queries.GetCourseLearningPath(ctx, courseID)
|
||||
if err != nil {
|
||||
return domain.LearningPath{}, err
|
||||
}
|
||||
if len(rows) == 0 {
|
||||
return domain.LearningPath{}, fmt.Errorf("course not found")
|
||||
}
|
||||
|
||||
first := rows[0]
|
||||
path := domain.LearningPath{
|
||||
CourseID: first.CourseID,
|
||||
CourseTitle: first.CourseTitle,
|
||||
Description: ptrString(first.CourseDescription),
|
||||
Thumbnail: ptrString(first.CourseThumbnail),
|
||||
IntroVideoURL: ptrString(first.CourseIntroVideoUrl),
|
||||
CategoryID: first.CategoryID,
|
||||
CategoryName: first.CategoryName,
|
||||
SubCourses: []domain.LearningPathSubCourse{},
|
||||
}
|
||||
|
||||
for _, row := range rows {
|
||||
if !row.SubCourseID.Valid {
|
||||
continue
|
||||
}
|
||||
|
||||
scID := row.SubCourseID.Int64
|
||||
|
||||
// Fetch prerequisites, videos, practices for this sub-course
|
||||
prerequisites, _ := s.getSubCoursePrerequisitesForPath(ctx, scID)
|
||||
videos, _ := s.getSubCourseVideosForPath(ctx, scID)
|
||||
practices, _ := s.getSubCoursePracticesForPath(ctx, scID)
|
||||
|
||||
sc := domain.LearningPathSubCourse{
|
||||
ID: scID,
|
||||
Title: row.SubCourseTitle.String,
|
||||
Description: ptrString(row.SubCourseDescription),
|
||||
Thumbnail: ptrString(row.SubCourseThumbnail),
|
||||
DisplayOrder: row.SubCourseDisplayOrder.Int32,
|
||||
Level: row.SubCourseLevel.String,
|
||||
SubLevel: row.SubCourseSubLevel.String,
|
||||
PrerequisiteCount: row.PrerequisiteCount,
|
||||
VideoCount: row.VideoCount,
|
||||
PracticeCount: row.PracticeCount,
|
||||
Prerequisites: prerequisites,
|
||||
Videos: videos,
|
||||
Practices: practices,
|
||||
}
|
||||
path.SubCourses = append(path.SubCourses, sc)
|
||||
}
|
||||
|
||||
return path, nil
|
||||
}
|
||||
|
||||
func (s *Store) getSubCoursePrerequisitesForPath(ctx context.Context, subCourseID int64) ([]domain.LearningPathPrerequisite, error) {
|
||||
rows, err := s.queries.GetSubCoursePrerequisitesForLearningPath(ctx, subCourseID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make([]domain.LearningPathPrerequisite, len(rows))
|
||||
for i, row := range rows {
|
||||
result[i] = domain.LearningPathPrerequisite{
|
||||
SubCourseID: row.PrerequisiteSubCourseID,
|
||||
Title: row.Title,
|
||||
Level: row.Level,
|
||||
SubLevel: row.SubLevel,
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *Store) getSubCourseVideosForPath(ctx context.Context, subCourseID int64) ([]domain.LearningPathVideo, error) {
|
||||
rows, err := s.queries.GetSubCourseVideosForLearningPath(ctx, subCourseID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make([]domain.LearningPathVideo, len(rows))
|
||||
for i, row := range rows {
|
||||
result[i] = domain.LearningPathVideo{
|
||||
ID: row.ID,
|
||||
Title: row.Title,
|
||||
Description: ptrString(row.Description),
|
||||
VideoURL: row.VideoUrl,
|
||||
Duration: row.Duration,
|
||||
Resolution: ptrString(row.Resolution),
|
||||
DisplayOrder: row.DisplayOrder,
|
||||
VimeoID: ptrString(row.VimeoID),
|
||||
VimeoEmbedURL: ptrString(row.VimeoEmbedUrl),
|
||||
VideoHostProvider: ptrString(row.VideoHostProvider),
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *Store) getSubCoursePracticesForPath(ctx context.Context, subCourseID int64) ([]domain.LearningPathPractice, error) {
|
||||
ownerID := pgtype.Int8{Int64: subCourseID, Valid: true}
|
||||
rows, err := s.queries.GetSubCoursePracticesForLearningPath(ctx, ownerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make([]domain.LearningPathPractice, len(rows))
|
||||
for i, row := range rows {
|
||||
result[i] = domain.LearningPathPractice{
|
||||
ID: row.ID,
|
||||
Title: row.Title,
|
||||
Description: ptrString(row.Description),
|
||||
Persona: ptrString(row.Persona),
|
||||
Status: row.Status,
|
||||
IntroVideoURL: ptrString(row.IntroVideoUrl),
|
||||
QuestionCount: row.QuestionCount,
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
|
@ -1,310 +0,0 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
dbgen "Yimaru-Backend/gen/db"
|
||||
"Yimaru-Backend/internal/domain"
|
||||
"Yimaru-Backend/internal/ports"
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
func NewProgressionStore(s *Store) ports.ProgressionStore { return s }
|
||||
|
||||
func (s *Store) AddSubCoursePrerequisite(ctx context.Context, subCourseID, prerequisiteSubCourseID int64) error {
|
||||
_, err := s.queries.AddSubCoursePrerequisite(ctx, dbgen.AddSubCoursePrerequisiteParams{
|
||||
SubCourseID: subCourseID,
|
||||
PrerequisiteSubCourseID: prerequisiteSubCourseID,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Store) RemoveSubCoursePrerequisite(ctx context.Context, subCourseID, prerequisiteSubCourseID int64) error {
|
||||
return s.queries.RemoveSubCoursePrerequisite(ctx, dbgen.RemoveSubCoursePrerequisiteParams{
|
||||
SubCourseID: subCourseID,
|
||||
PrerequisiteSubCourseID: prerequisiteSubCourseID,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Store) GetSubCoursePrerequisites(ctx context.Context, subCourseID int64) ([]domain.SubCoursePrerequisite, error) {
|
||||
rows, err := s.queries.GetSubCoursePrerequisites(ctx, subCourseID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
prereqs := make([]domain.SubCoursePrerequisite, len(rows))
|
||||
for i, row := range rows {
|
||||
prereqs[i] = domain.SubCoursePrerequisite{
|
||||
ID: row.ID,
|
||||
SubCourseID: row.SubCourseID,
|
||||
PrerequisiteSubCourseID: row.PrerequisiteSubCourseID,
|
||||
CreatedAt: row.CreatedAt.Time,
|
||||
PrerequisiteTitle: row.PrerequisiteTitle,
|
||||
PrerequisiteLevel: row.PrerequisiteLevel,
|
||||
PrerequisiteDisplayOrder: row.PrerequisiteDisplayOrder,
|
||||
}
|
||||
}
|
||||
return prereqs, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetSubCourseDependents(ctx context.Context, prerequisiteSubCourseID int64) ([]domain.SubCourseDependent, error) {
|
||||
rows, err := s.queries.GetSubCourseDependents(ctx, prerequisiteSubCourseID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
deps := make([]domain.SubCourseDependent, len(rows))
|
||||
for i, row := range rows {
|
||||
deps[i] = domain.SubCourseDependent{
|
||||
ID: row.ID,
|
||||
SubCourseID: row.SubCourseID,
|
||||
PrerequisiteSubCourseID: row.PrerequisiteSubCourseID,
|
||||
CreatedAt: row.CreatedAt.Time,
|
||||
DependentTitle: row.DependentTitle,
|
||||
DependentLevel: row.DependentLevel,
|
||||
}
|
||||
}
|
||||
return deps, nil
|
||||
}
|
||||
|
||||
func (s *Store) CountUnmetPrerequisites(ctx context.Context, subCourseID, userID int64) (int64, error) {
|
||||
return s.queries.CountUnmetPrerequisites(ctx, dbgen.CountUnmetPrerequisitesParams{
|
||||
SubCourseID: subCourseID,
|
||||
UserID: userID,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Store) DeleteAllPrerequisitesForSubCourse(ctx context.Context, subCourseID int64) error {
|
||||
return s.queries.DeleteAllPrerequisitesForSubCourse(ctx, subCourseID)
|
||||
}
|
||||
|
||||
func (s *Store) StartSubCourseProgress(ctx context.Context, userID, subCourseID int64) (domain.UserSubCourseProgress, error) {
|
||||
row, err := s.queries.StartSubCourseProgress(ctx, dbgen.StartSubCourseProgressParams{
|
||||
UserID: userID,
|
||||
SubCourseID: subCourseID,
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return s.GetUserSubCourseProgress(ctx, userID, subCourseID)
|
||||
}
|
||||
return domain.UserSubCourseProgress{}, err
|
||||
}
|
||||
return mapUserSubCourseProgress(row), nil
|
||||
}
|
||||
|
||||
func (s *Store) UpdateSubCourseProgress(ctx context.Context, userID, subCourseID int64, percentage int16) error {
|
||||
return s.queries.UpdateSubCourseProgress(ctx, dbgen.UpdateSubCourseProgressParams{
|
||||
ProgressPercentage: percentage,
|
||||
UserID: userID,
|
||||
SubCourseID: subCourseID,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Store) CompleteSubCourse(ctx context.Context, userID, subCourseID int64) error {
|
||||
return s.queries.CompleteSubCourse(ctx, dbgen.CompleteSubCourseParams{
|
||||
UserID: userID,
|
||||
SubCourseID: subCourseID,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Store) RecalculateSubCourseProgress(ctx context.Context, userID, subCourseID int64) error {
|
||||
const query = `
|
||||
WITH totals AS (
|
||||
SELECT
|
||||
(SELECT COUNT(*)::INT
|
||||
FROM sub_course_videos v
|
||||
WHERE v.sub_course_id = $2
|
||||
AND v.status = 'PUBLISHED') AS total_videos,
|
||||
(SELECT COUNT(*)::INT
|
||||
FROM question_sets qs
|
||||
WHERE qs.owner_type = 'SUB_COURSE'
|
||||
AND qs.owner_id = $2
|
||||
AND qs.set_type = 'PRACTICE'
|
||||
AND qs.status = 'PUBLISHED') AS total_practices
|
||||
),
|
||||
completed AS (
|
||||
SELECT
|
||||
(SELECT COUNT(*)::INT
|
||||
FROM user_sub_course_video_progress uv
|
||||
JOIN sub_course_videos v ON v.id = uv.video_id
|
||||
WHERE uv.user_id = $1
|
||||
AND uv.sub_course_id = $2
|
||||
AND uv.completed_at IS NOT NULL
|
||||
AND v.status = 'PUBLISHED') AS completed_videos,
|
||||
(SELECT COUNT(*)::INT
|
||||
FROM user_practice_progress up
|
||||
JOIN question_sets qs ON qs.id = up.question_set_id
|
||||
WHERE up.user_id = $1
|
||||
AND up.sub_course_id = $2
|
||||
AND up.completed_at IS NOT NULL
|
||||
AND qs.owner_type = 'SUB_COURSE'
|
||||
AND qs.owner_id = $2
|
||||
AND qs.set_type = 'PRACTICE'
|
||||
AND qs.status = 'PUBLISHED') AS completed_practices
|
||||
),
|
||||
stats AS (
|
||||
SELECT
|
||||
(total_videos + total_practices) AS total_items,
|
||||
(completed_videos + completed_practices) AS completed_items
|
||||
FROM totals, completed
|
||||
)
|
||||
INSERT INTO user_sub_course_progress (
|
||||
user_id,
|
||||
sub_course_id,
|
||||
status,
|
||||
progress_percentage,
|
||||
started_at,
|
||||
completed_at,
|
||||
updated_at
|
||||
)
|
||||
SELECT
|
||||
$1,
|
||||
$2,
|
||||
CASE
|
||||
WHEN stats.total_items > 0 AND stats.completed_items >= stats.total_items THEN 'COMPLETED'
|
||||
WHEN stats.completed_items > 0 THEN 'IN_PROGRESS'
|
||||
ELSE 'NOT_STARTED'
|
||||
END,
|
||||
CASE
|
||||
WHEN stats.total_items = 0 THEN 0
|
||||
ELSE ROUND((stats.completed_items::NUMERIC * 100.0) / stats.total_items::NUMERIC)::SMALLINT
|
||||
END,
|
||||
CASE
|
||||
WHEN stats.completed_items > 0 THEN CURRENT_TIMESTAMP
|
||||
ELSE NULL
|
||||
END,
|
||||
CASE
|
||||
WHEN stats.total_items > 0 AND stats.completed_items >= stats.total_items THEN CURRENT_TIMESTAMP
|
||||
ELSE NULL
|
||||
END,
|
||||
CURRENT_TIMESTAMP
|
||||
FROM stats
|
||||
ON CONFLICT (user_id, sub_course_id) DO UPDATE SET
|
||||
status = EXCLUDED.status,
|
||||
progress_percentage = EXCLUDED.progress_percentage,
|
||||
started_at = COALESCE(user_sub_course_progress.started_at, EXCLUDED.started_at),
|
||||
completed_at = EXCLUDED.completed_at,
|
||||
updated_at = EXCLUDED.updated_at;
|
||||
`
|
||||
|
||||
_, err := s.conn.Exec(ctx, query, userID, subCourseID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Store) GetUserSubCourseProgress(ctx context.Context, userID, subCourseID int64) (domain.UserSubCourseProgress, error) {
|
||||
row, err := s.queries.GetUserSubCourseProgress(ctx, dbgen.GetUserSubCourseProgressParams{
|
||||
UserID: userID,
|
||||
SubCourseID: subCourseID,
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return domain.UserSubCourseProgress{}, domain.ErrProgressNotFound
|
||||
}
|
||||
return domain.UserSubCourseProgress{}, err
|
||||
}
|
||||
return mapUserSubCourseProgress(row), nil
|
||||
}
|
||||
|
||||
func (s *Store) GetUserCourseProgress(ctx context.Context, userID, courseID int64) ([]domain.UserCourseProgressItem, error) {
|
||||
rows, err := s.queries.GetUserCourseProgress(ctx, dbgen.GetUserCourseProgressParams{
|
||||
UserID: userID,
|
||||
CourseID: courseID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
items := make([]domain.UserCourseProgressItem, len(rows))
|
||||
for i, row := range rows {
|
||||
var startedAt, completedAt *time.Time
|
||||
if row.StartedAt.Valid {
|
||||
startedAt = &row.StartedAt.Time
|
||||
}
|
||||
if row.CompletedAt.Valid {
|
||||
completedAt = &row.CompletedAt.Time
|
||||
}
|
||||
var updatedAt *time.Time
|
||||
if row.UpdatedAt.Valid {
|
||||
updatedAt = &row.UpdatedAt.Time
|
||||
}
|
||||
items[i] = domain.UserCourseProgressItem{
|
||||
ID: row.ID,
|
||||
UserID: row.UserID,
|
||||
SubCourseID: row.SubCourseID,
|
||||
Status: domain.ProgressStatus(row.Status),
|
||||
ProgressPercentage: row.ProgressPercentage,
|
||||
StartedAt: startedAt,
|
||||
CompletedAt: completedAt,
|
||||
CreatedAt: row.CreatedAt.Time,
|
||||
UpdatedAt: updatedAt,
|
||||
SubCourseTitle: row.SubCourseTitle,
|
||||
SubCourseLevel: row.SubCourseLevel,
|
||||
SubCourseDisplayOrder: row.SubCourseDisplayOrder,
|
||||
}
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetSubCoursesWithProgressByCourse(ctx context.Context, userID, courseID int64) ([]domain.SubCourseWithProgress, error) {
|
||||
rows, err := s.queries.GetSubCoursesWithProgressByCourse(ctx, dbgen.GetSubCoursesWithProgressByCourseParams{
|
||||
UserID: userID,
|
||||
CourseID: courseID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
items := make([]domain.SubCourseWithProgress, len(rows))
|
||||
for i, row := range rows {
|
||||
var startedAt, completedAt *time.Time
|
||||
if row.StartedAt.Valid {
|
||||
startedAt = &row.StartedAt.Time
|
||||
}
|
||||
if row.CompletedAt.Valid {
|
||||
completedAt = &row.CompletedAt.Time
|
||||
}
|
||||
items[i] = domain.SubCourseWithProgress{
|
||||
SubCourseID: row.SubCourseID,
|
||||
Title: row.Title,
|
||||
Description: ptrText(row.Description),
|
||||
Thumbnail: ptrText(row.Thumbnail),
|
||||
DisplayOrder: row.DisplayOrder,
|
||||
Level: row.Level,
|
||||
IsActive: row.IsActive,
|
||||
ProgressStatus: domain.ProgressStatus(row.ProgressStatus),
|
||||
ProgressPercentage: row.ProgressPercentage,
|
||||
StartedAt: startedAt,
|
||||
CompletedAt: completedAt,
|
||||
UnmetPrerequisitesCount: row.UnmetPrerequisitesCount,
|
||||
IsLocked: row.UnmetPrerequisitesCount > 0,
|
||||
}
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func mapUserSubCourseProgress(row dbgen.UserSubCourseProgress) domain.UserSubCourseProgress {
|
||||
var startedAt, completedAt *time.Time
|
||||
if row.StartedAt.Valid {
|
||||
startedAt = &row.StartedAt.Time
|
||||
}
|
||||
if row.CompletedAt.Valid {
|
||||
completedAt = &row.CompletedAt.Time
|
||||
}
|
||||
var updatedAt *time.Time
|
||||
if row.UpdatedAt.Valid {
|
||||
updatedAt = &row.UpdatedAt.Time
|
||||
}
|
||||
return domain.UserSubCourseProgress{
|
||||
ID: row.ID,
|
||||
UserID: row.UserID,
|
||||
SubCourseID: row.SubCourseID,
|
||||
Status: domain.ProgressStatus(row.Status),
|
||||
ProgressPercentage: row.ProgressPercentage,
|
||||
StartedAt: startedAt,
|
||||
CompletedAt: completedAt,
|
||||
CreatedAt: row.CreatedAt.Time,
|
||||
UpdatedAt: updatedAt,
|
||||
}
|
||||
}
|
||||
|
|
@ -1,329 +0,0 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
dbgen "Yimaru-Backend/gen/db"
|
||||
"Yimaru-Backend/internal/domain"
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
func (s *Store) CreateSubCourseVideo(
|
||||
ctx context.Context,
|
||||
subCourseID int64,
|
||||
title string,
|
||||
description *string,
|
||||
videoURL string,
|
||||
duration int32,
|
||||
resolution *string,
|
||||
instructorID *string,
|
||||
thumbnail *string,
|
||||
visibility *string,
|
||||
displayOrder *int32,
|
||||
status *string,
|
||||
vimeoID *string,
|
||||
vimeoEmbedURL *string,
|
||||
vimeoPlayerHTML *string,
|
||||
vimeoStatus *string,
|
||||
videoHostProvider *string,
|
||||
) (domain.SubCourseVideo, error) {
|
||||
var descText, resText, instrText, thumbText, visText, statusText pgtype.Text
|
||||
var vimeoIDText, vimeoEmbedText, vimeoHTMLText, vimeoStatusText, hostProviderText pgtype.Text
|
||||
|
||||
if description != nil {
|
||||
descText = pgtype.Text{String: *description, Valid: true}
|
||||
}
|
||||
if resolution != nil {
|
||||
resText = pgtype.Text{String: *resolution, Valid: true}
|
||||
}
|
||||
if instructorID != nil {
|
||||
instrText = pgtype.Text{String: *instructorID, Valid: true}
|
||||
}
|
||||
if thumbnail != nil {
|
||||
thumbText = pgtype.Text{String: *thumbnail, Valid: true}
|
||||
}
|
||||
if visibility != nil {
|
||||
visText = pgtype.Text{String: *visibility, Valid: true}
|
||||
}
|
||||
if status != nil {
|
||||
statusText = pgtype.Text{String: *status, Valid: true}
|
||||
}
|
||||
if vimeoID != nil {
|
||||
vimeoIDText = pgtype.Text{String: *vimeoID, Valid: true}
|
||||
}
|
||||
if vimeoEmbedURL != nil {
|
||||
vimeoEmbedText = pgtype.Text{String: *vimeoEmbedURL, Valid: true}
|
||||
}
|
||||
if vimeoPlayerHTML != nil {
|
||||
vimeoHTMLText = pgtype.Text{String: *vimeoPlayerHTML, Valid: true}
|
||||
}
|
||||
if vimeoStatus != nil {
|
||||
vimeoStatusText = pgtype.Text{String: *vimeoStatus, Valid: true}
|
||||
}
|
||||
if videoHostProvider != nil {
|
||||
hostProviderText = pgtype.Text{String: *videoHostProvider, Valid: true}
|
||||
}
|
||||
|
||||
var dispOrder pgtype.Int4
|
||||
if displayOrder != nil {
|
||||
dispOrder = pgtype.Int4{Int32: *displayOrder, Valid: true}
|
||||
}
|
||||
|
||||
row, err := s.queries.CreateSubCourseVideo(ctx, dbgen.CreateSubCourseVideoParams{
|
||||
SubCourseID: subCourseID,
|
||||
Title: title,
|
||||
Description: descText,
|
||||
VideoUrl: videoURL,
|
||||
Duration: duration,
|
||||
Resolution: resText,
|
||||
InstructorID: instrText,
|
||||
Thumbnail: thumbText,
|
||||
Visibility: visText,
|
||||
Column10: dispOrder,
|
||||
Column11: statusText,
|
||||
VimeoID: vimeoIDText,
|
||||
VimeoEmbedUrl: vimeoEmbedText,
|
||||
VimeoPlayerHtml: vimeoHTMLText,
|
||||
Column15: vimeoStatusText,
|
||||
Column16: hostProviderText,
|
||||
})
|
||||
if err != nil {
|
||||
return domain.SubCourseVideo{}, err
|
||||
}
|
||||
|
||||
return mapSubCourseVideoRow(row), nil
|
||||
}
|
||||
|
||||
func (s *Store) GetSubCourseVideoByID(
|
||||
ctx context.Context,
|
||||
id int64,
|
||||
) (domain.SubCourseVideo, error) {
|
||||
row, err := s.queries.GetSubCourseVideoByID(ctx, id)
|
||||
if err != nil {
|
||||
return domain.SubCourseVideo{}, err
|
||||
}
|
||||
|
||||
return mapSubCourseVideoRow(row), nil
|
||||
}
|
||||
|
||||
func (s *Store) GetVideosBySubCourse(
|
||||
ctx context.Context,
|
||||
subCourseID int64,
|
||||
) ([]domain.SubCourseVideo, int64, error) {
|
||||
rows, err := s.queries.GetVideosBySubCourse(ctx, subCourseID)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
var (
|
||||
videos []domain.SubCourseVideo
|
||||
totalCount int64
|
||||
)
|
||||
|
||||
for i, row := range rows {
|
||||
if i == 0 {
|
||||
totalCount = row.TotalCount
|
||||
}
|
||||
|
||||
videos = append(videos, domain.SubCourseVideo{
|
||||
ID: row.ID,
|
||||
SubCourseID: row.SubCourseID,
|
||||
Title: row.Title,
|
||||
Description: ptrString(row.Description),
|
||||
VideoURL: row.VideoUrl,
|
||||
Duration: row.Duration,
|
||||
Resolution: ptrString(row.Resolution),
|
||||
InstructorID: ptrString(row.InstructorID),
|
||||
Thumbnail: ptrString(row.Thumbnail),
|
||||
Visibility: ptrString(row.Visibility),
|
||||
DisplayOrder: row.DisplayOrder,
|
||||
IsPublished: row.IsPublished,
|
||||
PublishDate: ptrTimestamptz(row.PublishDate),
|
||||
Status: row.Status,
|
||||
VimeoID: ptrString(row.VimeoID),
|
||||
VimeoEmbedURL: ptrString(row.VimeoEmbedUrl),
|
||||
VimeoPlayerHTML: ptrString(row.VimeoPlayerHtml),
|
||||
VimeoStatus: ptrString(row.VimeoStatus),
|
||||
})
|
||||
}
|
||||
|
||||
return videos, totalCount, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetPublishedVideosBySubCourse(
|
||||
ctx context.Context,
|
||||
subCourseID int64,
|
||||
) ([]domain.SubCourseVideo, error) {
|
||||
rows, err := s.queries.GetPublishedVideosBySubCourse(ctx, subCourseID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
videos := make([]domain.SubCourseVideo, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
videos = append(videos, mapSubCourseVideoRow(row))
|
||||
}
|
||||
|
||||
return videos, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetFirstIncompletePreviousVideo(
|
||||
ctx context.Context,
|
||||
userID int64,
|
||||
videoID int64,
|
||||
) (*domain.VideoAccessBlock, error) {
|
||||
row, err := s.queries.GetFirstIncompletePreviousVideo(ctx, dbgen.GetFirstIncompletePreviousVideoParams{
|
||||
UserID: userID,
|
||||
VideoID: videoID,
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &domain.VideoAccessBlock{
|
||||
VideoID: row.ID,
|
||||
Title: row.Title,
|
||||
DisplayOrder: row.DisplayOrder,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Store) MarkVideoCompleted(
|
||||
ctx context.Context,
|
||||
userID int64,
|
||||
videoID int64,
|
||||
) error {
|
||||
_, err := s.queries.MarkVideoCompleted(ctx, dbgen.MarkVideoCompletedParams{
|
||||
UserID: userID,
|
||||
VideoID: videoID,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Store) PublishSubCourseVideo(
|
||||
ctx context.Context,
|
||||
videoID int64,
|
||||
) error {
|
||||
return s.queries.PublishSubCourseVideo(ctx, videoID)
|
||||
}
|
||||
|
||||
func (s *Store) UpdateSubCourseVideo(
|
||||
ctx context.Context,
|
||||
id int64,
|
||||
title *string,
|
||||
description *string,
|
||||
videoURL *string,
|
||||
duration *int32,
|
||||
resolution *string,
|
||||
visibility *string,
|
||||
thumbnail *string,
|
||||
displayOrder *int32,
|
||||
status *string,
|
||||
) error {
|
||||
var titleVal, descVal, urlVal, resVal, visVal, thumbVal, statusVal string
|
||||
var durationVal, dispOrderVal int32
|
||||
|
||||
if title != nil {
|
||||
titleVal = *title
|
||||
}
|
||||
if description != nil {
|
||||
descVal = *description
|
||||
}
|
||||
if videoURL != nil {
|
||||
urlVal = *videoURL
|
||||
}
|
||||
if duration != nil {
|
||||
durationVal = *duration
|
||||
}
|
||||
if resolution != nil {
|
||||
resVal = *resolution
|
||||
}
|
||||
if visibility != nil {
|
||||
visVal = *visibility
|
||||
}
|
||||
if thumbnail != nil {
|
||||
thumbVal = *thumbnail
|
||||
}
|
||||
if displayOrder != nil {
|
||||
dispOrderVal = *displayOrder
|
||||
}
|
||||
if status != nil {
|
||||
statusVal = *status
|
||||
}
|
||||
|
||||
return s.queries.UpdateSubCourseVideo(ctx, dbgen.UpdateSubCourseVideoParams{
|
||||
Title: titleVal,
|
||||
Description: pgtype.Text{String: descVal, Valid: description != nil},
|
||||
VideoUrl: urlVal,
|
||||
Duration: durationVal,
|
||||
Resolution: pgtype.Text{String: resVal, Valid: resolution != nil},
|
||||
Visibility: pgtype.Text{String: visVal, Valid: visibility != nil},
|
||||
Thumbnail: pgtype.Text{String: thumbVal, Valid: thumbnail != nil},
|
||||
DisplayOrder: dispOrderVal,
|
||||
Status: statusVal,
|
||||
ID: id,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Store) ArchiveSubCourseVideo(
|
||||
ctx context.Context,
|
||||
id int64,
|
||||
) error {
|
||||
return s.queries.ArchiveSubCourseVideo(ctx, id)
|
||||
}
|
||||
|
||||
func (s *Store) DeleteSubCourseVideo(
|
||||
ctx context.Context,
|
||||
id int64,
|
||||
) error {
|
||||
return s.queries.DeleteSubCourseVideo(ctx, id)
|
||||
}
|
||||
|
||||
func mapSubCourseVideoRow(row dbgen.SubCourseVideo) domain.SubCourseVideo {
|
||||
return domain.SubCourseVideo{
|
||||
ID: row.ID,
|
||||
SubCourseID: row.SubCourseID,
|
||||
Title: row.Title,
|
||||
Description: ptrString(row.Description),
|
||||
VideoURL: row.VideoUrl,
|
||||
Duration: row.Duration,
|
||||
Resolution: ptrString(row.Resolution),
|
||||
InstructorID: ptrString(row.InstructorID),
|
||||
Thumbnail: ptrString(row.Thumbnail),
|
||||
Visibility: ptrString(row.Visibility),
|
||||
DisplayOrder: row.DisplayOrder,
|
||||
IsPublished: row.IsPublished,
|
||||
PublishDate: ptrTimestamptz(row.PublishDate),
|
||||
Status: row.Status,
|
||||
VimeoID: ptrString(row.VimeoID),
|
||||
VimeoEmbedURL: ptrString(row.VimeoEmbedUrl),
|
||||
VimeoPlayerHTML: ptrString(row.VimeoPlayerHtml),
|
||||
VimeoStatus: ptrString(row.VimeoStatus),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Store) UpdateVimeoStatus(ctx context.Context, videoID int64, status string) error {
|
||||
return s.queries.UpdateVimeoStatus(ctx, dbgen.UpdateVimeoStatusParams{
|
||||
VimeoStatus: pgtype.Text{String: status, Valid: true},
|
||||
ID: videoID,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Store) GetVideoByVimeoID(ctx context.Context, vimeoID string) (domain.SubCourseVideo, error) {
|
||||
row, err := s.queries.GetVideosByVimeoID(ctx, pgtype.Text{String: vimeoID, Valid: true})
|
||||
if err != nil {
|
||||
return domain.SubCourseVideo{}, err
|
||||
}
|
||||
return mapSubCourseVideoRow(row), nil
|
||||
}
|
||||
|
||||
func (s *Store) ReorderSubCourseVideos(ctx context.Context, ids []int64, positions []int32) error {
|
||||
return s.queries.ReorderSubCourseVideos(ctx, dbgen.ReorderSubCourseVideosParams{
|
||||
Ids: ids,
|
||||
Positions: positions,
|
||||
})
|
||||
}
|
||||
|
|
@ -1,256 +0,0 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
dbgen "Yimaru-Backend/gen/db"
|
||||
"Yimaru-Backend/internal/domain"
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
func (s *Store) CreateSubCourse(
|
||||
ctx context.Context,
|
||||
courseID int64,
|
||||
title string,
|
||||
description *string,
|
||||
thumbnail *string,
|
||||
displayOrder *int32,
|
||||
level string,
|
||||
subLevel string,
|
||||
) (domain.SubCourse, error) {
|
||||
var descText, thumbText pgtype.Text
|
||||
if description != nil {
|
||||
descText = pgtype.Text{String: *description, Valid: true}
|
||||
}
|
||||
if thumbnail != nil {
|
||||
thumbText = pgtype.Text{String: *thumbnail, Valid: true}
|
||||
}
|
||||
|
||||
var dispOrder pgtype.Int4
|
||||
if displayOrder != nil {
|
||||
dispOrder = pgtype.Int4{Int32: *displayOrder, Valid: true}
|
||||
}
|
||||
|
||||
row, err := s.queries.CreateSubCourse(ctx, dbgen.CreateSubCourseParams{
|
||||
CourseID: courseID,
|
||||
Title: title,
|
||||
Description: descText,
|
||||
Thumbnail: thumbText,
|
||||
Column5: dispOrder,
|
||||
Level: level,
|
||||
SubLevel: subLevel,
|
||||
Column8: pgtype.Bool{Bool: true, Valid: true},
|
||||
})
|
||||
if err != nil {
|
||||
return domain.SubCourse{}, err
|
||||
}
|
||||
|
||||
return domain.SubCourse{
|
||||
ID: row.ID,
|
||||
CourseID: row.CourseID,
|
||||
Title: row.Title,
|
||||
Description: ptrString(row.Description),
|
||||
Thumbnail: ptrString(row.Thumbnail),
|
||||
DisplayOrder: row.DisplayOrder,
|
||||
Level: row.Level,
|
||||
SubLevel: row.SubLevel,
|
||||
IsActive: row.IsActive,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetSubCourseByID(
|
||||
ctx context.Context,
|
||||
id int64,
|
||||
) (domain.SubCourse, error) {
|
||||
row, err := s.queries.GetSubCourseByID(ctx, id)
|
||||
if err != nil {
|
||||
return domain.SubCourse{}, err
|
||||
}
|
||||
|
||||
return domain.SubCourse{
|
||||
ID: row.ID,
|
||||
CourseID: row.CourseID,
|
||||
Title: row.Title,
|
||||
Description: ptrString(row.Description),
|
||||
Thumbnail: ptrString(row.Thumbnail),
|
||||
DisplayOrder: row.DisplayOrder,
|
||||
Level: row.Level,
|
||||
SubLevel: row.SubLevel,
|
||||
IsActive: row.IsActive,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetSubCoursesByCourse(
|
||||
ctx context.Context,
|
||||
courseID int64,
|
||||
) ([]domain.SubCourse, int64, error) {
|
||||
rows, err := s.queries.GetSubCoursesByCourse(ctx, courseID)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
var (
|
||||
subCourses []domain.SubCourse
|
||||
totalCount int64
|
||||
)
|
||||
|
||||
for i, row := range rows {
|
||||
if i == 0 {
|
||||
totalCount = row.TotalCount
|
||||
}
|
||||
|
||||
subCourses = append(subCourses, domain.SubCourse{
|
||||
ID: row.ID,
|
||||
CourseID: row.CourseID,
|
||||
Title: row.Title,
|
||||
Description: ptrString(row.Description),
|
||||
Thumbnail: ptrString(row.Thumbnail),
|
||||
DisplayOrder: row.DisplayOrder,
|
||||
Level: row.Level,
|
||||
SubLevel: row.SubLevel,
|
||||
IsActive: row.IsActive,
|
||||
})
|
||||
}
|
||||
|
||||
return subCourses, totalCount, nil
|
||||
}
|
||||
|
||||
func (s *Store) ListSubCoursesByCourse(
|
||||
ctx context.Context,
|
||||
courseID int64,
|
||||
) ([]domain.SubCourse, error) {
|
||||
rows, err := s.queries.ListSubCoursesByCourse(ctx, courseID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
subCourses := make([]domain.SubCourse, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
subCourses = append(subCourses, domain.SubCourse{
|
||||
ID: row.ID,
|
||||
CourseID: row.CourseID,
|
||||
Title: row.Title,
|
||||
Description: ptrString(row.Description),
|
||||
Thumbnail: ptrString(row.Thumbnail),
|
||||
DisplayOrder: row.DisplayOrder,
|
||||
Level: row.Level,
|
||||
SubLevel: row.SubLevel,
|
||||
IsActive: row.IsActive,
|
||||
})
|
||||
}
|
||||
|
||||
return subCourses, nil
|
||||
}
|
||||
|
||||
func (s *Store) ListActiveSubCourses(
|
||||
ctx context.Context,
|
||||
) ([]domain.SubCourse, error) {
|
||||
rows, err := s.queries.ListActiveSubCourses(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
subCourses := make([]domain.SubCourse, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
subCourses = append(subCourses, domain.SubCourse{
|
||||
ID: row.ID,
|
||||
CourseID: row.CourseID,
|
||||
Title: row.Title,
|
||||
Description: ptrString(row.Description),
|
||||
Thumbnail: ptrString(row.Thumbnail),
|
||||
DisplayOrder: row.DisplayOrder,
|
||||
Level: row.Level,
|
||||
SubLevel: row.SubLevel,
|
||||
IsActive: row.IsActive,
|
||||
})
|
||||
}
|
||||
|
||||
return subCourses, nil
|
||||
}
|
||||
|
||||
func (s *Store) UpdateSubCourse(
|
||||
ctx context.Context,
|
||||
id int64,
|
||||
title *string,
|
||||
description *string,
|
||||
thumbnail *string,
|
||||
displayOrder *int32,
|
||||
level *string,
|
||||
subLevel *string,
|
||||
isActive *bool,
|
||||
) error {
|
||||
var titleVal, descVal, thumbVal, levelVal, subLevelVal string
|
||||
var dispOrderVal int32
|
||||
var isActiveVal bool
|
||||
|
||||
if title != nil {
|
||||
titleVal = *title
|
||||
}
|
||||
if description != nil {
|
||||
descVal = *description
|
||||
}
|
||||
if thumbnail != nil {
|
||||
thumbVal = *thumbnail
|
||||
}
|
||||
if displayOrder != nil {
|
||||
dispOrderVal = *displayOrder
|
||||
}
|
||||
if level != nil {
|
||||
levelVal = *level
|
||||
}
|
||||
if subLevel != nil {
|
||||
subLevelVal = *subLevel
|
||||
}
|
||||
if isActive != nil {
|
||||
isActiveVal = *isActive
|
||||
}
|
||||
|
||||
return s.queries.UpdateSubCourse(ctx, dbgen.UpdateSubCourseParams{
|
||||
Title: titleVal,
|
||||
Description: pgtype.Text{String: descVal, Valid: description != nil},
|
||||
Thumbnail: pgtype.Text{String: thumbVal, Valid: thumbnail != nil},
|
||||
DisplayOrder: dispOrderVal,
|
||||
Level: levelVal,
|
||||
SubLevel: subLevelVal,
|
||||
IsActive: isActiveVal,
|
||||
ID: id,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Store) DeactivateSubCourse(
|
||||
ctx context.Context,
|
||||
id int64,
|
||||
) error {
|
||||
return s.queries.DeactivateSubCourse(ctx, id)
|
||||
}
|
||||
|
||||
func (s *Store) DeleteSubCourse(
|
||||
ctx context.Context,
|
||||
id int64,
|
||||
) (domain.SubCourse, error) {
|
||||
row, err := s.queries.DeleteSubCourse(ctx, id)
|
||||
if err != nil {
|
||||
return domain.SubCourse{}, err
|
||||
}
|
||||
|
||||
return domain.SubCourse{
|
||||
ID: row.ID,
|
||||
CourseID: row.CourseID,
|
||||
Title: row.Title,
|
||||
Description: ptrString(row.Description),
|
||||
Thumbnail: ptrString(row.Thumbnail),
|
||||
DisplayOrder: row.DisplayOrder,
|
||||
Level: row.Level,
|
||||
SubLevel: row.SubLevel,
|
||||
IsActive: row.IsActive,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Store) ReorderSubCourses(ctx context.Context, ids []int64, positions []int32) error {
|
||||
return s.queries.ReorderSubCourses(ctx, dbgen.ReorderSubCoursesParams{
|
||||
Ids: ids,
|
||||
Positions: positions,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
package course_management
|
||||
|
||||
import (
|
||||
"Yimaru-Backend/internal/domain"
|
||||
"context"
|
||||
)
|
||||
|
||||
func (s *Service) CreateCourseCategory(
|
||||
ctx context.Context,
|
||||
name string,
|
||||
) (domain.CourseCategory, error) {
|
||||
return s.courseStore.CreateCourseCategory(ctx, name)
|
||||
}
|
||||
|
||||
func (s *Service) GetCourseCategoryByID(
|
||||
ctx context.Context,
|
||||
id int64,
|
||||
) (domain.CourseCategory, error) {
|
||||
return s.courseStore.GetCourseCategoryByID(ctx, id)
|
||||
}
|
||||
|
||||
func (s *Service) GetAllCourseCategories(
|
||||
ctx context.Context,
|
||||
limit int32,
|
||||
offset int32,
|
||||
) ([]domain.CourseCategory, int64, error) {
|
||||
return s.courseStore.GetAllCourseCategories(ctx, limit, offset)
|
||||
}
|
||||
|
||||
func (s *Service) UpdateCourseCategory(
|
||||
ctx context.Context,
|
||||
id int64,
|
||||
name *string,
|
||||
isActive *bool,
|
||||
) error {
|
||||
return s.courseStore.UpdateCourseCategory(ctx, id, name, isActive)
|
||||
}
|
||||
|
||||
func (s *Service) DeleteCourseCategory(
|
||||
ctx context.Context,
|
||||
id int64,
|
||||
) error {
|
||||
return s.courseStore.DeleteCourseCategory(ctx, id)
|
||||
}
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
package course_management
|
||||
|
||||
import (
|
||||
"Yimaru-Backend/internal/domain"
|
||||
"context"
|
||||
)
|
||||
|
||||
func (s *Service) CreateCourse(
|
||||
ctx context.Context,
|
||||
categoryID int64,
|
||||
title string,
|
||||
description *string,
|
||||
thumbnail *string,
|
||||
introVideoURL *string,
|
||||
) (domain.Course, error) {
|
||||
return s.courseStore.CreateCourse(ctx, categoryID, title, description, thumbnail, introVideoURL)
|
||||
}
|
||||
|
||||
func (s *Service) GetCourseByID(
|
||||
ctx context.Context,
|
||||
id int64,
|
||||
) (domain.Course, error) {
|
||||
return s.courseStore.GetCourseByID(ctx, id)
|
||||
}
|
||||
|
||||
func (s *Service) GetCoursesByCategory(
|
||||
ctx context.Context,
|
||||
categoryID int64,
|
||||
limit int32,
|
||||
offset int32,
|
||||
) ([]domain.Course, int64, error) {
|
||||
return s.courseStore.GetCoursesByCategory(ctx, categoryID, limit, offset)
|
||||
}
|
||||
|
||||
func (s *Service) UpdateCourse(
|
||||
ctx context.Context,
|
||||
id int64,
|
||||
title *string,
|
||||
description *string,
|
||||
thumbnail *string,
|
||||
introVideoURL *string,
|
||||
isActive *bool,
|
||||
) error {
|
||||
return s.courseStore.UpdateCourse(ctx, id, title, description, thumbnail, introVideoURL, isActive)
|
||||
}
|
||||
|
||||
func (s *Service) DeleteCourse(
|
||||
ctx context.Context,
|
||||
id int64,
|
||||
) error {
|
||||
return s.courseStore.DeleteCourse(ctx, id)
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
package course_management
|
||||
|
||||
import (
|
||||
"Yimaru-Backend/internal/domain"
|
||||
"context"
|
||||
)
|
||||
|
||||
func (s *Service) GetFullLearningTree(ctx context.Context) ([]domain.TreeCourse, error) {
|
||||
return s.courseStore.GetFullLearningTree(ctx)
|
||||
}
|
||||
|
||||
func (s *Service) GetCourseLearningPath(ctx context.Context, courseID int64) (domain.LearningPath, error) {
|
||||
return s.courseStore.GetCourseLearningPath(ctx, courseID)
|
||||
}
|
||||
|
||||
func (s *Service) ReorderCourseCategories(ctx context.Context, ids []int64, positions []int32) error {
|
||||
return s.courseStore.ReorderCourseCategories(ctx, ids, positions)
|
||||
}
|
||||
|
||||
func (s *Service) ReorderCourses(ctx context.Context, ids []int64, positions []int32) error {
|
||||
return s.courseStore.ReorderCourses(ctx, ids, positions)
|
||||
}
|
||||
|
||||
func (s *Service) ReorderSubCourses(ctx context.Context, ids []int64, positions []int32) error {
|
||||
return s.courseStore.ReorderSubCourses(ctx, ids, positions)
|
||||
}
|
||||
|
||||
func (s *Service) ReorderSubCourseVideos(ctx context.Context, ids []int64, positions []int32) error {
|
||||
return s.courseStore.ReorderSubCourseVideos(ctx, ids, positions)
|
||||
}
|
||||
|
||||
func (s *Service) ReorderQuestionSets(ctx context.Context, ids []int64, positions []int32) error {
|
||||
return s.courseStore.ReorderQuestionSets(ctx, ids, positions)
|
||||
}
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
package course_management
|
||||
|
||||
import (
|
||||
"Yimaru-Backend/internal/domain"
|
||||
"context"
|
||||
)
|
||||
|
||||
// --- Prerequisites (admin) ---
|
||||
|
||||
func (s *Service) AddSubCoursePrerequisite(ctx context.Context, subCourseID, prerequisiteSubCourseID int64) error {
|
||||
if subCourseID == prerequisiteSubCourseID {
|
||||
return domain.ErrSelfPrerequisite
|
||||
}
|
||||
return s.progressionStore.AddSubCoursePrerequisite(ctx, subCourseID, prerequisiteSubCourseID)
|
||||
}
|
||||
|
||||
func (s *Service) RemoveSubCoursePrerequisite(ctx context.Context, subCourseID, prerequisiteSubCourseID int64) error {
|
||||
return s.progressionStore.RemoveSubCoursePrerequisite(ctx, subCourseID, prerequisiteSubCourseID)
|
||||
}
|
||||
|
||||
func (s *Service) GetSubCoursePrerequisites(ctx context.Context, subCourseID int64) ([]domain.SubCoursePrerequisite, error) {
|
||||
return s.progressionStore.GetSubCoursePrerequisites(ctx, subCourseID)
|
||||
}
|
||||
|
||||
func (s *Service) GetSubCourseDependents(ctx context.Context, prerequisiteSubCourseID int64) ([]domain.SubCourseDependent, error) {
|
||||
return s.progressionStore.GetSubCourseDependents(ctx, prerequisiteSubCourseID)
|
||||
}
|
||||
|
||||
// --- User progress ---
|
||||
|
||||
func (s *Service) CheckSubCourseAccess(ctx context.Context, userID, subCourseID int64) (bool, error) {
|
||||
unmet, err := s.progressionStore.CountUnmetPrerequisites(ctx, subCourseID, userID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return unmet == 0, nil
|
||||
}
|
||||
|
||||
func (s *Service) StartSubCourse(ctx context.Context, userID, subCourseID int64) (domain.UserSubCourseProgress, error) {
|
||||
accessible, err := s.CheckSubCourseAccess(ctx, userID, subCourseID)
|
||||
if err != nil {
|
||||
return domain.UserSubCourseProgress{}, err
|
||||
}
|
||||
if !accessible {
|
||||
return domain.UserSubCourseProgress{}, domain.ErrPrerequisiteNotMet
|
||||
}
|
||||
|
||||
return s.progressionStore.StartSubCourseProgress(ctx, userID, subCourseID)
|
||||
}
|
||||
|
||||
func (s *Service) UpdateSubCourseProgress(ctx context.Context, userID, subCourseID int64, percentage int16) error {
|
||||
return s.progressionStore.UpdateSubCourseProgress(ctx, userID, subCourseID, percentage)
|
||||
}
|
||||
|
||||
func (s *Service) CompleteSubCourse(ctx context.Context, userID, subCourseID int64) error {
|
||||
return s.progressionStore.CompleteSubCourse(ctx, userID, subCourseID)
|
||||
}
|
||||
|
||||
func (s *Service) RecalculateSubCourseProgress(ctx context.Context, userID, subCourseID int64) error {
|
||||
return s.progressionStore.RecalculateSubCourseProgress(ctx, userID, subCourseID)
|
||||
}
|
||||
|
||||
func (s *Service) GetUserSubCourseProgress(ctx context.Context, userID, subCourseID int64) (domain.UserSubCourseProgress, error) {
|
||||
return s.progressionStore.GetUserSubCourseProgress(ctx, userID, subCourseID)
|
||||
}
|
||||
|
||||
func (s *Service) GetUserCourseProgress(ctx context.Context, userID, courseID int64) ([]domain.UserCourseProgressItem, error) {
|
||||
return s.progressionStore.GetUserCourseProgress(ctx, userID, courseID)
|
||||
}
|
||||
|
||||
func (s *Service) GetSubCoursesWithProgress(ctx context.Context, userID, courseID int64) ([]domain.SubCourseWithProgress, error) {
|
||||
return s.progressionStore.GetSubCoursesWithProgressByCourse(ctx, userID, courseID)
|
||||
}
|
||||
|
|
@ -10,8 +10,8 @@ import (
|
|||
|
||||
type Service struct {
|
||||
userStore ports.UserStore
|
||||
courseStore ports.CourseStore
|
||||
progressionStore ports.ProgressionStore
|
||||
courseStore interface{}
|
||||
progressionStore interface{}
|
||||
notificationSvc *notificationservice.Service
|
||||
vimeoSvc *vimeoservice.Service
|
||||
cloudConvertSvc *cloudconvertservice.Service
|
||||
|
|
@ -20,8 +20,8 @@ type Service struct {
|
|||
|
||||
func NewService(
|
||||
userStore ports.UserStore,
|
||||
courseStore ports.CourseStore,
|
||||
progressionStore ports.ProgressionStore,
|
||||
courseStore interface{},
|
||||
progressionStore interface{},
|
||||
notificationSvc *notificationservice.Service,
|
||||
cfg *config.Config,
|
||||
) *Service {
|
||||
|
|
|
|||
|
|
@ -1,299 +0,0 @@
|
|||
package course_management
|
||||
|
||||
import (
|
||||
"Yimaru-Backend/internal/domain"
|
||||
"Yimaru-Backend/internal/pkgs/vimeo"
|
||||
vimeoservice "Yimaru-Backend/internal/services/vimeo"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"path"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (s *Service) CreateSubCourseVideo(
|
||||
ctx context.Context,
|
||||
subCourseID int64,
|
||||
title string,
|
||||
description *string,
|
||||
videoURL string,
|
||||
duration int32,
|
||||
resolution *string,
|
||||
instructorID *string,
|
||||
thumbnail *string,
|
||||
visibility *string,
|
||||
displayOrder *int32,
|
||||
status *string,
|
||||
) (domain.SubCourseVideo, error) {
|
||||
// Default to DIRECT provider when no Vimeo info provided
|
||||
provider := string(domain.VideoHostProviderDirect)
|
||||
return s.courseStore.CreateSubCourseVideo(
|
||||
ctx, subCourseID, title, description, videoURL, duration,
|
||||
resolution, instructorID, thumbnail, visibility, displayOrder, status,
|
||||
nil, nil, nil, nil, &provider,
|
||||
)
|
||||
}
|
||||
|
||||
// CreateSubCourseVideoWithVimeo creates a video and uploads it to Vimeo
|
||||
func (s *Service) CreateSubCourseVideoWithVimeo(
|
||||
ctx context.Context,
|
||||
subCourseID int64,
|
||||
title string,
|
||||
description *string,
|
||||
sourceURL string,
|
||||
fileSize int64,
|
||||
duration int32,
|
||||
resolution *string,
|
||||
instructorID *string,
|
||||
thumbnail *string,
|
||||
visibility *string,
|
||||
displayOrder *int32,
|
||||
) (domain.SubCourseVideo, error) {
|
||||
if s.vimeoSvc == nil {
|
||||
return domain.SubCourseVideo{}, fmt.Errorf("vimeo service is not configured")
|
||||
}
|
||||
|
||||
descStr := ""
|
||||
if description != nil {
|
||||
descStr = *description
|
||||
}
|
||||
|
||||
var uploadResult *vimeoservice.UploadResult
|
||||
var err error
|
||||
|
||||
if s.cloudConvertSvc != nil {
|
||||
httpClient := &http.Client{Timeout: 30 * time.Minute}
|
||||
req, reqErr := http.NewRequestWithContext(ctx, http.MethodGet, sourceURL, nil)
|
||||
if reqErr != nil {
|
||||
return domain.SubCourseVideo{}, fmt.Errorf("failed to create download request: %w", reqErr)
|
||||
}
|
||||
resp, dlErr := httpClient.Do(req)
|
||||
if dlErr != nil {
|
||||
return domain.SubCourseVideo{}, fmt.Errorf("failed to download source video: %w", dlErr)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return domain.SubCourseVideo{}, fmt.Errorf("failed to download source video: status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
dlSize := resp.ContentLength
|
||||
if dlSize <= 0 {
|
||||
dlSize = fileSize
|
||||
}
|
||||
|
||||
filename := path.Base(sourceURL)
|
||||
if filename == "" || filename == "." || filename == "/" {
|
||||
filename = "video.mp4"
|
||||
}
|
||||
|
||||
result, compErr := s.cloudConvertSvc.CompressVideo(ctx, filename, resp.Body, dlSize)
|
||||
if compErr != nil {
|
||||
return domain.SubCourseVideo{}, fmt.Errorf("failed to compress video: %w", compErr)
|
||||
}
|
||||
defer result.Data.Close()
|
||||
|
||||
uploadResult, err = s.vimeoSvc.UploadVideoFile(ctx, title, descStr, result.Data, result.FileSize)
|
||||
} else {
|
||||
uploadResult, err = s.vimeoSvc.CreatePullUpload(ctx, title, descStr, sourceURL, fileSize)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return domain.SubCourseVideo{}, fmt.Errorf("failed to upload to Vimeo: %w", err)
|
||||
}
|
||||
|
||||
embedURL := vimeo.GenerateEmbedURL(uploadResult.VimeoID, &vimeo.EmbedOptions{
|
||||
Title: true,
|
||||
Byline: true,
|
||||
Portrait: true,
|
||||
})
|
||||
|
||||
embedHTML := vimeo.GenerateIframeEmbed(uploadResult.VimeoID, 640, 360, nil)
|
||||
|
||||
provider := string(domain.VideoHostProviderVimeo)
|
||||
vimeoStatus := "uploading"
|
||||
status := "DRAFT"
|
||||
|
||||
return s.courseStore.CreateSubCourseVideo(
|
||||
ctx, subCourseID, title, description,
|
||||
uploadResult.Link,
|
||||
duration, resolution, instructorID, thumbnail, visibility, displayOrder, &status,
|
||||
&uploadResult.VimeoID, &embedURL, &embedHTML, &vimeoStatus, &provider,
|
||||
)
|
||||
}
|
||||
|
||||
func (s *Service) CreateSubCourseVideoWithFileUpload(
|
||||
ctx context.Context,
|
||||
subCourseID int64,
|
||||
title string,
|
||||
description *string,
|
||||
filename string,
|
||||
fileData io.Reader,
|
||||
fileSize int64,
|
||||
duration int32,
|
||||
resolution *string,
|
||||
instructorID *string,
|
||||
thumbnail *string,
|
||||
visibility *string,
|
||||
displayOrder *int32,
|
||||
) (domain.SubCourseVideo, error) {
|
||||
if s.vimeoSvc == nil {
|
||||
return domain.SubCourseVideo{}, fmt.Errorf("vimeo service is not configured")
|
||||
}
|
||||
|
||||
descStr := ""
|
||||
if description != nil {
|
||||
descStr = *description
|
||||
}
|
||||
|
||||
videoReader := fileData
|
||||
videoSize := fileSize
|
||||
|
||||
if s.cloudConvertSvc != nil {
|
||||
result, err := s.cloudConvertSvc.CompressVideo(ctx, filename, fileData, fileSize)
|
||||
if err != nil {
|
||||
return domain.SubCourseVideo{}, fmt.Errorf("failed to compress video: %w", err)
|
||||
}
|
||||
defer result.Data.Close()
|
||||
videoReader = result.Data
|
||||
videoSize = result.FileSize
|
||||
}
|
||||
|
||||
uploadResult, err := s.vimeoSvc.UploadVideoFile(ctx, title, descStr, videoReader, videoSize)
|
||||
if err != nil {
|
||||
return domain.SubCourseVideo{}, fmt.Errorf("failed to upload video file to Vimeo: %w", err)
|
||||
}
|
||||
|
||||
embedURL := vimeo.GenerateEmbedURL(uploadResult.VimeoID, &vimeo.EmbedOptions{
|
||||
Title: true,
|
||||
Byline: true,
|
||||
Portrait: true,
|
||||
})
|
||||
|
||||
embedHTML := vimeo.GenerateIframeEmbed(uploadResult.VimeoID, 640, 360, nil)
|
||||
|
||||
provider := string(domain.VideoHostProviderVimeo)
|
||||
vimeoStatus := "uploading"
|
||||
status := "DRAFT"
|
||||
|
||||
return s.courseStore.CreateSubCourseVideo(
|
||||
ctx, subCourseID, title, description,
|
||||
uploadResult.Link,
|
||||
duration, resolution, instructorID, thumbnail, visibility, displayOrder, &status,
|
||||
&uploadResult.VimeoID, &embedURL, &embedHTML, &vimeoStatus, &provider,
|
||||
)
|
||||
}
|
||||
|
||||
// CreateSubCourseVideoFromVimeoID creates a video record from an existing Vimeo video
|
||||
func (s *Service) CreateSubCourseVideoFromVimeoID(
|
||||
ctx context.Context,
|
||||
subCourseID int64,
|
||||
vimeoVideoID string,
|
||||
title string,
|
||||
description *string,
|
||||
displayOrder *int32,
|
||||
instructorID *string,
|
||||
) (domain.SubCourseVideo, error) {
|
||||
if s.vimeoSvc == nil {
|
||||
return domain.SubCourseVideo{}, fmt.Errorf("vimeo service is not configured")
|
||||
}
|
||||
|
||||
// Fetch video info from Vimeo
|
||||
info, err := s.vimeoSvc.GetVideoInfo(ctx, vimeoVideoID)
|
||||
if err != nil {
|
||||
return domain.SubCourseVideo{}, fmt.Errorf("failed to get Vimeo video info: %w", err)
|
||||
}
|
||||
|
||||
// Use Vimeo data
|
||||
embedHTML := vimeo.GenerateIframeEmbed(vimeoVideoID, 640, 360, nil)
|
||||
provider := string(domain.VideoHostProviderVimeo)
|
||||
vimeoStatus := info.TranscodeStatus
|
||||
if vimeoStatus == "" {
|
||||
vimeoStatus = "available"
|
||||
}
|
||||
status := "DRAFT"
|
||||
duration := int32(info.Duration)
|
||||
resolution := fmt.Sprintf("%dx%d", info.Width, info.Height)
|
||||
thumbnail := info.ThumbnailURL
|
||||
|
||||
return s.courseStore.CreateSubCourseVideo(
|
||||
ctx, subCourseID, title, description,
|
||||
info.Link, duration, &resolution, instructorID, &thumbnail, nil, displayOrder, &status,
|
||||
&vimeoVideoID, &info.EmbedURL, &embedHTML, &vimeoStatus, &provider,
|
||||
)
|
||||
}
|
||||
|
||||
func (s *Service) GetSubCourseVideoByID(
|
||||
ctx context.Context,
|
||||
id int64,
|
||||
) (domain.SubCourseVideo, error) {
|
||||
return s.courseStore.GetSubCourseVideoByID(ctx, id)
|
||||
}
|
||||
|
||||
func (s *Service) GetVideosBySubCourse(
|
||||
ctx context.Context,
|
||||
subCourseID int64,
|
||||
) ([]domain.SubCourseVideo, int64, error) {
|
||||
return s.courseStore.GetVideosBySubCourse(ctx, subCourseID)
|
||||
}
|
||||
|
||||
func (s *Service) GetPublishedVideosBySubCourse(
|
||||
ctx context.Context,
|
||||
subCourseID int64,
|
||||
) ([]domain.SubCourseVideo, error) {
|
||||
return s.courseStore.GetPublishedVideosBySubCourse(ctx, subCourseID)
|
||||
}
|
||||
|
||||
func (s *Service) GetFirstIncompletePreviousVideo(
|
||||
ctx context.Context,
|
||||
userID int64,
|
||||
videoID int64,
|
||||
) (*domain.VideoAccessBlock, error) {
|
||||
return s.courseStore.GetFirstIncompletePreviousVideo(ctx, userID, videoID)
|
||||
}
|
||||
|
||||
func (s *Service) MarkVideoCompleted(
|
||||
ctx context.Context,
|
||||
userID int64,
|
||||
videoID int64,
|
||||
) error {
|
||||
return s.courseStore.MarkVideoCompleted(ctx, userID, videoID)
|
||||
}
|
||||
|
||||
func (s *Service) PublishSubCourseVideo(
|
||||
ctx context.Context,
|
||||
videoID int64,
|
||||
) error {
|
||||
return s.courseStore.PublishSubCourseVideo(ctx, videoID)
|
||||
}
|
||||
|
||||
func (s *Service) UpdateSubCourseVideo(
|
||||
ctx context.Context,
|
||||
id int64,
|
||||
title *string,
|
||||
description *string,
|
||||
videoURL *string,
|
||||
duration *int32,
|
||||
resolution *string,
|
||||
visibility *string,
|
||||
thumbnail *string,
|
||||
displayOrder *int32,
|
||||
status *string,
|
||||
) error {
|
||||
return s.courseStore.UpdateSubCourseVideo(ctx, id, title, description, videoURL, duration, resolution, visibility, thumbnail, displayOrder, status)
|
||||
}
|
||||
|
||||
func (s *Service) ArchiveSubCourseVideo(
|
||||
ctx context.Context,
|
||||
id int64,
|
||||
) error {
|
||||
return s.courseStore.ArchiveSubCourseVideo(ctx, id)
|
||||
}
|
||||
|
||||
func (s *Service) DeleteSubCourseVideo(
|
||||
ctx context.Context,
|
||||
id int64,
|
||||
) error {
|
||||
return s.courseStore.DeleteSubCourseVideo(ctx, id)
|
||||
}
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
package course_management
|
||||
|
||||
import (
|
||||
"Yimaru-Backend/internal/domain"
|
||||
"context"
|
||||
)
|
||||
|
||||
func (s *Service) CreateSubCourse(
|
||||
ctx context.Context,
|
||||
courseID int64,
|
||||
title string,
|
||||
description *string,
|
||||
thumbnail *string,
|
||||
displayOrder *int32,
|
||||
level string,
|
||||
subLevel string,
|
||||
) (domain.SubCourse, error) {
|
||||
return s.courseStore.CreateSubCourse(ctx, courseID, title, description, thumbnail, displayOrder, level, subLevel)
|
||||
}
|
||||
|
||||
func (s *Service) GetSubCourseByID(
|
||||
ctx context.Context,
|
||||
id int64,
|
||||
) (domain.SubCourse, error) {
|
||||
return s.courseStore.GetSubCourseByID(ctx, id)
|
||||
}
|
||||
|
||||
func (s *Service) GetSubCoursesByCourse(
|
||||
ctx context.Context,
|
||||
courseID int64,
|
||||
) ([]domain.SubCourse, int64, error) {
|
||||
return s.courseStore.GetSubCoursesByCourse(ctx, courseID)
|
||||
}
|
||||
|
||||
func (s *Service) ListSubCoursesByCourse(
|
||||
ctx context.Context,
|
||||
courseID int64,
|
||||
) ([]domain.SubCourse, error) {
|
||||
return s.courseStore.ListSubCoursesByCourse(ctx, courseID)
|
||||
}
|
||||
|
||||
func (s *Service) ListActiveSubCourses(
|
||||
ctx context.Context,
|
||||
) ([]domain.SubCourse, error) {
|
||||
return s.courseStore.ListActiveSubCourses(ctx)
|
||||
}
|
||||
|
||||
func (s *Service) UpdateSubCourse(
|
||||
ctx context.Context,
|
||||
id int64,
|
||||
title *string,
|
||||
description *string,
|
||||
thumbnail *string,
|
||||
displayOrder *int32,
|
||||
level *string,
|
||||
subLevel *string,
|
||||
isActive *bool,
|
||||
) error {
|
||||
return s.courseStore.UpdateSubCourse(ctx, id, title, description, thumbnail, displayOrder, level, subLevel, isActive)
|
||||
}
|
||||
|
||||
func (s *Service) DeactivateSubCourse(
|
||||
ctx context.Context,
|
||||
id int64,
|
||||
) error {
|
||||
return s.courseStore.DeactivateSubCourse(ctx, id)
|
||||
}
|
||||
|
||||
func (s *Service) DeleteSubCourse(
|
||||
ctx context.Context,
|
||||
id int64,
|
||||
) (domain.SubCourse, error) {
|
||||
return s.courseStore.DeleteSubCourse(ctx, id)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -313,7 +313,7 @@ func normalizeAndValidateMediaContentType(mediaType, contentType, fileName strin
|
|||
// @Summary Upload an audio file
|
||||
// @Tags files
|
||||
// @Accept multipart/form-data
|
||||
// @Param file formance file true "Audio file (mp3, wav, ogg, m4a, aac, webm)"
|
||||
// @Param file formData file true "Audio file (mp3, wav, ogg, m4a, aac, webm)"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Router /api/v1/files/audio [post]
|
||||
func (h *Handler) UploadAudio(c *fiber.Ctx) error {
|
||||
|
|
|
|||
382
internal/web_server/handlers/hierarchy_handler.go
Normal file
382
internal/web_server/handlers/hierarchy_handler.go
Normal file
|
|
@ -0,0 +1,382 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
dbgen "Yimaru-Backend/gen/db"
|
||||
"Yimaru-Backend/internal/domain"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
type createCourseSubCategoryReq struct {
|
||||
CategoryID int64 `json:"category_id"`
|
||||
Name string `json:"name"`
|
||||
Description *string `json:"description"`
|
||||
DisplayOrder *int32 `json:"display_order"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
type createLevelReq struct {
|
||||
CourseID int64 `json:"course_id"`
|
||||
CEFRLevel string `json:"cefr_level"`
|
||||
DisplayOrder *int32 `json:"display_order"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
type createModuleReq struct {
|
||||
LevelID int64 `json:"level_id"`
|
||||
Title string `json:"title"`
|
||||
Description *string `json:"description"`
|
||||
DisplayOrder *int32 `json:"display_order"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
type createSubModuleReq struct {
|
||||
ModuleID int64 `json:"module_id"`
|
||||
Title string `json:"title"`
|
||||
Description *string `json:"description"`
|
||||
DisplayOrder *int32 `json:"display_order"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
type createSubModuleVideoReq struct {
|
||||
SubModuleID int64 `json:"sub_module_id"`
|
||||
Title string `json:"title"`
|
||||
Description *string `json:"description"`
|
||||
VideoURL string `json:"video_url"`
|
||||
Duration *int32 `json:"duration"`
|
||||
Resolution *string `json:"resolution"`
|
||||
Visibility *string `json:"visibility"`
|
||||
InstructorID *string `json:"instructor_id"`
|
||||
Thumbnail *string `json:"thumbnail"`
|
||||
DisplayOrder *int32 `json:"display_order"`
|
||||
Status *string `json:"status"`
|
||||
}
|
||||
|
||||
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 createSubModulePracticeReq struct {
|
||||
SubModuleID int64 `json:"sub_module_id"`
|
||||
Title string `json:"title"`
|
||||
Description *string `json:"description"`
|
||||
Thumbnail *string `json:"thumbnail"`
|
||||
IntroVideoURL *string `json:"intro_video_url"`
|
||||
QuestionSetID int64 `json:"question_set_id"`
|
||||
DisplayOrder *int32 `json:"display_order"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
func toText(v *string) pgtype.Text {
|
||||
if v == nil {
|
||||
return pgtype.Text{Valid: false}
|
||||
}
|
||||
return pgtype.Text{String: *v, Valid: true}
|
||||
}
|
||||
|
||||
func toInt4(v *int32) pgtype.Int4 {
|
||||
if v == nil {
|
||||
return pgtype.Int4{Valid: false}
|
||||
}
|
||||
return pgtype.Int4{Int32: *v, Valid: true}
|
||||
}
|
||||
|
||||
func boolOrNil(v *bool) interface{} {
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
return *v
|
||||
}
|
||||
|
||||
func intOrNil(v *int32) interface{} {
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
return *v
|
||||
}
|
||||
|
||||
// UnifiedHierarchy godoc
|
||||
// @Summary Get unified course hierarchy
|
||||
// @Description Returns full hierarchy: category -> sub-category -> course
|
||||
// @Tags course-management
|
||||
// @Produce json
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/course-management/hierarchy [get]
|
||||
func (h *Handler) UnifiedHierarchy(c *fiber.Ctx) error {
|
||||
rows, err := h.analyticsDB.GetCoursesWithHierarchy(c.Context())
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load hierarchy", Error: err.Error()})
|
||||
}
|
||||
return c.JSON(domain.Response{Message: "Unified hierarchy retrieved successfully", Data: rows})
|
||||
}
|
||||
|
||||
// UnifiedHierarchyByCourse godoc
|
||||
// @Summary Get hierarchy for a course
|
||||
// @Description Returns hierarchy nodes for one course including levels/modules/sub-modules
|
||||
// @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}/hierarchy [get]
|
||||
func (h *Handler) UnifiedHierarchyByCourse(c *fiber.Ctx) error {
|
||||
courseID, err := strconv.ParseInt(c.Params("courseId"), 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid course ID", Error: err.Error()})
|
||||
}
|
||||
rows, err := h.analyticsDB.GetFullHierarchyByCourseID(c.Context(), courseID)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load course hierarchy", Error: err.Error()})
|
||||
}
|
||||
return c.JSON(domain.Response{Message: "Course hierarchy retrieved successfully", Data: rows})
|
||||
}
|
||||
|
||||
// CreateCourseSubCategory godoc
|
||||
// @Summary Create course sub-category
|
||||
// @Description Creates a sub-category under a course category
|
||||
// @Tags course-management
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body createCourseSubCategoryReq true "Create sub-category payload"
|
||||
// @Success 201 {object} domain.Response
|
||||
// @Failure 400 {object} domain.ErrorResponse
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/course-management/sub-categories [post]
|
||||
func (h *Handler) CreateCourseSubCategory(c *fiber.Ctx) error {
|
||||
var req createCourseSubCategoryReq
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()})
|
||||
}
|
||||
if req.CategoryID <= 0 || strings.TrimSpace(req.Name) == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "category_id and name are required"})
|
||||
}
|
||||
created, err := h.analyticsDB.CreateCourseSubCategory(c.Context(), dbgen.CreateCourseSubCategoryParams{
|
||||
CategoryID: req.CategoryID,
|
||||
Name: req.Name,
|
||||
Description: toText(req.Description),
|
||||
Column4: intOrNil(req.DisplayOrder),
|
||||
Column5: boolOrNil(req.IsActive),
|
||||
})
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to create sub-category", Error: err.Error()})
|
||||
}
|
||||
return c.Status(fiber.StatusCreated).JSON(domain.Response{Message: "Course sub-category created", Data: created})
|
||||
}
|
||||
|
||||
// CreateLevel godoc
|
||||
// @Summary Create level
|
||||
// @Description Creates a CEFR level under a course
|
||||
// @Tags course-management
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body createLevelReq true "Create level payload"
|
||||
// @Success 201 {object} domain.Response
|
||||
// @Failure 400 {object} domain.ErrorResponse
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/course-management/levels [post]
|
||||
func (h *Handler) CreateLevel(c *fiber.Ctx) error {
|
||||
var req createLevelReq
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()})
|
||||
}
|
||||
req.CEFRLevel = strings.ToUpper(strings.TrimSpace(req.CEFRLevel))
|
||||
validCEFR := map[string]bool{"A1": true, "A2": true, "A3": true, "B1": true, "B2": true, "B3": true, "C1": true, "C2": true, "C3": true}
|
||||
if req.CourseID <= 0 || !validCEFR[req.CEFRLevel] {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "course_id and valid cefr_level are required"})
|
||||
}
|
||||
created, err := h.analyticsDB.CreateLevel(c.Context(), dbgen.CreateLevelParams{
|
||||
CourseID: req.CourseID,
|
||||
CefrLevel: req.CEFRLevel,
|
||||
Column3: intOrNil(req.DisplayOrder),
|
||||
Column4: boolOrNil(req.IsActive),
|
||||
})
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to create level", Error: err.Error()})
|
||||
}
|
||||
return c.Status(fiber.StatusCreated).JSON(domain.Response{Message: "Level created", Data: created})
|
||||
}
|
||||
|
||||
// CreateModule godoc
|
||||
// @Summary Create module
|
||||
// @Description Creates a module under a level
|
||||
// @Tags course-management
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body createModuleReq true "Create module payload"
|
||||
// @Success 201 {object} domain.Response
|
||||
// @Failure 400 {object} domain.ErrorResponse
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/course-management/modules [post]
|
||||
func (h *Handler) CreateModule(c *fiber.Ctx) error {
|
||||
var req createModuleReq
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()})
|
||||
}
|
||||
if req.LevelID <= 0 || strings.TrimSpace(req.Title) == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "level_id and title are required"})
|
||||
}
|
||||
created, err := h.analyticsDB.CreateModule(c.Context(), dbgen.CreateModuleParams{
|
||||
LevelID: req.LevelID,
|
||||
Title: req.Title,
|
||||
Description: toText(req.Description),
|
||||
Column4: intOrNil(req.DisplayOrder),
|
||||
Column5: boolOrNil(req.IsActive),
|
||||
})
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to create module", Error: err.Error()})
|
||||
}
|
||||
return c.Status(fiber.StatusCreated).JSON(domain.Response{Message: "Module created", Data: created})
|
||||
}
|
||||
|
||||
// CreateSubModule godoc
|
||||
// @Summary Create sub-module
|
||||
// @Description Creates a sub-module under a module
|
||||
// @Tags course-management
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body createSubModuleReq true "Create sub-module payload"
|
||||
// @Success 201 {object} domain.Response
|
||||
// @Failure 400 {object} domain.ErrorResponse
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/course-management/sub-modules [post]
|
||||
func (h *Handler) CreateSubModule(c *fiber.Ctx) error {
|
||||
var req createSubModuleReq
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()})
|
||||
}
|
||||
if req.ModuleID <= 0 || strings.TrimSpace(req.Title) == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "module_id and title are required"})
|
||||
}
|
||||
created, err := h.analyticsDB.CreateSubModule(c.Context(), dbgen.CreateSubModuleParams{
|
||||
ModuleID: req.ModuleID,
|
||||
Title: req.Title,
|
||||
Description: toText(req.Description),
|
||||
Column4: intOrNil(req.DisplayOrder),
|
||||
Column5: boolOrNil(req.IsActive),
|
||||
})
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to create sub-module", Error: err.Error()})
|
||||
}
|
||||
return c.Status(fiber.StatusCreated).JSON(domain.Response{Message: "Sub-module created", Data: created})
|
||||
}
|
||||
|
||||
// CreateSubModuleVideo godoc
|
||||
// @Summary Create sub-module video
|
||||
// @Description Creates a video under a sub-module
|
||||
// @Tags course-management
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body createSubModuleVideoReq true "Create sub-module video payload"
|
||||
// @Success 201 {object} domain.Response
|
||||
// @Failure 400 {object} domain.ErrorResponse
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/course-management/sub-module-videos [post]
|
||||
func (h *Handler) CreateSubModuleVideo(c *fiber.Ctx) error {
|
||||
var req createSubModuleVideoReq
|
||||
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 || strings.TrimSpace(req.Title) == "" || strings.TrimSpace(req.VideoURL) == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "sub_module_id, title and video_url are required"})
|
||||
}
|
||||
created, err := h.analyticsDB.CreateSubModuleVideo(c.Context(), dbgen.CreateSubModuleVideoParams{
|
||||
SubModuleID: req.SubModuleID,
|
||||
Title: req.Title,
|
||||
Description: toText(req.Description),
|
||||
VideoUrl: req.VideoURL,
|
||||
Duration: toInt4(req.Duration),
|
||||
Resolution: toText(req.Resolution),
|
||||
Column7: nil,
|
||||
Visibility: toText(req.Visibility),
|
||||
InstructorID: toText(req.InstructorID),
|
||||
Thumbnail: toText(req.Thumbnail),
|
||||
Column12: intOrNil(req.DisplayOrder),
|
||||
Column13: req.Status,
|
||||
VimeoID: pgtype.Text{Valid: false},
|
||||
VimeoEmbedUrl: pgtype.Text{Valid: false},
|
||||
VimeoPlayerHtml: pgtype.Text{Valid: false},
|
||||
VimeoStatus: pgtype.Text{Valid: false},
|
||||
Column18: nil,
|
||||
})
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to create sub-module video", Error: err.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
|
||||
// @Tags course-management
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body attachSubModuleLessonReq true "Attach 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
|
||||
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"})
|
||||
}
|
||||
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),
|
||||
})
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to attach lesson", Error: err.Error()})
|
||||
}
|
||||
return c.Status(fiber.StatusCreated).JSON(domain.Response{Message: "Lesson attached to sub-module", Data: attached})
|
||||
}
|
||||
|
||||
// CreateSubModulePractice godoc
|
||||
// @Summary Create practice under sub-module
|
||||
// @Description Creates a sub-module practice with metadata and linked question set
|
||||
// @Tags course-management
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body createSubModulePracticeReq true "Create practice payload"
|
||||
// @Success 201 {object} domain.Response
|
||||
// @Failure 400 {object} domain.ErrorResponse
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/course-management/sub-module-practices [post]
|
||||
func (h *Handler) CreateSubModulePractice(c *fiber.Ctx) error {
|
||||
var req createSubModulePracticeReq
|
||||
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 || strings.TrimSpace(req.Title) == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "sub_module_id, title and question_set_id are required"})
|
||||
}
|
||||
created, err := h.analyticsDB.CreateSubModulePractice(c.Context(), dbgen.CreateSubModulePracticeParams{
|
||||
SubModuleID: req.SubModuleID,
|
||||
Title: req.Title,
|
||||
Description: toText(req.Description),
|
||||
Thumbnail: toText(req.Thumbnail),
|
||||
IntroVideoUrl: toText(req.IntroVideoURL),
|
||||
QuestionSetID: req.QuestionSetID,
|
||||
Column7: intOrNil(req.DisplayOrder),
|
||||
Column8: boolOrNil(req.IsActive),
|
||||
})
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to create practice", Error: err.Error()})
|
||||
}
|
||||
return c.Status(fiber.StatusCreated).JSON(domain.Response{Message: "Practice created", Data: created})
|
||||
}
|
||||
|
||||
|
|
@ -1,541 +0,0 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"Yimaru-Backend/internal/domain"
|
||||
"errors"
|
||||
"math"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
// --- Request / Response types ---
|
||||
|
||||
type addPrerequisiteReq struct {
|
||||
PrerequisiteSubCourseID int64 `json:"prerequisite_sub_course_id" validate:"required"`
|
||||
}
|
||||
|
||||
type prerequisiteRes struct {
|
||||
ID int64 `json:"id"`
|
||||
SubCourseID int64 `json:"sub_course_id"`
|
||||
PrerequisiteSubCourseID int64 `json:"prerequisite_sub_course_id"`
|
||||
PrerequisiteTitle string `json:"prerequisite_title"`
|
||||
PrerequisiteLevel string `json:"prerequisite_level"`
|
||||
PrerequisiteDisplayOrder int32 `json:"prerequisite_display_order"`
|
||||
}
|
||||
|
||||
type dependentRes struct {
|
||||
ID int64 `json:"id"`
|
||||
SubCourseID int64 `json:"sub_course_id"`
|
||||
PrerequisiteSubCourseID int64 `json:"prerequisite_sub_course_id"`
|
||||
DependentTitle string `json:"dependent_title"`
|
||||
DependentLevel string `json:"dependent_level"`
|
||||
}
|
||||
|
||||
type updateProgressReq struct {
|
||||
ProgressPercentage int16 `json:"progress_percentage" validate:"required,min=0,max=100"`
|
||||
}
|
||||
|
||||
type subCourseProgressRes struct {
|
||||
SubCourseID int64 `json:"sub_course_id"`
|
||||
Title string `json:"title"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Thumbnail *string `json:"thumbnail,omitempty"`
|
||||
DisplayOrder int32 `json:"display_order"`
|
||||
Level string `json:"level"`
|
||||
ProgressStatus string `json:"progress_status"`
|
||||
ProgressPercentage int16 `json:"progress_percentage"`
|
||||
StartedAt *time.Time `json:"started_at,omitempty"`
|
||||
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
||||
IsLocked bool `json:"is_locked"`
|
||||
}
|
||||
|
||||
type userProgressRes struct {
|
||||
SubCourseID int64 `json:"sub_course_id"`
|
||||
SubCourseTitle string `json:"sub_course_title"`
|
||||
SubCourseLevel string `json:"sub_course_level"`
|
||||
Status string `json:"status"`
|
||||
ProgressPercentage int16 `json:"progress_percentage"`
|
||||
StartedAt *time.Time `json:"started_at,omitempty"`
|
||||
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
||||
}
|
||||
|
||||
type courseProgressSummaryRes struct {
|
||||
CourseID int64 `json:"course_id"`
|
||||
LearnerUserID int64 `json:"learner_user_id"`
|
||||
OverallProgressPercentage int16 `json:"overall_progress_percentage"`
|
||||
TotalSubCourses int32 `json:"total_sub_courses"`
|
||||
CompletedSubCourses int32 `json:"completed_sub_courses"`
|
||||
InProgressSubCourses int32 `json:"in_progress_sub_courses"`
|
||||
NotStartedSubCourses int32 `json:"not_started_sub_courses"`
|
||||
LockedSubCourses int32 `json:"locked_sub_courses"`
|
||||
}
|
||||
|
||||
func mapSubCourseProgress(items []domain.SubCourseWithProgress) []subCourseProgressRes {
|
||||
res := make([]subCourseProgressRes, 0, len(items))
|
||||
for _, item := range items {
|
||||
res = append(res, subCourseProgressRes{
|
||||
SubCourseID: item.SubCourseID,
|
||||
Title: item.Title,
|
||||
Description: item.Description,
|
||||
Thumbnail: item.Thumbnail,
|
||||
DisplayOrder: item.DisplayOrder,
|
||||
Level: item.Level,
|
||||
ProgressStatus: string(item.ProgressStatus),
|
||||
ProgressPercentage: item.ProgressPercentage,
|
||||
StartedAt: item.StartedAt,
|
||||
CompletedAt: item.CompletedAt,
|
||||
IsLocked: item.IsLocked,
|
||||
})
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// --- Prerequisite Handlers (admin) ---
|
||||
|
||||
// AddSubCoursePrerequisite godoc
|
||||
// @Summary Add prerequisite to sub-course
|
||||
// @Description Link a prerequisite sub-course that must be completed before accessing this sub-course
|
||||
// @Tags progression
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "Sub-course ID"
|
||||
// @Param body body addPrerequisiteReq true "Prerequisite sub-course ID"
|
||||
// @Success 201 {object} domain.Response
|
||||
// @Failure 400 {object} domain.ErrorResponse
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/course-management/sub-courses/{id}/prerequisites [post]
|
||||
func (h *Handler) AddSubCoursePrerequisite(c *fiber.Ctx) error {
|
||||
subCourseID, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid sub-course ID",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
var req addPrerequisiteReq
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid request body",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
if err := h.courseMgmtSvc.AddSubCoursePrerequisite(c.Context(), subCourseID, req.PrerequisiteSubCourseID); err != nil {
|
||||
if errors.Is(err, domain.ErrSelfPrerequisite) {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid prerequisite",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to add prerequisite",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusCreated).JSON(domain.Response{
|
||||
Message: "Prerequisite added successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// GetSubCoursePrerequisites godoc
|
||||
// @Summary Get sub-course prerequisites
|
||||
// @Description Returns all prerequisites for a sub-course
|
||||
// @Tags progression
|
||||
// @Produce json
|
||||
// @Param id path int true "Sub-course ID"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Failure 400 {object} domain.ErrorResponse
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/course-management/sub-courses/{id}/prerequisites [get]
|
||||
func (h *Handler) GetSubCoursePrerequisites(c *fiber.Ctx) error {
|
||||
subCourseID, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid sub-course ID",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
prerequisites, err := h.courseMgmtSvc.GetSubCoursePrerequisites(c.Context(), subCourseID)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to get prerequisites",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
var res []prerequisiteRes
|
||||
for _, p := range prerequisites {
|
||||
res = append(res, prerequisiteRes{
|
||||
ID: p.ID,
|
||||
SubCourseID: p.SubCourseID,
|
||||
PrerequisiteSubCourseID: p.PrerequisiteSubCourseID,
|
||||
PrerequisiteTitle: p.PrerequisiteTitle,
|
||||
PrerequisiteLevel: p.PrerequisiteLevel,
|
||||
PrerequisiteDisplayOrder: p.PrerequisiteDisplayOrder,
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Prerequisites retrieved successfully",
|
||||
Data: res,
|
||||
})
|
||||
}
|
||||
|
||||
// RemoveSubCoursePrerequisite godoc
|
||||
// @Summary Remove prerequisite from sub-course
|
||||
// @Description Unlink a prerequisite from a sub-course
|
||||
// @Tags progression
|
||||
// @Produce json
|
||||
// @Param id path int true "Sub-course ID"
|
||||
// @Param prerequisiteId path int true "Prerequisite sub-course ID"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Failure 400 {object} domain.ErrorResponse
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/course-management/sub-courses/{id}/prerequisites/{prerequisiteId} [delete]
|
||||
func (h *Handler) RemoveSubCoursePrerequisite(c *fiber.Ctx) error {
|
||||
subCourseID, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid sub-course ID",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
prerequisiteID, err := strconv.ParseInt(c.Params("prerequisiteId"), 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid prerequisite ID",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
if err := h.courseMgmtSvc.RemoveSubCoursePrerequisite(c.Context(), subCourseID, prerequisiteID); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to remove prerequisite",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Prerequisite removed successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// --- User Progress Handlers ---
|
||||
|
||||
// StartSubCourse godoc
|
||||
// @Summary Start a sub-course
|
||||
// @Description Mark a sub-course as started for the authenticated user (checks prerequisites)
|
||||
// @Tags progression
|
||||
// @Produce json
|
||||
// @Param id path int true "Sub-course ID"
|
||||
// @Success 201 {object} domain.Response
|
||||
// @Failure 400 {object} domain.ErrorResponse
|
||||
// @Failure 403 {object} domain.ErrorResponse
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/progress/sub-courses/{id}/start [post]
|
||||
func (h *Handler) StartSubCourse(c *fiber.Ctx) error {
|
||||
userID := c.Locals("user_id").(int64)
|
||||
subCourseID, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid sub-course ID",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
progress, err := h.courseMgmtSvc.StartSubCourse(c.Context(), userID, subCourseID)
|
||||
if err != nil {
|
||||
if errors.Is(err, domain.ErrPrerequisiteNotMet) {
|
||||
return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{
|
||||
Message: "Cannot start sub-course",
|
||||
Error: "Prerequisites not completed",
|
||||
})
|
||||
}
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to start sub-course",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusCreated).JSON(domain.Response{
|
||||
Message: "Sub-course started",
|
||||
Data: userProgressRes{
|
||||
SubCourseID: progress.SubCourseID,
|
||||
Status: string(progress.Status),
|
||||
ProgressPercentage: progress.ProgressPercentage,
|
||||
StartedAt: progress.StartedAt,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateSubCourseProgress godoc
|
||||
// @Summary Update sub-course progress
|
||||
// @Description Update the progress percentage for a sub-course
|
||||
// @Tags progression
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "Sub-course ID"
|
||||
// @Param body body updateProgressReq true "Progress update"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Failure 400 {object} domain.ErrorResponse
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/progress/sub-courses/{id} [put]
|
||||
func (h *Handler) UpdateSubCourseProgress(c *fiber.Ctx) error {
|
||||
userID := c.Locals("user_id").(int64)
|
||||
subCourseID, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid sub-course ID",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
var req updateProgressReq
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid request body",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
if err := h.courseMgmtSvc.UpdateSubCourseProgress(c.Context(), userID, subCourseID, req.ProgressPercentage); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to update progress",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Progress updated successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// CompleteSubCourse godoc
|
||||
// @Summary Complete a sub-course
|
||||
// @Description Mark a sub-course as completed for the authenticated user
|
||||
// @Tags progression
|
||||
// @Produce json
|
||||
// @Param id path int true "Sub-course ID"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Failure 400 {object} domain.ErrorResponse
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/progress/sub-courses/{id}/complete [post]
|
||||
func (h *Handler) CompleteSubCourse(c *fiber.Ctx) error {
|
||||
userID := c.Locals("user_id").(int64)
|
||||
subCourseID, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid sub-course ID",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
if err := h.courseMgmtSvc.CompleteSubCourse(c.Context(), userID, subCourseID); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to complete sub-course",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Sub-course completed",
|
||||
})
|
||||
}
|
||||
|
||||
// CheckSubCourseAccess godoc
|
||||
// @Summary Check sub-course access
|
||||
// @Description Check if the authenticated user has completed all prerequisites for a sub-course
|
||||
// @Tags progression
|
||||
// @Produce json
|
||||
// @Param id path int true "Sub-course ID"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Failure 400 {object} domain.ErrorResponse
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/progress/sub-courses/{id}/access [get]
|
||||
func (h *Handler) CheckSubCourseAccess(c *fiber.Ctx) error {
|
||||
userID := c.Locals("user_id").(int64)
|
||||
subCourseID, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid sub-course ID",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
accessible, err := h.courseMgmtSvc.CheckSubCourseAccess(c.Context(), userID, subCourseID)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to check access",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Access check completed",
|
||||
Data: fiber.Map{
|
||||
"accessible": accessible,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// GetUserCourseProgress godoc
|
||||
// @Summary Get user's course progress
|
||||
// @Description Returns the authenticated user's progress for all sub-courses in a course, including lock status
|
||||
// @Tags progression
|
||||
// @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/progress/courses/{courseId} [get]
|
||||
func (h *Handler) GetUserCourseProgress(c *fiber.Ctx) error {
|
||||
userID := c.Locals("user_id").(int64)
|
||||
courseID, err := strconv.ParseInt(c.Params("courseId"), 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid course ID",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
items, err := h.courseMgmtSvc.GetSubCoursesWithProgress(c.Context(), userID, courseID)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to get course progress",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Course progress retrieved successfully",
|
||||
Data: mapSubCourseProgress(items),
|
||||
})
|
||||
}
|
||||
|
||||
// GetUserCourseProgressForAdmin godoc
|
||||
// @Summary Get learner's course progress (admin)
|
||||
// @Description Returns a target learner's progress for all sub-courses in a course, including lock status
|
||||
// @Tags progression
|
||||
// @Produce json
|
||||
// @Param userId path int true "Learner User ID"
|
||||
// @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/admin/users/{userId}/progress/courses/{courseId} [get]
|
||||
func (h *Handler) GetUserCourseProgressForAdmin(c *fiber.Ctx) error {
|
||||
targetUserID, err := strconv.ParseInt(c.Params("userId"), 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid user ID",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
courseID, err := strconv.ParseInt(c.Params("courseId"), 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid course ID",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
items, err := h.courseMgmtSvc.GetSubCoursesWithProgress(c.Context(), targetUserID, courseID)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to get learner course progress",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Learner course progress retrieved successfully",
|
||||
Data: mapSubCourseProgress(items),
|
||||
})
|
||||
}
|
||||
|
||||
// GetUserCourseProgressSummaryForAdmin godoc
|
||||
// @Summary Get learner's course progress summary (admin)
|
||||
// @Description Returns course-level aggregated progress metrics for a target learner
|
||||
// @Tags progression
|
||||
// @Produce json
|
||||
// @Param userId path int true "Learner User ID"
|
||||
// @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/admin/users/{userId}/progress/courses/{courseId}/summary [get]
|
||||
func (h *Handler) GetUserCourseProgressSummaryForAdmin(c *fiber.Ctx) error {
|
||||
targetUserID, err := strconv.ParseInt(c.Params("userId"), 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid user ID",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
courseID, err := strconv.ParseInt(c.Params("courseId"), 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid course ID",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
items, err := h.courseMgmtSvc.GetSubCoursesWithProgress(c.Context(), targetUserID, courseID)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to get learner course progress summary",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
var (
|
||||
completedCount int32
|
||||
inProgressCount int32
|
||||
notStartedCount int32
|
||||
lockedCount int32
|
||||
sumPercentage int64
|
||||
)
|
||||
|
||||
for _, item := range items {
|
||||
sumPercentage += int64(item.ProgressPercentage)
|
||||
switch item.ProgressStatus {
|
||||
case domain.ProgressStatusCompleted:
|
||||
completedCount++
|
||||
case domain.ProgressStatusInProgress:
|
||||
inProgressCount++
|
||||
default:
|
||||
notStartedCount++
|
||||
}
|
||||
if item.IsLocked {
|
||||
lockedCount++
|
||||
}
|
||||
}
|
||||
|
||||
totalSubCourses := int32(len(items))
|
||||
overall := int16(0)
|
||||
if totalSubCourses > 0 {
|
||||
overall = int16(math.Round(float64(sumPercentage) / float64(totalSubCourses)))
|
||||
}
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Learner course progress summary retrieved successfully",
|
||||
Data: courseProgressSummaryRes{
|
||||
CourseID: courseID,
|
||||
LearnerUserID: targetUserID,
|
||||
OverallProgressPercentage: overall,
|
||||
TotalSubCourses: totalSubCourses,
|
||||
CompletedSubCourses: completedCount,
|
||||
InProgressSubCourses: inProgressCount,
|
||||
NotStartedSubCourses: notStartedCount,
|
||||
LockedSubCourses: lockedCount,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
@ -1350,19 +1350,6 @@ func (h *Handler) CompletePractice(c *fiber.Ctx) error {
|
|||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
if set.OwnerID == nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to update sub-course progress",
|
||||
Error: "practice owner is missing",
|
||||
})
|
||||
}
|
||||
if err := h.courseMgmtSvc.RecalculateSubCourseProgress(c.Context(), userID, *set.OwnerID); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to update sub-course progress",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Practice completed",
|
||||
})
|
||||
|
|
|
|||
85
internal/web_server/handlers/thumbnail_helper.go
Normal file
85
internal/web_server/handlers/thumbnail_helper.go
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"Yimaru-Backend/internal/domain"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/google/uuid"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func (h *Handler) processAndSaveThumbnail(c *fiber.Ctx, subDir string) (string, error) {
|
||||
fileHeader, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
return "", c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Image file is required", Error: err.Error()})
|
||||
}
|
||||
if fileHeader.Size > 10*1024*1024 {
|
||||
return "", c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "File too large", Error: "Thumbnail image must be <= 10MB"})
|
||||
}
|
||||
|
||||
fh, err := fileHeader.Open()
|
||||
if err != nil {
|
||||
return "", c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to read file", Error: err.Error()})
|
||||
}
|
||||
defer fh.Close()
|
||||
|
||||
head := make([]byte, 512)
|
||||
n, _ := fh.Read(head)
|
||||
contentType := http.DetectContentType(head[:n])
|
||||
if contentType != "image/jpeg" && contentType != "image/png" && contentType != "image/webp" {
|
||||
return "", c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid file type", Error: "Only jpg, png and webp images are allowed"})
|
||||
}
|
||||
|
||||
rest, err := io.ReadAll(fh)
|
||||
if err != nil {
|
||||
return "", c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to read file", Error: err.Error()})
|
||||
}
|
||||
data := append(head[:n], rest...)
|
||||
|
||||
if h.cloudConvertSvc != nil {
|
||||
optimized, optErr := h.cloudConvertSvc.OptimizeImage(c.Context(), fileHeader.Filename, bytes.NewReader(data), int64(len(data)), 1200, 80)
|
||||
if optErr != nil {
|
||||
h.mongoLoggerSvc.Warn("CloudConvert thumbnail optimization failed, using original", zap.Error(optErr))
|
||||
} else {
|
||||
optimizedData, readErr := io.ReadAll(optimized.Data)
|
||||
optimized.Data.Close()
|
||||
if readErr == nil {
|
||||
data = optimizedData
|
||||
contentType = "image/webp"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if h.minioSvc != nil {
|
||||
result, uploadErr := h.minioSvc.Upload(c.Context(), subDir, fileHeader.Filename, bytes.NewReader(data), int64(len(data)), contentType)
|
||||
if uploadErr != nil {
|
||||
return "", c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to upload file to storage", Error: uploadErr.Error()})
|
||||
}
|
||||
return "minio://" + result.ObjectKey, nil
|
||||
}
|
||||
|
||||
ext := ".jpg"
|
||||
if contentType == "image/png" {
|
||||
ext = ".png"
|
||||
}
|
||||
if contentType == "image/webp" {
|
||||
ext = ".webp"
|
||||
}
|
||||
dir := filepath.Join(".", "static", subDir)
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return "", c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to create storage directory", Error: err.Error()})
|
||||
}
|
||||
filename := uuid.New().String() + ext
|
||||
fullpath := filepath.Join(dir, filename)
|
||||
if err := os.WriteFile(fullpath, data, 0o644); err != nil {
|
||||
return "", c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to save file", Error: err.Error()})
|
||||
}
|
||||
return "/static/" + subDir + "/" + filename, nil
|
||||
}
|
||||
|
||||
|
|
@ -78,60 +78,16 @@ func (a *App) initAppRoutes() {
|
|||
groupV1.Get("/assessment/questions", h.ListAssessmentQuestions)
|
||||
groupV1.Get("/assessment/questions/:id", h.GetAssessmentQuestionByID)
|
||||
|
||||
// Reorder (drag-and-drop support)
|
||||
// Keep static reorder routes before dynamic `/:id` routes to avoid route collisions
|
||||
// (e.g., `/courses/reorder` being parsed as `/courses/:id`).
|
||||
groupV1.Put("/course-management/categories/reorder", a.authMiddleware, a.RequirePermission("course_categories.reorder"), h.ReorderCourseCategories)
|
||||
groupV1.Put("/course-management/courses/reorder", a.authMiddleware, a.RequirePermission("courses.reorder"), h.ReorderCourses)
|
||||
groupV1.Put("/course-management/sub-courses/reorder", a.authMiddleware, a.RequirePermission("subcourses.reorder"), h.ReorderSubCourses)
|
||||
groupV1.Put("/course-management/videos/reorder", a.authMiddleware, a.RequirePermission("videos.reorder"), h.ReorderSubCourseVideos)
|
||||
groupV1.Put("/course-management/practices/reorder", a.authMiddleware, a.RequirePermission("practices.reorder"), h.ReorderPractices)
|
||||
|
||||
// Course Categories
|
||||
groupV1.Post("/course-management/categories", a.authMiddleware, a.RequirePermission("course_categories.create"), h.CreateCourseCategory)
|
||||
groupV1.Get("/course-management/categories", a.authMiddleware, a.RequirePermission("course_categories.list"), h.GetAllCourseCategories)
|
||||
groupV1.Get("/course-management/categories/:id", a.authMiddleware, a.RequirePermission("course_categories.get"), h.GetCourseCategoryByID)
|
||||
groupV1.Put("/course-management/categories/:id", a.authMiddleware, a.RequirePermission("course_categories.update"), h.UpdateCourseCategory)
|
||||
groupV1.Delete("/course-management/categories/:id", a.authMiddleware, a.RequirePermission("course_categories.delete"), h.DeleteCourseCategory)
|
||||
|
||||
// Courses
|
||||
groupV1.Post("/course-management/courses", a.authMiddleware, a.RequirePermission("courses.create"), h.CreateCourse)
|
||||
groupV1.Get("/course-management/courses/:id", a.authMiddleware, a.RequirePermission("courses.get"), h.GetCourseByID)
|
||||
groupV1.Get("/course-management/categories/:categoryId/courses", a.authMiddleware, a.RequirePermission("courses.list_by_category"), h.GetCoursesByCategory)
|
||||
groupV1.Put("/course-management/courses/:id", a.authMiddleware, a.RequirePermission("courses.update"), h.UpdateCourse)
|
||||
groupV1.Post("/course-management/courses/:id/thumbnail", a.authMiddleware, a.RequirePermission("courses.upload_thumbnail"), h.UploadCourseThumbnail)
|
||||
groupV1.Delete("/course-management/courses/:id", a.authMiddleware, a.RequirePermission("courses.delete"), h.DeleteCourse)
|
||||
|
||||
// Sub-courses
|
||||
groupV1.Post("/course-management/sub-courses", a.authMiddleware, a.RequirePermission("subcourses.create"), h.CreateSubCourse)
|
||||
groupV1.Get("/course-management/sub-courses/:id", a.authMiddleware, a.RequirePermission("subcourses.get"), h.GetSubCourseByID)
|
||||
groupV1.Get("/course-management/courses/:courseId/sub-courses", a.authMiddleware, a.RequirePermission("subcourses.list_by_course"), h.GetSubCoursesByCourse)
|
||||
groupV1.Get("/course-management/courses/:courseId/sub-courses/list", a.authMiddleware, a.RequirePermission("subcourses.list_by_course_list"), h.ListSubCoursesByCourse)
|
||||
groupV1.Get("/course-management/sub-courses/active", a.authMiddleware, a.RequirePermission("subcourses.list_active"), h.ListActiveSubCourses)
|
||||
groupV1.Patch("/course-management/sub-courses/:id", a.authMiddleware, a.RequirePermission("subcourses.update"), h.UpdateSubCourse)
|
||||
groupV1.Post("/course-management/sub-courses/:id/thumbnail", a.authMiddleware, a.RequirePermission("subcourses.upload_thumbnail"), h.UploadSubCourseThumbnail)
|
||||
groupV1.Put("/course-management/sub-courses/:id/deactivate", a.authMiddleware, a.RequirePermission("subcourses.deactivate"), h.DeactivateSubCourse)
|
||||
groupV1.Delete("/course-management/sub-courses/:id", a.authMiddleware, a.RequirePermission("subcourses.delete"), h.DeleteSubCourse)
|
||||
|
||||
// Sub-course Videos
|
||||
groupV1.Post("/course-management/videos", a.authMiddleware, a.RequirePermission("videos.create"), h.CreateSubCourseVideo)
|
||||
groupV1.Post("/course-management/videos/vimeo", a.authMiddleware, a.RequirePermission("videos.create_vimeo"), h.CreateSubCourseVideoWithVimeo)
|
||||
groupV1.Post("/course-management/videos/upload", a.authMiddleware, a.RequirePermission("videos.upload"), h.UploadSubCourseVideo)
|
||||
groupV1.Post("/course-management/videos/vimeo/import", a.authMiddleware, a.RequirePermission("videos.import_vimeo"), h.CreateSubCourseVideoFromVimeoID)
|
||||
groupV1.Get("/course-management/videos/:id", a.authMiddleware, a.RequirePermission("videos.get"), h.GetSubCourseVideoByID)
|
||||
groupV1.Get("/course-management/sub-courses/:subCourseId/videos", a.authMiddleware, a.RequirePermission("videos.list_by_subcourse"), h.GetVideosBySubCourse)
|
||||
groupV1.Get("/course-management/sub-courses/:subCourseId/videos/published", a.authMiddleware, a.RequirePermission("videos.list_published"), h.GetPublishedVideosBySubCourse)
|
||||
groupV1.Put("/course-management/videos/:id/publish", a.authMiddleware, a.RequirePermission("videos.publish"), h.PublishSubCourseVideo)
|
||||
groupV1.Put("/course-management/videos/:id", a.authMiddleware, a.RequirePermission("videos.update"), h.UpdateSubCourseVideo)
|
||||
groupV1.Delete("/course-management/videos/:id", a.authMiddleware, a.RequirePermission("videos.delete"), h.DeleteSubCourseVideo)
|
||||
|
||||
// Learning Tree
|
||||
groupV1.Get("/course-management/learning-tree", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.GetFullLearningTree)
|
||||
groupV1.Get("/course-management/courses/:courseId/learning-path", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.GetCourseLearningPath)
|
||||
groupV1.Get("/course-management/human-language/hierarchy", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.GetHumanLanguageHierarchy)
|
||||
groupV1.Get("/course-management/human-language/courses/:courseId/lessons", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.GetHumanLanguageLessonsByCourse)
|
||||
groupV1.Post("/course-management/human-language/lessons", a.authMiddleware, a.RequirePermission("subcourses.create"), h.CreateHumanLanguageLesson)
|
||||
groupV1.Patch("/course-management/human-language/lessons/:id", a.authMiddleware, a.RequirePermission("subcourses.update"), h.UpdateHumanLanguageLesson)
|
||||
// Unified Course Management (single hierarchy model)
|
||||
groupV1.Get("/course-management/hierarchy", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.UnifiedHierarchy)
|
||||
groupV1.Get("/course-management/courses/:courseId/hierarchy", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.UnifiedHierarchyByCourse)
|
||||
groupV1.Post("/course-management/sub-categories", a.authMiddleware, a.RequirePermission("course_categories.create"), h.CreateCourseSubCategory)
|
||||
groupV1.Post("/course-management/levels", a.authMiddleware, a.RequirePermission("subcourses.create"), h.CreateLevel)
|
||||
groupV1.Post("/course-management/modules", a.authMiddleware, a.RequirePermission("subcourses.create"), h.CreateModule)
|
||||
groupV1.Post("/course-management/sub-modules", a.authMiddleware, a.RequirePermission("subcourses.create"), h.CreateSubModule)
|
||||
groupV1.Post("/course-management/sub-module-videos", a.authMiddleware, a.RequirePermission("videos.create"), h.CreateSubModuleVideo)
|
||||
groupV1.Post("/course-management/sub-module-lessons", a.authMiddleware, a.RequirePermission("question_sets.update"), h.AttachSubModuleLesson)
|
||||
groupV1.Post("/course-management/sub-module-practices", a.authMiddleware, a.RequirePermission("question_sets.update"), h.CreateSubModulePractice)
|
||||
|
||||
// Questions
|
||||
groupV1.Post("/questions", a.authMiddleware, a.RequirePermission("questions.create"), h.CreateQuestion)
|
||||
|
|
@ -145,7 +101,6 @@ func (a *App) initAppRoutes() {
|
|||
groupV1.Post("/question-sets", a.authMiddleware, a.RequirePermission("question_sets.create"), h.CreateQuestionSet)
|
||||
groupV1.Get("/question-sets", a.authMiddleware, a.RequirePermission("question_sets.list"), h.GetQuestionSetsByType)
|
||||
groupV1.Get("/question-sets/by-owner", a.authMiddleware, a.RequirePermission("question_sets.list_by_owner"), h.GetQuestionSetsByOwner)
|
||||
groupV1.Get("/question-sets/sub-courses/:subCourseId/entry-assessment", a.authMiddleware, a.RequirePermission("question_sets.list_by_owner"), h.GetSubCourseEntryAssessmentSet)
|
||||
groupV1.Get("/question-sets/:id", a.authMiddleware, a.RequirePermission("question_sets.get"), h.GetQuestionSetByID)
|
||||
groupV1.Put("/question-sets/:id", a.authMiddleware, a.RequirePermission("question_sets.update"), h.UpdateQuestionSet)
|
||||
groupV1.Delete("/question-sets/:id", a.authMiddleware, a.RequirePermission("question_sets.delete"), h.DeleteQuestionSet)
|
||||
|
|
@ -336,21 +291,7 @@ func (a *App) initAppRoutes() {
|
|||
teamGroup.Delete("/members/:id", a.authMiddleware, a.RequirePermission("team.members.delete"), h.DeleteTeamMember)
|
||||
teamGroup.Post("/members/:id/change-password", a.authMiddleware, a.RequirePermission("team.members.change_password"), h.ChangeTeamMemberPassword)
|
||||
|
||||
// Sub-course Prerequisites
|
||||
groupV1.Post("/course-management/sub-courses/:id/prerequisites", a.authMiddleware, a.RequirePermission("subcourse_prerequisites.add"), h.AddSubCoursePrerequisite)
|
||||
groupV1.Get("/course-management/sub-courses/:id/prerequisites", a.authMiddleware, a.RequirePermission("subcourse_prerequisites.list"), h.GetSubCoursePrerequisites)
|
||||
groupV1.Delete("/course-management/sub-courses/:id/prerequisites/:prerequisiteId", a.authMiddleware, a.RequirePermission("subcourse_prerequisites.remove"), h.RemoveSubCoursePrerequisite)
|
||||
|
||||
// User Progression
|
||||
groupV1.Post("/progress/sub-courses/:id/start", a.authMiddleware, a.RequirePermission("progress.start"), h.StartSubCourse)
|
||||
groupV1.Put("/progress/sub-courses/:id", a.authMiddleware, a.RequirePermission("progress.update"), h.UpdateSubCourseProgress)
|
||||
groupV1.Post("/progress/sub-courses/:id/complete", a.authMiddleware, a.RequirePermission("progress.complete"), h.CompleteSubCourse)
|
||||
groupV1.Post("/progress/videos/:id/complete", a.authMiddleware, a.RequirePermission("progress.update"), h.CompleteSubCourseVideo)
|
||||
groupV1.Post("/progress/practices/:id/complete", a.authMiddleware, a.RequirePermission("progress.update"), h.CompletePractice)
|
||||
groupV1.Get("/progress/sub-courses/:id/access", a.authMiddleware, a.RequirePermission("progress.check_access"), h.CheckSubCourseAccess)
|
||||
groupV1.Get("/progress/courses/:courseId", a.authMiddleware, a.RequirePermission("progress.get_course"), h.GetUserCourseProgress)
|
||||
groupV1.Get("/admin/users/:userId/progress/courses/:courseId", a.authMiddleware, a.RequirePermission("progress.get_any_user"), h.GetUserCourseProgressForAdmin)
|
||||
groupV1.Get("/admin/users/:userId/progress/courses/:courseId/summary", a.authMiddleware, a.RequirePermission("progress.get_any_user"), h.GetUserCourseProgressSummaryForAdmin)
|
||||
// Legacy sub-course prerequisite/progression routes removed after hierarchy cutover.
|
||||
|
||||
// Ratings
|
||||
groupV1.Post("/ratings", a.authMiddleware, a.RequirePermission("ratings.submit"), h.SubmitRating)
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user