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
|
is_active
|
||||||
)
|
)
|
||||||
VALUES ($1, $2, $3, $4, $5, COALESCE($6, true))
|
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 {
|
type CreateCourseParams struct {
|
||||||
|
|
@ -52,6 +52,7 @@ func (q *Queries) CreateCourse(ctx context.Context, arg CreateCourseParams) (Cou
|
||||||
&i.Thumbnail,
|
&i.Thumbnail,
|
||||||
&i.IntroVideoUrl,
|
&i.IntroVideoUrl,
|
||||||
&i.DisplayOrder,
|
&i.DisplayOrder,
|
||||||
|
&i.SubCategoryID,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
@ -67,7 +68,7 @@ func (q *Queries) DeleteCourse(ctx context.Context, id int64) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
const GetCourseByID = `-- name: GetCourseByID :one
|
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
|
FROM courses
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
`
|
`
|
||||||
|
|
@ -84,6 +85,7 @@ func (q *Queries) GetCourseByID(ctx context.Context, id int64) (Course, error) {
|
||||||
&i.Thumbnail,
|
&i.Thumbnail,
|
||||||
&i.IntroVideoUrl,
|
&i.IntroVideoUrl,
|
||||||
&i.DisplayOrder,
|
&i.DisplayOrder,
|
||||||
|
&i.SubCategoryID,
|
||||||
)
|
)
|
||||||
return i, err
|
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"`
|
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||||
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
||||||
DisplayOrder int32 `json:"display_order"`
|
DisplayOrder int32 `json:"display_order"`
|
||||||
|
SubCategoryID pgtype.Int8 `json:"sub_category_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CourseCategory struct {
|
type CourseCategory struct {
|
||||||
|
|
@ -41,6 +42,16 @@ type CourseCategory struct {
|
||||||
DisplayOrder int32 `json:"display_order"`
|
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 {
|
type Device struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
UserID int64 `json:"user_id"`
|
UserID int64 `json:"user_id"`
|
||||||
|
|
@ -58,11 +69,30 @@ type GlobalSetting struct {
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
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 {
|
type LevelToSubCourse struct {
|
||||||
LevelID int64 `json:"level_id"`
|
LevelID int64 `json:"level_id"`
|
||||||
SubCourseID int64 `json:"sub_course_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 {
|
type ModuleToSubCourse struct {
|
||||||
ModuleID int64 `json:"module_id"`
|
ModuleID int64 `json:"module_id"`
|
||||||
SubCourseID int64 `json:"sub_course_id"`
|
SubCourseID int64 `json:"sub_course_id"`
|
||||||
|
|
@ -314,6 +344,63 @@ type SubCourseVideo struct {
|
||||||
VideoHostProvider pgtype.Text `json:"video_host_provider"`
|
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 {
|
type SubscriptionPlan struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
|
@ -404,7 +491,7 @@ type UserAudioResponse struct {
|
||||||
type UserPracticeProgress struct {
|
type UserPracticeProgress struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
UserID int64 `json:"user_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"`
|
QuestionSetID int64 `json:"question_set_id"`
|
||||||
CompletedAt pgtype.Timestamp `json:"completed_at"`
|
CompletedAt pgtype.Timestamp `json:"completed_at"`
|
||||||
CreatedAt pgtype.Timestamp `json:"created_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 (
|
import (
|
||||||
dbgen "Yimaru-Backend/gen/db"
|
dbgen "Yimaru-Backend/gen/db"
|
||||||
"Yimaru-Backend/internal/domain"
|
"Yimaru-Backend/internal/domain"
|
||||||
"Yimaru-Backend/internal/ports"
|
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"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(
|
func (s *Store) CreateCourseCategory(
|
||||||
ctx context.Context,
|
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 {
|
type Service struct {
|
||||||
userStore ports.UserStore
|
userStore ports.UserStore
|
||||||
courseStore ports.CourseStore
|
courseStore interface{}
|
||||||
progressionStore ports.ProgressionStore
|
progressionStore interface{}
|
||||||
notificationSvc *notificationservice.Service
|
notificationSvc *notificationservice.Service
|
||||||
vimeoSvc *vimeoservice.Service
|
vimeoSvc *vimeoservice.Service
|
||||||
cloudConvertSvc *cloudconvertservice.Service
|
cloudConvertSvc *cloudconvertservice.Service
|
||||||
|
|
@ -20,8 +20,8 @@ type Service struct {
|
||||||
|
|
||||||
func NewService(
|
func NewService(
|
||||||
userStore ports.UserStore,
|
userStore ports.UserStore,
|
||||||
courseStore ports.CourseStore,
|
courseStore interface{},
|
||||||
progressionStore ports.ProgressionStore,
|
progressionStore interface{},
|
||||||
notificationSvc *notificationservice.Service,
|
notificationSvc *notificationservice.Service,
|
||||||
cfg *config.Config,
|
cfg *config.Config,
|
||||||
) *Service {
|
) *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
|
// @Summary Upload an audio file
|
||||||
// @Tags files
|
// @Tags files
|
||||||
// @Accept multipart/form-data
|
// @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
|
// @Success 200 {object} domain.Response
|
||||||
// @Router /api/v1/files/audio [post]
|
// @Router /api/v1/files/audio [post]
|
||||||
func (h *Handler) UploadAudio(c *fiber.Ctx) error {
|
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(),
|
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{
|
return c.JSON(domain.Response{
|
||||||
Message: "Practice completed",
|
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", h.ListAssessmentQuestions)
|
||||||
groupV1.Get("/assessment/questions/:id", h.GetAssessmentQuestionByID)
|
groupV1.Get("/assessment/questions/:id", h.GetAssessmentQuestionByID)
|
||||||
|
|
||||||
// Reorder (drag-and-drop support)
|
// Unified Course Management (single hierarchy model)
|
||||||
// Keep static reorder routes before dynamic `/:id` routes to avoid route collisions
|
groupV1.Get("/course-management/hierarchy", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.UnifiedHierarchy)
|
||||||
// (e.g., `/courses/reorder` being parsed as `/courses/:id`).
|
groupV1.Get("/course-management/courses/:courseId/hierarchy", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.UnifiedHierarchyByCourse)
|
||||||
groupV1.Put("/course-management/categories/reorder", a.authMiddleware, a.RequirePermission("course_categories.reorder"), h.ReorderCourseCategories)
|
groupV1.Post("/course-management/sub-categories", a.authMiddleware, a.RequirePermission("course_categories.create"), h.CreateCourseSubCategory)
|
||||||
groupV1.Put("/course-management/courses/reorder", a.authMiddleware, a.RequirePermission("courses.reorder"), h.ReorderCourses)
|
groupV1.Post("/course-management/levels", a.authMiddleware, a.RequirePermission("subcourses.create"), h.CreateLevel)
|
||||||
groupV1.Put("/course-management/sub-courses/reorder", a.authMiddleware, a.RequirePermission("subcourses.reorder"), h.ReorderSubCourses)
|
groupV1.Post("/course-management/modules", a.authMiddleware, a.RequirePermission("subcourses.create"), h.CreateModule)
|
||||||
groupV1.Put("/course-management/videos/reorder", a.authMiddleware, a.RequirePermission("videos.reorder"), h.ReorderSubCourseVideos)
|
groupV1.Post("/course-management/sub-modules", a.authMiddleware, a.RequirePermission("subcourses.create"), h.CreateSubModule)
|
||||||
groupV1.Put("/course-management/practices/reorder", a.authMiddleware, a.RequirePermission("practices.reorder"), h.ReorderPractices)
|
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)
|
||||||
// Course Categories
|
groupV1.Post("/course-management/sub-module-practices", a.authMiddleware, a.RequirePermission("question_sets.update"), h.CreateSubModulePractice)
|
||||||
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)
|
|
||||||
|
|
||||||
// Questions
|
// Questions
|
||||||
groupV1.Post("/questions", a.authMiddleware, a.RequirePermission("questions.create"), h.CreateQuestion)
|
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.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", 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/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.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.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)
|
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.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)
|
teamGroup.Post("/members/:id/change-password", a.authMiddleware, a.RequirePermission("team.members.change_password"), h.ChangeTeamMemberPassword)
|
||||||
|
|
||||||
// Sub-course Prerequisites
|
// Legacy sub-course prerequisite/progression routes removed after hierarchy cutover.
|
||||||
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)
|
|
||||||
|
|
||||||
// Ratings
|
// Ratings
|
||||||
groupV1.Post("/ratings", a.authMiddleware, a.RequirePermission("ratings.submit"), h.SubmitRating)
|
groupV1.Post("/ratings", a.authMiddleware, a.RequirePermission("ratings.submit"), h.SubmitRating)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user