feat: add hierarchy publish status and resolve question type definition IDs

Extend DRAFT/PUBLISHED to programs, courses, modules, and exam-prep hierarchy entities with learner visibility gating and progress exclusion. Resolve question_type_definition_id in question responses for legacy system types and unlinked dynamic questions.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Yared Yemane 2026-06-10 02:40:16 -07:00
parent 5c6cb1b8d3
commit e56bea3abf
62 changed files with 1894 additions and 889 deletions

View File

@ -0,0 +1,27 @@
ALTER TABLE programs DROP CONSTRAINT IF EXISTS chk_programs_publish_status;
ALTER TABLE programs DROP COLUMN IF EXISTS publish_status;
ALTER TABLE courses DROP CONSTRAINT IF EXISTS chk_courses_publish_status;
ALTER TABLE courses DROP COLUMN IF EXISTS publish_status;
ALTER TABLE modules DROP CONSTRAINT IF EXISTS chk_modules_publish_status;
ALTER TABLE modules DROP COLUMN IF EXISTS publish_status;
ALTER TABLE exam_prep.catalog_courses DROP CONSTRAINT IF EXISTS chk_exam_prep_catalog_courses_publish_status;
ALTER TABLE exam_prep.catalog_courses DROP COLUMN IF EXISTS publish_status;
ALTER TABLE exam_prep.units DROP CONSTRAINT IF EXISTS chk_exam_prep_units_publish_status;
ALTER TABLE exam_prep.units DROP COLUMN IF EXISTS publish_status;
ALTER TABLE exam_prep.unit_modules DROP CONSTRAINT IF EXISTS chk_exam_prep_unit_modules_publish_status;
ALTER TABLE exam_prep.unit_modules DROP COLUMN IF EXISTS publish_status;
ALTER TABLE exam_prep.unit_module_lessons DROP CONSTRAINT IF EXISTS chk_exam_prep_unit_module_lessons_publish_status;
ALTER TABLE exam_prep.unit_module_lessons DROP COLUMN IF EXISTS publish_status;

View File

@ -0,0 +1,54 @@
-- Draft vs published visibility for the remaining LMS and exam-prep hierarchy levels
-- (mirrors lessons.publish_status from 000062 and practice publish_status from 000060).
-- Existing rows stay PUBLISHED; new inserts default to DRAFT unless the API sends PUBLISHED.
-- LMS hierarchy
ALTER TABLE programs
ADD COLUMN publish_status VARCHAR(16) NOT NULL DEFAULT 'PUBLISHED'
CONSTRAINT chk_programs_publish_status CHECK (publish_status IN ('DRAFT', 'PUBLISHED'));
ALTER TABLE programs
ALTER COLUMN publish_status SET DEFAULT 'DRAFT';
ALTER TABLE courses
ADD COLUMN publish_status VARCHAR(16) NOT NULL DEFAULT 'PUBLISHED'
CONSTRAINT chk_courses_publish_status CHECK (publish_status IN ('DRAFT', 'PUBLISHED'));
ALTER TABLE courses
ALTER COLUMN publish_status SET DEFAULT 'DRAFT';
ALTER TABLE modules
ADD COLUMN publish_status VARCHAR(16) NOT NULL DEFAULT 'PUBLISHED'
CONSTRAINT chk_modules_publish_status CHECK (publish_status IN ('DRAFT', 'PUBLISHED'));
ALTER TABLE modules
ALTER COLUMN publish_status SET DEFAULT 'DRAFT';
-- Exam-prep hierarchy
ALTER TABLE exam_prep.catalog_courses
ADD COLUMN publish_status VARCHAR(16) NOT NULL DEFAULT 'PUBLISHED'
CONSTRAINT chk_exam_prep_catalog_courses_publish_status CHECK (publish_status IN ('DRAFT', 'PUBLISHED'));
ALTER TABLE exam_prep.catalog_courses
ALTER COLUMN publish_status SET DEFAULT 'DRAFT';
ALTER TABLE exam_prep.units
ADD COLUMN publish_status VARCHAR(16) NOT NULL DEFAULT 'PUBLISHED'
CONSTRAINT chk_exam_prep_units_publish_status CHECK (publish_status IN ('DRAFT', 'PUBLISHED'));
ALTER TABLE exam_prep.units
ALTER COLUMN publish_status SET DEFAULT 'DRAFT';
ALTER TABLE exam_prep.unit_modules
ADD COLUMN publish_status VARCHAR(16) NOT NULL DEFAULT 'PUBLISHED'
CONSTRAINT chk_exam_prep_unit_modules_publish_status CHECK (publish_status IN ('DRAFT', 'PUBLISHED'));
ALTER TABLE exam_prep.unit_modules
ALTER COLUMN publish_status SET DEFAULT 'DRAFT';
ALTER TABLE exam_prep.unit_module_lessons
ADD COLUMN publish_status VARCHAR(16) NOT NULL DEFAULT 'PUBLISHED'
CONSTRAINT chk_exam_prep_unit_module_lessons_publish_status CHECK (publish_status IN ('DRAFT', 'PUBLISHED'));
ALTER TABLE exam_prep.unit_module_lessons
ALTER COLUMN publish_status SET DEFAULT 'DRAFT';

View File

@ -1,5 +1,5 @@
-- name: ExamPrepCreateCatalogCourse :one
INSERT INTO exam_prep.catalog_courses (name, description, category, thumbnail, sort_order)
INSERT INTO exam_prep.catalog_courses (name, description, category, thumbnail, sort_order, publish_status)
SELECT
$1,
$2,
@ -8,7 +8,8 @@ SELECT
coalesce((
SELECT
max(c.sort_order)
FROM exam_prep.catalog_courses AS c), 0) + 1
FROM exam_prep.catalog_courses AS c), 0) + 1,
$5
RETURNING
*;
@ -22,6 +23,10 @@ SELECT
INNER JOIN exam_prep.unit_modules m ON m.id = l.unit_module_id
INNER JOIN exam_prep.units u ON u.id = m.unit_id
WHERE u.catalog_course_id = c.id
AND p.publish_status = 'PUBLISHED'
AND l.publish_status = 'PUBLISHED'
AND m.publish_status = 'PUBLISHED'
AND u.publish_status = 'PUBLISHED'
) AS has_practice
FROM exam_prep.catalog_courses c
WHERE c.id = $1;
@ -35,7 +40,10 @@ WITH catalog_course_counts AS (
COUNT(DISTINCT l.id)::BIGINT AS lessons_count
FROM exam_prep.units u
LEFT JOIN exam_prep.unit_modules m ON m.unit_id = u.id
AND m.publish_status = 'PUBLISHED'
LEFT JOIN exam_prep.unit_module_lessons l ON l.unit_module_id = m.id
AND l.publish_status = 'PUBLISHED'
WHERE u.publish_status = 'PUBLISHED'
GROUP BY u.catalog_course_id
)
SELECT
@ -46,6 +54,7 @@ SELECT
c.category,
c.thumbnail,
c.sort_order,
c.publish_status,
COALESCE(cc.units_count, 0)::BIGINT AS units_count,
COALESCE(cc.modules_count, 0)::BIGINT AS modules_count,
COALESCE(cc.lessons_count, 0)::BIGINT AS lessons_count,
@ -56,11 +65,19 @@ SELECT
INNER JOIN exam_prep.unit_modules m ON m.id = l.unit_module_id
INNER JOIN exam_prep.units u ON u.id = m.unit_id
WHERE u.catalog_course_id = c.id
AND p.publish_status = 'PUBLISHED'
AND l.publish_status = 'PUBLISHED'
AND m.publish_status = 'PUBLISHED'
AND u.publish_status = 'PUBLISHED'
) AS has_practice,
c.created_at,
c.updated_at
FROM exam_prep.catalog_courses c
LEFT JOIN catalog_course_counts cc ON cc.catalog_course_id = c.id
WHERE (
sqlc.arg('published_only')::boolean = FALSE
OR c.publish_status = 'PUBLISHED'::TEXT
)
ORDER BY c.sort_order ASC, c.id ASC
LIMIT $1 OFFSET $2;
@ -78,6 +95,7 @@ SET
category = coalesce(sqlc.narg('category')::varchar, category),
thumbnail = coalesce(sqlc.narg('thumbnail')::text, thumbnail),
sort_order = coalesce(sqlc.narg('sort_order')::int, sort_order),
publish_status = coalesce(sqlc.narg('publish_status')::varchar, publish_status),
updated_at = CURRENT_TIMESTAMP
WHERE id = sqlc.arg('id')
RETURNING

View File

@ -34,6 +34,7 @@ FROM
INNER JOIN question_sets qs ON qs.id = p.question_set_id
WHERE
l.unit_module_id = $1
AND l.publish_status = 'PUBLISHED'
AND qs.set_type = 'PRACTICE'
AND qs.status = 'PUBLISHED'
AND p.publish_status = 'PUBLISHED';
@ -48,6 +49,7 @@ FROM
INNER JOIN user_practice_progress upp ON upp.question_set_id = p.question_set_id
WHERE
l.unit_module_id = $1
AND l.publish_status = 'PUBLISHED'
AND upp.user_id = $2
AND upp.completed_at IS NOT NULL
AND qs.set_type = 'PRACTICE'
@ -64,6 +66,8 @@ FROM
INNER JOIN question_sets qs ON qs.id = p.question_set_id
WHERE
m.unit_id = $1
AND l.publish_status = 'PUBLISHED'
AND m.publish_status = 'PUBLISHED'
AND qs.set_type = 'PRACTICE'
AND qs.status = 'PUBLISHED'
AND p.publish_status = 'PUBLISHED';
@ -79,6 +83,8 @@ FROM
INNER JOIN user_practice_progress upp ON upp.question_set_id = p.question_set_id
WHERE
m.unit_id = $1
AND l.publish_status = 'PUBLISHED'
AND m.publish_status = 'PUBLISHED'
AND upp.user_id = $2
AND upp.completed_at IS NOT NULL
AND qs.set_type = 'PRACTICE'
@ -96,6 +102,9 @@ FROM
INNER JOIN question_sets qs ON qs.id = p.question_set_id
WHERE
u.catalog_course_id = $1
AND l.publish_status = 'PUBLISHED'
AND m.publish_status = 'PUBLISHED'
AND u.publish_status = 'PUBLISHED'
AND qs.set_type = 'PRACTICE'
AND qs.status = 'PUBLISHED'
AND p.publish_status = 'PUBLISHED';
@ -112,6 +121,9 @@ FROM
INNER JOIN user_practice_progress upp ON upp.question_set_id = p.question_set_id
WHERE
u.catalog_course_id = $1
AND l.publish_status = 'PUBLISHED'
AND m.publish_status = 'PUBLISHED'
AND u.publish_status = 'PUBLISHED'
AND upp.user_id = $2
AND upp.completed_at IS NOT NULL
AND qs.set_type = 'PRACTICE'

View File

@ -1,5 +1,5 @@
-- name: ExamPrepCreateUnitModuleLesson :one
INSERT INTO exam_prep.unit_module_lessons (unit_module_id, title, video_url, thumbnail, description, sort_order)
INSERT INTO exam_prep.unit_module_lessons (unit_module_id, title, video_url, thumbnail, description, sort_order, publish_status)
SELECT
$1,
$2,
@ -11,7 +11,8 @@ SELECT
max(l.sort_order)
FROM exam_prep.unit_module_lessons l
WHERE
l.unit_module_id = $1), 0) + 1
l.unit_module_id = $1), 0) + 1,
$6
RETURNING
*;
@ -22,6 +23,7 @@ SELECT
SELECT 1
FROM exam_prep.lesson_practices p
WHERE p.unit_module_lesson_id = l.id
AND p.publish_status = 'PUBLISHED'
) AS has_practice
FROM exam_prep.unit_module_lessons l
WHERE l.id = $1;
@ -35,6 +37,17 @@ WHERE
ORDER BY
l.id;
-- Published lessons only, for learner-facing progress rollups.
-- name: ExamPrepListPublishedUnitModuleLessonIDsByUnitModule :many
SELECT
l.id
FROM exam_prep.unit_module_lessons l
WHERE
l.unit_module_id = $1
AND l.publish_status = 'PUBLISHED'
ORDER BY
l.id;
-- name: ExamPrepListUnitModuleLessonsByUnitModuleID :many
SELECT
COUNT(*) OVER () AS total_count,
@ -45,16 +58,22 @@ SELECT
l.thumbnail,
l.description,
l.sort_order,
l.publish_status,
EXISTS (
SELECT 1
FROM exam_prep.lesson_practices p
WHERE p.unit_module_lesson_id = l.id
AND p.publish_status = 'PUBLISHED'
) AS has_practice,
l.created_at,
l.updated_at
FROM exam_prep.unit_module_lessons l
WHERE
l.unit_module_id = $1
AND (
sqlc.arg('published_only')::boolean = FALSE
OR l.publish_status = 'PUBLISHED'::TEXT
)
ORDER BY
l.sort_order ASC,
l.id ASC
@ -69,6 +88,7 @@ SET
thumbnail = coalesce(sqlc.narg('thumbnail')::text, thumbnail),
description = coalesce(sqlc.narg('description')::text, description),
sort_order = coalesce(sqlc.narg('sort_order')::int, sort_order),
publish_status = coalesce(sqlc.narg('publish_status')::varchar, publish_status),
updated_at = CURRENT_TIMESTAMP
WHERE id = sqlc.arg('id')
RETURNING

View File

@ -1,5 +1,5 @@
-- name: ExamPrepCreateUnitModule :one
INSERT INTO exam_prep.unit_modules (unit_id, name, description, thumbnail, icon, sort_order)
INSERT INTO exam_prep.unit_modules (unit_id, name, description, thumbnail, icon, sort_order, publish_status)
SELECT
$1,
$2,
@ -11,7 +11,8 @@ SELECT
max(m.sort_order)
FROM exam_prep.unit_modules m
WHERE
m.unit_id = $1), 0) + 1
m.unit_id = $1), 0) + 1,
$6
RETURNING
*;
@ -23,6 +24,8 @@ SELECT
FROM exam_prep.lesson_practices p
INNER JOIN exam_prep.unit_module_lessons l ON l.id = p.unit_module_lesson_id
WHERE l.unit_module_id = m.id
AND p.publish_status = 'PUBLISHED'
AND l.publish_status = 'PUBLISHED'
) AS has_practice
FROM exam_prep.unit_modules m
WHERE m.id = $1;
@ -36,6 +39,17 @@ WHERE
ORDER BY
m.id;
-- Published modules only, for learner-facing progress rollups.
-- name: ExamPrepListPublishedUnitModuleIDsByUnit :many
SELECT
m.id
FROM exam_prep.unit_modules m
WHERE
m.unit_id = $1
AND m.publish_status = 'PUBLISHED'
ORDER BY
m.id;
-- name: ExamPrepListUnitModulesByUnit :many
WITH module_counts AS (
SELECT
@ -44,7 +58,9 @@ WITH module_counts AS (
COUNT(DISTINCT p.id)::BIGINT AS practices_count
FROM exam_prep.unit_modules m
LEFT JOIN exam_prep.unit_module_lessons l ON l.unit_module_id = m.id
AND l.publish_status = 'PUBLISHED'
LEFT JOIN exam_prep.lesson_practices p ON p.unit_module_lesson_id = l.id
AND p.publish_status = 'PUBLISHED'
GROUP BY m.id
)
SELECT
@ -56,6 +72,7 @@ SELECT
m.thumbnail,
m.icon,
m.sort_order,
m.publish_status,
COALESCE(mc.lessons_count, 0)::BIGINT AS lessons_count,
COALESCE(mc.practices_count, 0)::BIGINT AS practices_count,
(COALESCE(mc.practices_count, 0)::BIGINT > 0) AS has_practice,
@ -65,6 +82,10 @@ FROM exam_prep.unit_modules m
LEFT JOIN module_counts mc ON mc.module_id = m.id
WHERE
m.unit_id = $1
AND (
sqlc.arg('published_only')::boolean = FALSE
OR m.publish_status = 'PUBLISHED'::TEXT
)
ORDER BY
m.sort_order ASC,
m.id ASC
@ -79,6 +100,7 @@ SET
thumbnail = coalesce(sqlc.narg('thumbnail')::text, thumbnail),
icon = coalesce(sqlc.narg('icon')::text, icon),
sort_order = coalesce(sqlc.narg('sort_order')::int, sort_order),
publish_status = coalesce(sqlc.narg('publish_status')::varchar, publish_status),
updated_at = CURRENT_TIMESTAMP
WHERE id = sqlc.arg('id')
RETURNING

View File

@ -1,5 +1,5 @@
-- name: ExamPrepCreateUnit :one
INSERT INTO exam_prep.units (catalog_course_id, name, description, thumbnail, sort_order)
INSERT INTO exam_prep.units (catalog_course_id, name, description, thumbnail, sort_order, publish_status)
SELECT
sqlc.arg('catalog_course_id'),
sqlc.arg('name'),
@ -11,7 +11,8 @@ SELECT
max(u.sort_order)
FROM exam_prep.units u
WHERE
u.catalog_course_id = sqlc.arg('catalog_course_id')), 0) + 1)
u.catalog_course_id = sqlc.arg('catalog_course_id')), 0) + 1),
sqlc.arg('publish_status')
RETURNING
*;
@ -24,6 +25,9 @@ SELECT
INNER JOIN exam_prep.unit_module_lessons l ON l.id = p.unit_module_lesson_id
INNER JOIN exam_prep.unit_modules m ON m.id = l.unit_module_id
WHERE m.unit_id = u.id
AND p.publish_status = 'PUBLISHED'
AND l.publish_status = 'PUBLISHED'
AND m.publish_status = 'PUBLISHED'
) AS has_practice
FROM exam_prep.units u
WHERE u.id = $1;
@ -37,6 +41,17 @@ WHERE
ORDER BY
u.id;
-- Published units only, for learner-facing progress rollups.
-- name: ExamPrepListPublishedUnitIDsByCatalogCourse :many
SELECT
u.id
FROM exam_prep.units u
WHERE
u.catalog_course_id = $1
AND u.publish_status = 'PUBLISHED'
ORDER BY
u.id;
-- name: ExamPrepListUnitsByCatalogCourse :many
WITH unit_counts AS (
SELECT
@ -46,8 +61,11 @@ WITH unit_counts AS (
COUNT(DISTINCT p.id)::BIGINT AS practices_count
FROM exam_prep.units u
LEFT JOIN exam_prep.unit_modules m ON m.unit_id = u.id
AND m.publish_status = 'PUBLISHED'
LEFT JOIN exam_prep.unit_module_lessons l ON l.unit_module_id = m.id
AND l.publish_status = 'PUBLISHED'
LEFT JOIN exam_prep.lesson_practices p ON p.unit_module_lesson_id = l.id
AND p.publish_status = 'PUBLISHED'
GROUP BY u.id
)
SELECT
@ -58,6 +76,7 @@ SELECT
u.description,
u.thumbnail,
u.sort_order,
u.publish_status,
COALESCE(uc.modules_count, 0)::BIGINT AS modules_count,
COALESCE(uc.lessons_count, 0)::BIGINT AS lessons_count,
COALESCE(uc.practices_count, 0)::BIGINT AS practices_count,
@ -68,6 +87,10 @@ FROM exam_prep.units u
LEFT JOIN unit_counts uc ON uc.unit_id = u.id
WHERE
u.catalog_course_id = $1
AND (
sqlc.arg('published_only')::boolean = FALSE
OR u.publish_status = 'PUBLISHED'::TEXT
)
ORDER BY
u.sort_order ASC,
u.id ASC
@ -81,6 +104,7 @@ SET
description = coalesce(sqlc.narg('description')::text, description),
thumbnail = coalesce(sqlc.narg('thumbnail')::text, thumbnail),
sort_order = coalesce(sqlc.narg('sort_order')::int, sort_order),
publish_status = coalesce(sqlc.narg('publish_status')::varchar, publish_status),
updated_at = CURRENT_TIMESTAMP
WHERE id = sqlc.arg('id')
RETURNING

View File

@ -1,5 +1,5 @@
-- name: CreateCourse :one
INSERT INTO courses (program_id, name, description, thumbnail, sort_order)
INSERT INTO courses (program_id, name, description, thumbnail, sort_order, publish_status)
SELECT
sqlc.arg('program_id'),
sqlc.arg('name'),
@ -11,7 +11,8 @@ SELECT
max(c.sort_order)
FROM courses c
WHERE
c.program_id = sqlc.arg('program_id')), 0) + 1)
c.program_id = sqlc.arg('program_id')), 0) + 1),
sqlc.arg('publish_status')
RETURNING
*;
@ -40,6 +41,18 @@ WHERE
ORDER BY
c.id;
-- Published courses only, for learner-facing progress rollups.
-- name: ListPublishedCourseIDsByProgram :many
SELECT
c.id
FROM
courses AS c
WHERE
c.program_id = $1
AND c.publish_status = 'PUBLISHED'
ORDER BY
c.id;
-- name: ListCoursesByProgramID :many
SELECT
COUNT(*) OVER () AS total_count,
@ -49,6 +62,7 @@ SELECT
c.description,
c.thumbnail,
c.sort_order,
c.publish_status,
c.created_at,
c.updated_at,
(
@ -57,7 +71,8 @@ SELECT
FROM
modules m
WHERE
m.course_id = c.id) AS module_count,
m.course_id = c.id
AND m.publish_status = 'PUBLISHED') AS module_count,
(
SELECT
COUNT(*)::bigint
@ -91,6 +106,10 @@ FROM
courses c
WHERE
c.program_id = $1
AND (
sqlc.arg('published_only')::boolean = FALSE
OR c.publish_status = 'PUBLISHED'::TEXT
)
ORDER BY
c.sort_order ASC,
c.id ASC
@ -103,6 +122,7 @@ SET
description = COALESCE(sqlc.narg('description')::text, description),
thumbnail = COALESCE(sqlc.narg('thumbnail')::text, thumbnail),
sort_order = coalesce(sqlc.narg('sort_order')::int, sort_order),
publish_status = COALESCE(sqlc.narg('publish_status')::varchar, publish_status),
updated_at = CURRENT_TIMESTAMP
WHERE
id = sqlc.arg('id')

View File

@ -1,5 +1,5 @@
-- name: CreateModule :one
INSERT INTO modules (program_id, course_id, name, description, icon, sort_order)
INSERT INTO modules (program_id, course_id, name, description, icon, sort_order, publish_status)
SELECT
sqlc.arg('program_id'),
sqlc.arg('course_id'),
@ -12,7 +12,8 @@ SELECT
max(m.sort_order)
FROM modules m
WHERE
m.course_id = sqlc.arg('course_id')), 0) + 1)
m.course_id = sqlc.arg('course_id')), 0) + 1),
sqlc.arg('publish_status')
RETURNING
*;
@ -40,6 +41,18 @@ WHERE
ORDER BY
m.id;
-- Published modules only, for learner-facing progress rollups.
-- name: ListPublishedModuleIDsByCourse :many
SELECT
m.id
FROM
modules AS m
WHERE
m.course_id = $1
AND m.publish_status = 'PUBLISHED'
ORDER BY
m.id;
-- name: ListModulesByProgramAndCourse :many
SELECT
COUNT(*) OVER () AS total_count,
@ -50,6 +63,7 @@ SELECT
m.description,
m.icon,
m.sort_order,
m.publish_status,
m.created_at,
m.updated_at,
EXISTS (
@ -64,6 +78,10 @@ FROM
WHERE
m.program_id = $1
AND m.course_id = $2
AND (
sqlc.arg('published_only')::boolean = FALSE
OR m.publish_status = 'PUBLISHED'::TEXT
)
ORDER BY
m.sort_order ASC,
m.id ASC
@ -77,6 +95,7 @@ SET
description = COALESCE(sqlc.narg('description')::text, description),
icon = COALESCE(sqlc.narg('icon')::text, icon),
sort_order = coalesce(sqlc.narg('sort_order')::int, sort_order),
publish_status = COALESCE(sqlc.narg('publish_status')::varchar, publish_status),
updated_at = CURRENT_TIMESTAMP
WHERE
id = sqlc.arg('id')

View File

@ -5,6 +5,7 @@ SELECT
FROM
programs AS p1
INNER JOIN programs AS p2 ON p2.category = p1.category
AND p2.publish_status = 'PUBLISHED'
AND (
p2.sort_order < p1.sort_order
OR (
@ -25,6 +26,7 @@ SELECT
FROM
courses AS c1
INNER JOIN courses AS c2 ON c2.program_id = c1.program_id
AND c2.publish_status = 'PUBLISHED'
AND (
c2.sort_order < c1.sort_order
OR (
@ -45,6 +47,7 @@ SELECT
FROM
modules AS m1
INNER JOIN modules AS m2 ON m2.course_id = m1.course_id
AND m2.publish_status = 'PUBLISHED'
AND (
m2.sort_order < m1.sort_order
OR (
@ -226,7 +229,8 @@ SELECT
FROM
modules
WHERE
course_id = $1;
course_id = $1
AND publish_status = 'PUBLISHED';
-- name: CountUserCompletedModulesInCourse :one
SELECT
@ -236,7 +240,8 @@ FROM
INNER JOIN modules m ON m.id = ump.module_id
WHERE
m.course_id = $1
AND ump.user_id = $2;
AND ump.user_id = $2
AND m.publish_status = 'PUBLISHED';
-- name: CountCoursesInProgram :one
SELECT
@ -244,7 +249,8 @@ SELECT
FROM
courses
WHERE
program_id = $1;
program_id = $1
AND publish_status = 'PUBLISHED';
-- name: CountUserCompletedCoursesInProgram :one
SELECT
@ -254,7 +260,8 @@ FROM
INNER JOIN courses c ON c.id = ucp.course_id
WHERE
c.program_id = $1
AND ucp.user_id = $2;
AND ucp.user_id = $2
AND c.publish_status = 'PUBLISHED';
-- name: ListLMSCompletedLessonIDsByUser :many
SELECT
@ -448,7 +455,7 @@ WHERE
AND ulp.user_id = $2
AND l.publish_status = 'PUBLISHED';
-- Published practices in a module (direct module practices and practices on lessons in the module).
-- Published practices in a module (direct module practices and practices on published lessons in the module).
-- name: CountPublishedPracticesInModule :one
SELECT
count(*)::int AS n
@ -464,7 +471,8 @@ WHERE
FROM
lessons
WHERE
module_id = $1))
module_id = $1
AND publish_status = 'PUBLISHED'))
AND qs.set_type = 'PRACTICE'
AND qs.status = 'PUBLISHED'
AND lp.publish_status = 'PUBLISHED';
@ -485,7 +493,8 @@ WHERE
FROM
lessons
WHERE
module_id = $1))
module_id = $1
AND publish_status = 'PUBLISHED'))
AND upp.user_id = $2
AND upp.completed_at IS NOT NULL
AND qs.set_type = 'PRACTICE'
@ -507,7 +516,8 @@ WHERE
FROM
modules
WHERE
course_id = $1)
course_id = $1
AND publish_status = 'PUBLISHED')
OR lp.lesson_id IN (
SELECT
l.id
@ -515,7 +525,9 @@ WHERE
lessons l
INNER JOIN modules m ON m.id = l.module_id
WHERE
m.course_id = $1))
m.course_id = $1
AND l.publish_status = 'PUBLISHED'
AND m.publish_status = 'PUBLISHED'))
AND qs.set_type = 'PRACTICE'
AND qs.status = 'PUBLISHED'
AND lp.publish_status = 'PUBLISHED';
@ -536,7 +548,8 @@ WHERE
FROM
modules
WHERE
course_id = $1)
course_id = $1
AND publish_status = 'PUBLISHED')
OR lp.lesson_id IN (
SELECT
l.id
@ -544,7 +557,9 @@ WHERE
lessons l
INNER JOIN modules m ON m.id = l.module_id
WHERE
m.course_id = $1))
m.course_id = $1
AND l.publish_status = 'PUBLISHED'
AND m.publish_status = 'PUBLISHED'))
AND upp.user_id = $2
AND upp.completed_at IS NOT NULL
AND qs.set_type = 'PRACTICE'
@ -565,7 +580,8 @@ WHERE
FROM
courses c
WHERE
c.program_id = $1)
c.program_id = $1
AND c.publish_status = 'PUBLISHED')
OR lp.module_id IN (
SELECT
m.id
@ -573,7 +589,9 @@ WHERE
modules m
INNER JOIN courses c ON c.id = m.course_id
WHERE
c.program_id = $1)
c.program_id = $1
AND m.publish_status = 'PUBLISHED'
AND c.publish_status = 'PUBLISHED')
OR lp.lesson_id IN (
SELECT
l.id
@ -582,7 +600,10 @@ WHERE
INNER JOIN modules m ON m.id = l.module_id
INNER JOIN courses c ON c.id = m.course_id
WHERE
c.program_id = $1))
c.program_id = $1
AND l.publish_status = 'PUBLISHED'
AND m.publish_status = 'PUBLISHED'
AND c.publish_status = 'PUBLISHED'))
AND qs.set_type = 'PRACTICE'
AND qs.status = 'PUBLISHED'
AND lp.publish_status = 'PUBLISHED';
@ -602,7 +623,8 @@ WHERE
FROM
courses c
WHERE
c.program_id = $1)
c.program_id = $1
AND c.publish_status = 'PUBLISHED')
OR lp.module_id IN (
SELECT
m.id
@ -610,7 +632,9 @@ WHERE
modules m
INNER JOIN courses c ON c.id = m.course_id
WHERE
c.program_id = $1)
c.program_id = $1
AND m.publish_status = 'PUBLISHED'
AND c.publish_status = 'PUBLISHED')
OR lp.lesson_id IN (
SELECT
l.id
@ -619,7 +643,10 @@ WHERE
INNER JOIN modules m ON m.id = l.module_id
INNER JOIN courses c ON c.id = m.course_id
WHERE
c.program_id = $1))
c.program_id = $1
AND l.publish_status = 'PUBLISHED'
AND m.publish_status = 'PUBLISHED'
AND c.publish_status = 'PUBLISHED'))
AND upp.user_id = $2
AND upp.completed_at IS NOT NULL
AND qs.set_type = 'PRACTICE'

View File

@ -1,5 +1,5 @@
-- name: CreateProgram :one
INSERT INTO programs (name, description, category, thumbnail, sort_order)
INSERT INTO programs (name, description, category, thumbnail, sort_order, publish_status)
SELECT
sqlc.arg('name'),
sqlc.arg('description'),
@ -8,7 +8,8 @@ SELECT
COALESCE(sqlc.narg('sort_order')::int, COALESCE((
SELECT
max(p.sort_order)
FROM programs AS p), 0) + 1)
FROM programs AS p), 0) + 1),
sqlc.arg('publish_status')
RETURNING
*;
@ -34,9 +35,14 @@ SELECT
p.category,
p.thumbnail,
p.sort_order,
p.publish_status,
p.created_at,
p.updated_at
FROM programs p
WHERE (
sqlc.arg('published_only')::boolean = FALSE
OR p.publish_status = 'PUBLISHED'::TEXT
)
ORDER BY p.sort_order ASC, p.id ASC
LIMIT $1 OFFSET $2;
@ -48,6 +54,7 @@ SET
category = COALESCE(sqlc.narg('category')::varchar, category),
thumbnail = COALESCE(sqlc.narg('thumbnail')::text, thumbnail),
sort_order = coalesce(sqlc.narg('sort_order')::int, sort_order),
publish_status = COALESCE(sqlc.narg('publish_status')::varchar, publish_status),
updated_at = CURRENT_TIMESTAMP
WHERE
id = sqlc.arg('id')

View File

@ -12,7 +12,7 @@ import (
)
const ExamPrepCreateCatalogCourse = `-- name: ExamPrepCreateCatalogCourse :one
INSERT INTO exam_prep.catalog_courses (name, description, category, thumbnail, sort_order)
INSERT INTO exam_prep.catalog_courses (name, description, category, thumbnail, sort_order, publish_status)
SELECT
$1,
$2,
@ -21,9 +21,10 @@ SELECT
coalesce((
SELECT
max(c.sort_order)
FROM exam_prep.catalog_courses AS c), 0) + 1
FROM exam_prep.catalog_courses AS c), 0) + 1,
$5
RETURNING
id, name, description, thumbnail, sort_order, created_at, updated_at, category
id, name, description, thumbnail, sort_order, created_at, updated_at, category, publish_status
`
type ExamPrepCreateCatalogCourseParams struct {
@ -31,6 +32,7 @@ type ExamPrepCreateCatalogCourseParams struct {
Description pgtype.Text `json:"description"`
Category string `json:"category"`
Thumbnail pgtype.Text `json:"thumbnail"`
PublishStatus string `json:"publish_status"`
}
func (q *Queries) ExamPrepCreateCatalogCourse(ctx context.Context, arg ExamPrepCreateCatalogCourseParams) (ExamPrepCatalogCourse, error) {
@ -39,6 +41,7 @@ func (q *Queries) ExamPrepCreateCatalogCourse(ctx context.Context, arg ExamPrepC
arg.Description,
arg.Category,
arg.Thumbnail,
arg.PublishStatus,
)
var i ExamPrepCatalogCourse
err := row.Scan(
@ -50,6 +53,7 @@ func (q *Queries) ExamPrepCreateCatalogCourse(ctx context.Context, arg ExamPrepC
&i.CreatedAt,
&i.UpdatedAt,
&i.Category,
&i.PublishStatus,
)
return i, err
}
@ -66,7 +70,7 @@ func (q *Queries) ExamPrepDeleteCatalogCourse(ctx context.Context, id int64) err
const ExamPrepGetCatalogCourseByID = `-- name: ExamPrepGetCatalogCourseByID :one
SELECT
c.id, c.name, c.description, c.thumbnail, c.sort_order, c.created_at, c.updated_at, c.category,
c.id, c.name, c.description, c.thumbnail, c.sort_order, c.created_at, c.updated_at, c.category, c.publish_status,
EXISTS (
SELECT 1
FROM exam_prep.lesson_practices p
@ -74,6 +78,10 @@ SELECT
INNER JOIN exam_prep.unit_modules m ON m.id = l.unit_module_id
INNER JOIN exam_prep.units u ON u.id = m.unit_id
WHERE u.catalog_course_id = c.id
AND p.publish_status = 'PUBLISHED'
AND l.publish_status = 'PUBLISHED'
AND m.publish_status = 'PUBLISHED'
AND u.publish_status = 'PUBLISHED'
) AS has_practice
FROM exam_prep.catalog_courses c
WHERE c.id = $1
@ -88,6 +96,7 @@ type ExamPrepGetCatalogCourseByIDRow struct {
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
Category string `json:"category"`
PublishStatus string `json:"publish_status"`
HasPractice bool `json:"has_practice"`
}
@ -103,6 +112,7 @@ func (q *Queries) ExamPrepGetCatalogCourseByID(ctx context.Context, id int64) (E
&i.CreatedAt,
&i.UpdatedAt,
&i.Category,
&i.PublishStatus,
&i.HasPractice,
)
return i, err
@ -144,7 +154,10 @@ WITH catalog_course_counts AS (
COUNT(DISTINCT l.id)::BIGINT AS lessons_count
FROM exam_prep.units u
LEFT JOIN exam_prep.unit_modules m ON m.unit_id = u.id
AND m.publish_status = 'PUBLISHED'
LEFT JOIN exam_prep.unit_module_lessons l ON l.unit_module_id = m.id
AND l.publish_status = 'PUBLISHED'
WHERE u.publish_status = 'PUBLISHED'
GROUP BY u.catalog_course_id
)
SELECT
@ -155,6 +168,7 @@ SELECT
c.category,
c.thumbnail,
c.sort_order,
c.publish_status,
COALESCE(cc.units_count, 0)::BIGINT AS units_count,
COALESCE(cc.modules_count, 0)::BIGINT AS modules_count,
COALESCE(cc.lessons_count, 0)::BIGINT AS lessons_count,
@ -165,11 +179,19 @@ SELECT
INNER JOIN exam_prep.unit_modules m ON m.id = l.unit_module_id
INNER JOIN exam_prep.units u ON u.id = m.unit_id
WHERE u.catalog_course_id = c.id
AND p.publish_status = 'PUBLISHED'
AND l.publish_status = 'PUBLISHED'
AND m.publish_status = 'PUBLISHED'
AND u.publish_status = 'PUBLISHED'
) AS has_practice,
c.created_at,
c.updated_at
FROM exam_prep.catalog_courses c
LEFT JOIN catalog_course_counts cc ON cc.catalog_course_id = c.id
WHERE (
$3::boolean = FALSE
OR c.publish_status = 'PUBLISHED'::TEXT
)
ORDER BY c.sort_order ASC, c.id ASC
LIMIT $1 OFFSET $2
`
@ -177,6 +199,7 @@ LIMIT $1 OFFSET $2
type ExamPrepListCatalogCoursesParams struct {
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
PublishedOnly bool `json:"published_only"`
}
type ExamPrepListCatalogCoursesRow struct {
@ -187,6 +210,7 @@ type ExamPrepListCatalogCoursesRow struct {
Category string `json:"category"`
Thumbnail pgtype.Text `json:"thumbnail"`
SortOrder int32 `json:"sort_order"`
PublishStatus string `json:"publish_status"`
UnitsCount int64 `json:"units_count"`
ModulesCount int64 `json:"modules_count"`
LessonsCount int64 `json:"lessons_count"`
@ -196,7 +220,7 @@ type ExamPrepListCatalogCoursesRow struct {
}
func (q *Queries) ExamPrepListCatalogCourses(ctx context.Context, arg ExamPrepListCatalogCoursesParams) ([]ExamPrepListCatalogCoursesRow, error) {
rows, err := q.db.Query(ctx, ExamPrepListCatalogCourses, arg.Limit, arg.Offset)
rows, err := q.db.Query(ctx, ExamPrepListCatalogCourses, arg.Limit, arg.Offset, arg.PublishedOnly)
if err != nil {
return nil, err
}
@ -212,6 +236,7 @@ func (q *Queries) ExamPrepListCatalogCourses(ctx context.Context, arg ExamPrepLi
&i.Category,
&i.Thumbnail,
&i.SortOrder,
&i.PublishStatus,
&i.UnitsCount,
&i.ModulesCount,
&i.LessonsCount,
@ -237,10 +262,11 @@ SET
category = coalesce($3::varchar, category),
thumbnail = coalesce($4::text, thumbnail),
sort_order = coalesce($5::int, sort_order),
publish_status = coalesce($6::varchar, publish_status),
updated_at = CURRENT_TIMESTAMP
WHERE id = $6
WHERE id = $7
RETURNING
id, name, description, thumbnail, sort_order, created_at, updated_at, category
id, name, description, thumbnail, sort_order, created_at, updated_at, category, publish_status
`
type ExamPrepUpdateCatalogCourseParams struct {
@ -249,6 +275,7 @@ type ExamPrepUpdateCatalogCourseParams struct {
Category pgtype.Text `json:"category"`
Thumbnail pgtype.Text `json:"thumbnail"`
SortOrder pgtype.Int4 `json:"sort_order"`
PublishStatus pgtype.Text `json:"publish_status"`
ID int64 `json:"id"`
}
@ -259,6 +286,7 @@ func (q *Queries) ExamPrepUpdateCatalogCourse(ctx context.Context, arg ExamPrepU
arg.Category,
arg.Thumbnail,
arg.SortOrder,
arg.PublishStatus,
arg.ID,
)
var i ExamPrepCatalogCourse
@ -271,6 +299,7 @@ func (q *Queries) ExamPrepUpdateCatalogCourse(ctx context.Context, arg ExamPrepU
&i.CreatedAt,
&i.UpdatedAt,
&i.Category,
&i.PublishStatus,
)
return i, err
}

View File

@ -20,6 +20,9 @@ FROM
INNER JOIN question_sets qs ON qs.id = p.question_set_id
WHERE
u.catalog_course_id = $1
AND l.publish_status = 'PUBLISHED'
AND m.publish_status = 'PUBLISHED'
AND u.publish_status = 'PUBLISHED'
AND qs.set_type = 'PRACTICE'
AND qs.status = 'PUBLISHED'
AND p.publish_status = 'PUBLISHED'
@ -61,6 +64,7 @@ FROM
INNER JOIN question_sets qs ON qs.id = p.question_set_id
WHERE
l.unit_module_id = $1
AND l.publish_status = 'PUBLISHED'
AND qs.set_type = 'PRACTICE'
AND qs.status = 'PUBLISHED'
AND p.publish_status = 'PUBLISHED'
@ -83,6 +87,8 @@ FROM
INNER JOIN question_sets qs ON qs.id = p.question_set_id
WHERE
m.unit_id = $1
AND l.publish_status = 'PUBLISHED'
AND m.publish_status = 'PUBLISHED'
AND qs.set_type = 'PRACTICE'
AND qs.status = 'PUBLISHED'
AND p.publish_status = 'PUBLISHED'
@ -107,6 +113,9 @@ FROM
INNER JOIN user_practice_progress upp ON upp.question_set_id = p.question_set_id
WHERE
u.catalog_course_id = $1
AND l.publish_status = 'PUBLISHED'
AND m.publish_status = 'PUBLISHED'
AND u.publish_status = 'PUBLISHED'
AND upp.user_id = $2
AND upp.completed_at IS NOT NULL
AND qs.set_type = 'PRACTICE'
@ -164,6 +173,7 @@ FROM
INNER JOIN user_practice_progress upp ON upp.question_set_id = p.question_set_id
WHERE
l.unit_module_id = $1
AND l.publish_status = 'PUBLISHED'
AND upp.user_id = $2
AND upp.completed_at IS NOT NULL
AND qs.set_type = 'PRACTICE'
@ -194,6 +204,8 @@ FROM
INNER JOIN user_practice_progress upp ON upp.question_set_id = p.question_set_id
WHERE
m.unit_id = $1
AND l.publish_status = 'PUBLISHED'
AND m.publish_status = 'PUBLISHED'
AND upp.user_id = $2
AND upp.completed_at IS NOT NULL
AND qs.set_type = 'PRACTICE'

View File

@ -12,7 +12,7 @@ import (
)
const ExamPrepCreateUnitModuleLesson = `-- name: ExamPrepCreateUnitModuleLesson :one
INSERT INTO exam_prep.unit_module_lessons (unit_module_id, title, video_url, thumbnail, description, sort_order)
INSERT INTO exam_prep.unit_module_lessons (unit_module_id, title, video_url, thumbnail, description, sort_order, publish_status)
SELECT
$1,
$2,
@ -24,9 +24,10 @@ SELECT
max(l.sort_order)
FROM exam_prep.unit_module_lessons l
WHERE
l.unit_module_id = $1), 0) + 1
l.unit_module_id = $1), 0) + 1,
$6
RETURNING
id, unit_module_id, title, video_url, thumbnail, description, sort_order, created_at, updated_at
id, unit_module_id, title, video_url, thumbnail, description, sort_order, created_at, updated_at, publish_status
`
type ExamPrepCreateUnitModuleLessonParams struct {
@ -35,6 +36,7 @@ type ExamPrepCreateUnitModuleLessonParams struct {
VideoUrl pgtype.Text `json:"video_url"`
Thumbnail pgtype.Text `json:"thumbnail"`
Description pgtype.Text `json:"description"`
PublishStatus string `json:"publish_status"`
}
func (q *Queries) ExamPrepCreateUnitModuleLesson(ctx context.Context, arg ExamPrepCreateUnitModuleLessonParams) (ExamPrepUnitModuleLesson, error) {
@ -44,6 +46,7 @@ func (q *Queries) ExamPrepCreateUnitModuleLesson(ctx context.Context, arg ExamPr
arg.VideoUrl,
arg.Thumbnail,
arg.Description,
arg.PublishStatus,
)
var i ExamPrepUnitModuleLesson
err := row.Scan(
@ -56,6 +59,7 @@ func (q *Queries) ExamPrepCreateUnitModuleLesson(ctx context.Context, arg ExamPr
&i.SortOrder,
&i.CreatedAt,
&i.UpdatedAt,
&i.PublishStatus,
)
return i, err
}
@ -72,11 +76,12 @@ func (q *Queries) ExamPrepDeleteUnitModuleLesson(ctx context.Context, id int64)
const ExamPrepGetUnitModuleLessonByID = `-- name: ExamPrepGetUnitModuleLessonByID :one
SELECT
l.id, l.unit_module_id, l.title, l.video_url, l.thumbnail, l.description, l.sort_order, l.created_at, l.updated_at,
l.id, l.unit_module_id, l.title, l.video_url, l.thumbnail, l.description, l.sort_order, l.created_at, l.updated_at, l.publish_status,
EXISTS (
SELECT 1
FROM exam_prep.lesson_practices p
WHERE p.unit_module_lesson_id = l.id
AND p.publish_status = 'PUBLISHED'
) AS has_practice
FROM exam_prep.unit_module_lessons l
WHERE l.id = $1
@ -92,6 +97,7 @@ type ExamPrepGetUnitModuleLessonByIDRow struct {
SortOrder int32 `json:"sort_order"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
PublishStatus string `json:"publish_status"`
HasPractice bool `json:"has_practice"`
}
@ -108,11 +114,44 @@ func (q *Queries) ExamPrepGetUnitModuleLessonByID(ctx context.Context, id int64)
&i.SortOrder,
&i.CreatedAt,
&i.UpdatedAt,
&i.PublishStatus,
&i.HasPractice,
)
return i, err
}
const ExamPrepListPublishedUnitModuleLessonIDsByUnitModule = `-- name: ExamPrepListPublishedUnitModuleLessonIDsByUnitModule :many
SELECT
l.id
FROM exam_prep.unit_module_lessons l
WHERE
l.unit_module_id = $1
AND l.publish_status = 'PUBLISHED'
ORDER BY
l.id
`
// Published lessons only, for learner-facing progress rollups.
func (q *Queries) ExamPrepListPublishedUnitModuleLessonIDsByUnitModule(ctx context.Context, unitModuleID int64) ([]int64, error) {
rows, err := q.db.Query(ctx, ExamPrepListPublishedUnitModuleLessonIDsByUnitModule, unitModuleID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []int64
for rows.Next() {
var id int64
if err := rows.Scan(&id); err != nil {
return nil, err
}
items = append(items, id)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const ExamPrepListUnitModuleLessonIDsByUnitModule = `-- name: ExamPrepListUnitModuleLessonIDsByUnitModule :many
SELECT
l.id
@ -153,16 +192,22 @@ SELECT
l.thumbnail,
l.description,
l.sort_order,
l.publish_status,
EXISTS (
SELECT 1
FROM exam_prep.lesson_practices p
WHERE p.unit_module_lesson_id = l.id
AND p.publish_status = 'PUBLISHED'
) AS has_practice,
l.created_at,
l.updated_at
FROM exam_prep.unit_module_lessons l
WHERE
l.unit_module_id = $1
AND (
$4::boolean = FALSE
OR l.publish_status = 'PUBLISHED'::TEXT
)
ORDER BY
l.sort_order ASC,
l.id ASC
@ -174,6 +219,7 @@ type ExamPrepListUnitModuleLessonsByUnitModuleIDParams struct {
UnitModuleID int64 `json:"unit_module_id"`
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
PublishedOnly bool `json:"published_only"`
}
type ExamPrepListUnitModuleLessonsByUnitModuleIDRow struct {
@ -185,13 +231,19 @@ type ExamPrepListUnitModuleLessonsByUnitModuleIDRow struct {
Thumbnail pgtype.Text `json:"thumbnail"`
Description pgtype.Text `json:"description"`
SortOrder int32 `json:"sort_order"`
PublishStatus string `json:"publish_status"`
HasPractice bool `json:"has_practice"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
func (q *Queries) ExamPrepListUnitModuleLessonsByUnitModuleID(ctx context.Context, arg ExamPrepListUnitModuleLessonsByUnitModuleIDParams) ([]ExamPrepListUnitModuleLessonsByUnitModuleIDRow, error) {
rows, err := q.db.Query(ctx, ExamPrepListUnitModuleLessonsByUnitModuleID, arg.UnitModuleID, arg.Limit, arg.Offset)
rows, err := q.db.Query(ctx, ExamPrepListUnitModuleLessonsByUnitModuleID,
arg.UnitModuleID,
arg.Limit,
arg.Offset,
arg.PublishedOnly,
)
if err != nil {
return nil, err
}
@ -208,6 +260,7 @@ func (q *Queries) ExamPrepListUnitModuleLessonsByUnitModuleID(ctx context.Contex
&i.Thumbnail,
&i.Description,
&i.SortOrder,
&i.PublishStatus,
&i.HasPractice,
&i.CreatedAt,
&i.UpdatedAt,
@ -230,10 +283,11 @@ SET
thumbnail = coalesce($3::text, thumbnail),
description = coalesce($4::text, description),
sort_order = coalesce($5::int, sort_order),
publish_status = coalesce($6::varchar, publish_status),
updated_at = CURRENT_TIMESTAMP
WHERE id = $6
WHERE id = $7
RETURNING
id, unit_module_id, title, video_url, thumbnail, description, sort_order, created_at, updated_at
id, unit_module_id, title, video_url, thumbnail, description, sort_order, created_at, updated_at, publish_status
`
type ExamPrepUpdateUnitModuleLessonParams struct {
@ -242,6 +296,7 @@ type ExamPrepUpdateUnitModuleLessonParams struct {
Thumbnail pgtype.Text `json:"thumbnail"`
Description pgtype.Text `json:"description"`
SortOrder pgtype.Int4 `json:"sort_order"`
PublishStatus pgtype.Text `json:"publish_status"`
ID int64 `json:"id"`
}
@ -252,6 +307,7 @@ func (q *Queries) ExamPrepUpdateUnitModuleLesson(ctx context.Context, arg ExamPr
arg.Thumbnail,
arg.Description,
arg.SortOrder,
arg.PublishStatus,
arg.ID,
)
var i ExamPrepUnitModuleLesson
@ -265,6 +321,7 @@ func (q *Queries) ExamPrepUpdateUnitModuleLesson(ctx context.Context, arg ExamPr
&i.SortOrder,
&i.CreatedAt,
&i.UpdatedAt,
&i.PublishStatus,
)
return i, err
}

View File

@ -12,7 +12,7 @@ import (
)
const ExamPrepCreateUnitModule = `-- name: ExamPrepCreateUnitModule :one
INSERT INTO exam_prep.unit_modules (unit_id, name, description, thumbnail, icon, sort_order)
INSERT INTO exam_prep.unit_modules (unit_id, name, description, thumbnail, icon, sort_order, publish_status)
SELECT
$1,
$2,
@ -24,9 +24,10 @@ SELECT
max(m.sort_order)
FROM exam_prep.unit_modules m
WHERE
m.unit_id = $1), 0) + 1
m.unit_id = $1), 0) + 1,
$6
RETURNING
id, unit_id, name, description, thumbnail, icon, sort_order, created_at, updated_at
id, unit_id, name, description, thumbnail, icon, sort_order, created_at, updated_at, publish_status
`
type ExamPrepCreateUnitModuleParams struct {
@ -35,6 +36,7 @@ type ExamPrepCreateUnitModuleParams struct {
Description pgtype.Text `json:"description"`
Thumbnail pgtype.Text `json:"thumbnail"`
Icon pgtype.Text `json:"icon"`
PublishStatus string `json:"publish_status"`
}
func (q *Queries) ExamPrepCreateUnitModule(ctx context.Context, arg ExamPrepCreateUnitModuleParams) (ExamPrepUnitModule, error) {
@ -44,6 +46,7 @@ func (q *Queries) ExamPrepCreateUnitModule(ctx context.Context, arg ExamPrepCrea
arg.Description,
arg.Thumbnail,
arg.Icon,
arg.PublishStatus,
)
var i ExamPrepUnitModule
err := row.Scan(
@ -56,6 +59,7 @@ func (q *Queries) ExamPrepCreateUnitModule(ctx context.Context, arg ExamPrepCrea
&i.SortOrder,
&i.CreatedAt,
&i.UpdatedAt,
&i.PublishStatus,
)
return i, err
}
@ -72,12 +76,14 @@ func (q *Queries) ExamPrepDeleteUnitModule(ctx context.Context, id int64) error
const ExamPrepGetUnitModuleByID = `-- name: ExamPrepGetUnitModuleByID :one
SELECT
m.id, m.unit_id, m.name, m.description, m.thumbnail, m.icon, m.sort_order, m.created_at, m.updated_at,
m.id, m.unit_id, m.name, m.description, m.thumbnail, m.icon, m.sort_order, m.created_at, m.updated_at, m.publish_status,
EXISTS (
SELECT 1
FROM exam_prep.lesson_practices p
INNER JOIN exam_prep.unit_module_lessons l ON l.id = p.unit_module_lesson_id
WHERE l.unit_module_id = m.id
AND p.publish_status = 'PUBLISHED'
AND l.publish_status = 'PUBLISHED'
) AS has_practice
FROM exam_prep.unit_modules m
WHERE m.id = $1
@ -93,6 +99,7 @@ type ExamPrepGetUnitModuleByIDRow struct {
SortOrder int32 `json:"sort_order"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
PublishStatus string `json:"publish_status"`
HasPractice bool `json:"has_practice"`
}
@ -109,11 +116,44 @@ func (q *Queries) ExamPrepGetUnitModuleByID(ctx context.Context, id int64) (Exam
&i.SortOrder,
&i.CreatedAt,
&i.UpdatedAt,
&i.PublishStatus,
&i.HasPractice,
)
return i, err
}
const ExamPrepListPublishedUnitModuleIDsByUnit = `-- name: ExamPrepListPublishedUnitModuleIDsByUnit :many
SELECT
m.id
FROM exam_prep.unit_modules m
WHERE
m.unit_id = $1
AND m.publish_status = 'PUBLISHED'
ORDER BY
m.id
`
// Published modules only, for learner-facing progress rollups.
func (q *Queries) ExamPrepListPublishedUnitModuleIDsByUnit(ctx context.Context, unitID int64) ([]int64, error) {
rows, err := q.db.Query(ctx, ExamPrepListPublishedUnitModuleIDsByUnit, unitID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []int64
for rows.Next() {
var id int64
if err := rows.Scan(&id); err != nil {
return nil, err
}
items = append(items, id)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const ExamPrepListUnitModuleIDsByUnit = `-- name: ExamPrepListUnitModuleIDsByUnit :many
SELECT
m.id
@ -152,7 +192,9 @@ WITH module_counts AS (
COUNT(DISTINCT p.id)::BIGINT AS practices_count
FROM exam_prep.unit_modules m
LEFT JOIN exam_prep.unit_module_lessons l ON l.unit_module_id = m.id
AND l.publish_status = 'PUBLISHED'
LEFT JOIN exam_prep.lesson_practices p ON p.unit_module_lesson_id = l.id
AND p.publish_status = 'PUBLISHED'
GROUP BY m.id
)
SELECT
@ -164,6 +206,7 @@ SELECT
m.thumbnail,
m.icon,
m.sort_order,
m.publish_status,
COALESCE(mc.lessons_count, 0)::BIGINT AS lessons_count,
COALESCE(mc.practices_count, 0)::BIGINT AS practices_count,
(COALESCE(mc.practices_count, 0)::BIGINT > 0) AS has_practice,
@ -173,6 +216,10 @@ FROM exam_prep.unit_modules m
LEFT JOIN module_counts mc ON mc.module_id = m.id
WHERE
m.unit_id = $1
AND (
$4::boolean = FALSE
OR m.publish_status = 'PUBLISHED'::TEXT
)
ORDER BY
m.sort_order ASC,
m.id ASC
@ -184,6 +231,7 @@ type ExamPrepListUnitModulesByUnitParams struct {
UnitID int64 `json:"unit_id"`
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
PublishedOnly bool `json:"published_only"`
}
type ExamPrepListUnitModulesByUnitRow struct {
@ -195,6 +243,7 @@ type ExamPrepListUnitModulesByUnitRow struct {
Thumbnail pgtype.Text `json:"thumbnail"`
Icon pgtype.Text `json:"icon"`
SortOrder int32 `json:"sort_order"`
PublishStatus string `json:"publish_status"`
LessonsCount int64 `json:"lessons_count"`
PracticesCount int64 `json:"practices_count"`
HasPractice bool `json:"has_practice"`
@ -203,7 +252,12 @@ type ExamPrepListUnitModulesByUnitRow struct {
}
func (q *Queries) ExamPrepListUnitModulesByUnit(ctx context.Context, arg ExamPrepListUnitModulesByUnitParams) ([]ExamPrepListUnitModulesByUnitRow, error) {
rows, err := q.db.Query(ctx, ExamPrepListUnitModulesByUnit, arg.UnitID, arg.Limit, arg.Offset)
rows, err := q.db.Query(ctx, ExamPrepListUnitModulesByUnit,
arg.UnitID,
arg.Limit,
arg.Offset,
arg.PublishedOnly,
)
if err != nil {
return nil, err
}
@ -220,6 +274,7 @@ func (q *Queries) ExamPrepListUnitModulesByUnit(ctx context.Context, arg ExamPre
&i.Thumbnail,
&i.Icon,
&i.SortOrder,
&i.PublishStatus,
&i.LessonsCount,
&i.PracticesCount,
&i.HasPractice,
@ -244,10 +299,11 @@ SET
thumbnail = coalesce($3::text, thumbnail),
icon = coalesce($4::text, icon),
sort_order = coalesce($5::int, sort_order),
publish_status = coalesce($6::varchar, publish_status),
updated_at = CURRENT_TIMESTAMP
WHERE id = $6
WHERE id = $7
RETURNING
id, unit_id, name, description, thumbnail, icon, sort_order, created_at, updated_at
id, unit_id, name, description, thumbnail, icon, sort_order, created_at, updated_at, publish_status
`
type ExamPrepUpdateUnitModuleParams struct {
@ -256,6 +312,7 @@ type ExamPrepUpdateUnitModuleParams struct {
Thumbnail pgtype.Text `json:"thumbnail"`
Icon pgtype.Text `json:"icon"`
SortOrder pgtype.Int4 `json:"sort_order"`
PublishStatus pgtype.Text `json:"publish_status"`
ID int64 `json:"id"`
}
@ -266,6 +323,7 @@ func (q *Queries) ExamPrepUpdateUnitModule(ctx context.Context, arg ExamPrepUpda
arg.Thumbnail,
arg.Icon,
arg.SortOrder,
arg.PublishStatus,
arg.ID,
)
var i ExamPrepUnitModule
@ -279,6 +337,7 @@ func (q *Queries) ExamPrepUpdateUnitModule(ctx context.Context, arg ExamPrepUpda
&i.SortOrder,
&i.CreatedAt,
&i.UpdatedAt,
&i.PublishStatus,
)
return i, err
}

View File

@ -12,7 +12,7 @@ import (
)
const ExamPrepCreateUnit = `-- name: ExamPrepCreateUnit :one
INSERT INTO exam_prep.units (catalog_course_id, name, description, thumbnail, sort_order)
INSERT INTO exam_prep.units (catalog_course_id, name, description, thumbnail, sort_order, publish_status)
SELECT
$1,
$2,
@ -24,9 +24,10 @@ SELECT
max(u.sort_order)
FROM exam_prep.units u
WHERE
u.catalog_course_id = $1), 0) + 1)
u.catalog_course_id = $1), 0) + 1),
$6
RETURNING
id, catalog_course_id, name, description, thumbnail, sort_order, created_at, updated_at
id, catalog_course_id, name, description, thumbnail, sort_order, created_at, updated_at, publish_status
`
type ExamPrepCreateUnitParams struct {
@ -35,6 +36,7 @@ type ExamPrepCreateUnitParams struct {
Description pgtype.Text `json:"description"`
Thumbnail pgtype.Text `json:"thumbnail"`
SortOrder pgtype.Int4 `json:"sort_order"`
PublishStatus string `json:"publish_status"`
}
func (q *Queries) ExamPrepCreateUnit(ctx context.Context, arg ExamPrepCreateUnitParams) (ExamPrepUnit, error) {
@ -44,6 +46,7 @@ func (q *Queries) ExamPrepCreateUnit(ctx context.Context, arg ExamPrepCreateUnit
arg.Description,
arg.Thumbnail,
arg.SortOrder,
arg.PublishStatus,
)
var i ExamPrepUnit
err := row.Scan(
@ -55,6 +58,7 @@ func (q *Queries) ExamPrepCreateUnit(ctx context.Context, arg ExamPrepCreateUnit
&i.SortOrder,
&i.CreatedAt,
&i.UpdatedAt,
&i.PublishStatus,
)
return i, err
}
@ -71,13 +75,16 @@ func (q *Queries) ExamPrepDeleteUnit(ctx context.Context, id int64) error {
const ExamPrepGetUnitByID = `-- name: ExamPrepGetUnitByID :one
SELECT
u.id, u.catalog_course_id, u.name, u.description, u.thumbnail, u.sort_order, u.created_at, u.updated_at,
u.id, u.catalog_course_id, u.name, u.description, u.thumbnail, u.sort_order, u.created_at, u.updated_at, u.publish_status,
EXISTS (
SELECT 1
FROM exam_prep.lesson_practices p
INNER JOIN exam_prep.unit_module_lessons l ON l.id = p.unit_module_lesson_id
INNER JOIN exam_prep.unit_modules m ON m.id = l.unit_module_id
WHERE m.unit_id = u.id
AND p.publish_status = 'PUBLISHED'
AND l.publish_status = 'PUBLISHED'
AND m.publish_status = 'PUBLISHED'
) AS has_practice
FROM exam_prep.units u
WHERE u.id = $1
@ -92,6 +99,7 @@ type ExamPrepGetUnitByIDRow struct {
SortOrder int32 `json:"sort_order"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
PublishStatus string `json:"publish_status"`
HasPractice bool `json:"has_practice"`
}
@ -107,11 +115,44 @@ func (q *Queries) ExamPrepGetUnitByID(ctx context.Context, id int64) (ExamPrepGe
&i.SortOrder,
&i.CreatedAt,
&i.UpdatedAt,
&i.PublishStatus,
&i.HasPractice,
)
return i, err
}
const ExamPrepListPublishedUnitIDsByCatalogCourse = `-- name: ExamPrepListPublishedUnitIDsByCatalogCourse :many
SELECT
u.id
FROM exam_prep.units u
WHERE
u.catalog_course_id = $1
AND u.publish_status = 'PUBLISHED'
ORDER BY
u.id
`
// Published units only, for learner-facing progress rollups.
func (q *Queries) ExamPrepListPublishedUnitIDsByCatalogCourse(ctx context.Context, catalogCourseID int64) ([]int64, error) {
rows, err := q.db.Query(ctx, ExamPrepListPublishedUnitIDsByCatalogCourse, catalogCourseID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []int64
for rows.Next() {
var id int64
if err := rows.Scan(&id); err != nil {
return nil, err
}
items = append(items, id)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const ExamPrepListUnitIDsByCatalogCourse = `-- name: ExamPrepListUnitIDsByCatalogCourse :many
SELECT
u.id
@ -151,8 +192,11 @@ WITH unit_counts AS (
COUNT(DISTINCT p.id)::BIGINT AS practices_count
FROM exam_prep.units u
LEFT JOIN exam_prep.unit_modules m ON m.unit_id = u.id
AND m.publish_status = 'PUBLISHED'
LEFT JOIN exam_prep.unit_module_lessons l ON l.unit_module_id = m.id
AND l.publish_status = 'PUBLISHED'
LEFT JOIN exam_prep.lesson_practices p ON p.unit_module_lesson_id = l.id
AND p.publish_status = 'PUBLISHED'
GROUP BY u.id
)
SELECT
@ -163,6 +207,7 @@ SELECT
u.description,
u.thumbnail,
u.sort_order,
u.publish_status,
COALESCE(uc.modules_count, 0)::BIGINT AS modules_count,
COALESCE(uc.lessons_count, 0)::BIGINT AS lessons_count,
COALESCE(uc.practices_count, 0)::BIGINT AS practices_count,
@ -173,6 +218,10 @@ FROM exam_prep.units u
LEFT JOIN unit_counts uc ON uc.unit_id = u.id
WHERE
u.catalog_course_id = $1
AND (
$4::boolean = FALSE
OR u.publish_status = 'PUBLISHED'::TEXT
)
ORDER BY
u.sort_order ASC,
u.id ASC
@ -184,6 +233,7 @@ type ExamPrepListUnitsByCatalogCourseParams struct {
CatalogCourseID int64 `json:"catalog_course_id"`
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
PublishedOnly bool `json:"published_only"`
}
type ExamPrepListUnitsByCatalogCourseRow struct {
@ -194,6 +244,7 @@ type ExamPrepListUnitsByCatalogCourseRow struct {
Description pgtype.Text `json:"description"`
Thumbnail pgtype.Text `json:"thumbnail"`
SortOrder int32 `json:"sort_order"`
PublishStatus string `json:"publish_status"`
ModulesCount int64 `json:"modules_count"`
LessonsCount int64 `json:"lessons_count"`
PracticesCount int64 `json:"practices_count"`
@ -203,7 +254,12 @@ type ExamPrepListUnitsByCatalogCourseRow struct {
}
func (q *Queries) ExamPrepListUnitsByCatalogCourse(ctx context.Context, arg ExamPrepListUnitsByCatalogCourseParams) ([]ExamPrepListUnitsByCatalogCourseRow, error) {
rows, err := q.db.Query(ctx, ExamPrepListUnitsByCatalogCourse, arg.CatalogCourseID, arg.Limit, arg.Offset)
rows, err := q.db.Query(ctx, ExamPrepListUnitsByCatalogCourse,
arg.CatalogCourseID,
arg.Limit,
arg.Offset,
arg.PublishedOnly,
)
if err != nil {
return nil, err
}
@ -219,6 +275,7 @@ func (q *Queries) ExamPrepListUnitsByCatalogCourse(ctx context.Context, arg Exam
&i.Description,
&i.Thumbnail,
&i.SortOrder,
&i.PublishStatus,
&i.ModulesCount,
&i.LessonsCount,
&i.PracticesCount,
@ -243,10 +300,11 @@ SET
description = coalesce($2::text, description),
thumbnail = coalesce($3::text, thumbnail),
sort_order = coalesce($4::int, sort_order),
publish_status = coalesce($5::varchar, publish_status),
updated_at = CURRENT_TIMESTAMP
WHERE id = $5
WHERE id = $6
RETURNING
id, catalog_course_id, name, description, thumbnail, sort_order, created_at, updated_at
id, catalog_course_id, name, description, thumbnail, sort_order, created_at, updated_at, publish_status
`
type ExamPrepUpdateUnitParams struct {
@ -254,6 +312,7 @@ type ExamPrepUpdateUnitParams struct {
Description pgtype.Text `json:"description"`
Thumbnail pgtype.Text `json:"thumbnail"`
SortOrder pgtype.Int4 `json:"sort_order"`
PublishStatus pgtype.Text `json:"publish_status"`
ID int64 `json:"id"`
}
@ -263,6 +322,7 @@ func (q *Queries) ExamPrepUpdateUnit(ctx context.Context, arg ExamPrepUpdateUnit
arg.Description,
arg.Thumbnail,
arg.SortOrder,
arg.PublishStatus,
arg.ID,
)
var i ExamPrepUnit
@ -275,6 +335,7 @@ func (q *Queries) ExamPrepUpdateUnit(ctx context.Context, arg ExamPrepUpdateUnit
&i.SortOrder,
&i.CreatedAt,
&i.UpdatedAt,
&i.PublishStatus,
)
return i, err
}

View File

@ -12,7 +12,7 @@ import (
)
const CreateCourse = `-- name: CreateCourse :one
INSERT INTO courses (program_id, name, description, thumbnail, sort_order)
INSERT INTO courses (program_id, name, description, thumbnail, sort_order, publish_status)
SELECT
$1,
$2,
@ -24,9 +24,10 @@ SELECT
max(c.sort_order)
FROM courses c
WHERE
c.program_id = $1), 0) + 1)
c.program_id = $1), 0) + 1),
$6
RETURNING
id, program_id, name, description, thumbnail, created_at, updated_at, sort_order
id, program_id, name, description, thumbnail, created_at, updated_at, sort_order, publish_status
`
type CreateCourseParams struct {
@ -35,6 +36,7 @@ type CreateCourseParams struct {
Description pgtype.Text `json:"description"`
Thumbnail pgtype.Text `json:"thumbnail"`
SortOrder pgtype.Int4 `json:"sort_order"`
PublishStatus string `json:"publish_status"`
}
func (q *Queries) CreateCourse(ctx context.Context, arg CreateCourseParams) (Course, error) {
@ -44,6 +46,7 @@ func (q *Queries) CreateCourse(ctx context.Context, arg CreateCourseParams) (Cou
arg.Description,
arg.Thumbnail,
arg.SortOrder,
arg.PublishStatus,
)
var i Course
err := row.Scan(
@ -55,6 +58,7 @@ func (q *Queries) CreateCourse(ctx context.Context, arg CreateCourseParams) (Cou
&i.CreatedAt,
&i.UpdatedAt,
&i.SortOrder,
&i.PublishStatus,
)
return i, err
}
@ -71,7 +75,7 @@ func (q *Queries) DeleteCourse(ctx context.Context, id int64) error {
const GetCourseByID = `-- name: GetCourseByID :one
SELECT
c.id, c.program_id, c.name, c.description, c.thumbnail, c.created_at, c.updated_at, c.sort_order,
c.id, c.program_id, c.name, c.description, c.thumbnail, c.created_at, c.updated_at, c.sort_order, c.publish_status,
EXISTS (
SELECT 1
FROM lms_practices p
@ -94,6 +98,7 @@ type GetCourseByIDRow struct {
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
SortOrder int32 `json:"sort_order"`
PublishStatus string `json:"publish_status"`
HasPractice bool `json:"has_practice"`
}
@ -109,6 +114,7 @@ func (q *Queries) GetCourseByID(ctx context.Context, id int64) (GetCourseByIDRow
&i.CreatedAt,
&i.UpdatedAt,
&i.SortOrder,
&i.PublishStatus,
&i.HasPractice,
)
return i, err
@ -154,6 +160,7 @@ SELECT
c.description,
c.thumbnail,
c.sort_order,
c.publish_status,
c.created_at,
c.updated_at,
(
@ -162,7 +169,8 @@ SELECT
FROM
modules m
WHERE
m.course_id = c.id) AS module_count,
m.course_id = c.id
AND m.publish_status = 'PUBLISHED') AS module_count,
(
SELECT
COUNT(*)::bigint
@ -196,6 +204,10 @@ FROM
courses c
WHERE
c.program_id = $1
AND (
$4::boolean = FALSE
OR c.publish_status = 'PUBLISHED'::TEXT
)
ORDER BY
c.sort_order ASC,
c.id ASC
@ -206,6 +218,7 @@ type ListCoursesByProgramIDParams struct {
ProgramID int64 `json:"program_id"`
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
PublishedOnly bool `json:"published_only"`
}
type ListCoursesByProgramIDRow struct {
@ -216,6 +229,7 @@ type ListCoursesByProgramIDRow struct {
Description pgtype.Text `json:"description"`
Thumbnail pgtype.Text `json:"thumbnail"`
SortOrder int32 `json:"sort_order"`
PublishStatus string `json:"publish_status"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
ModuleCount int64 `json:"module_count"`
@ -225,7 +239,12 @@ type ListCoursesByProgramIDRow struct {
}
func (q *Queries) ListCoursesByProgramID(ctx context.Context, arg ListCoursesByProgramIDParams) ([]ListCoursesByProgramIDRow, error) {
rows, err := q.db.Query(ctx, ListCoursesByProgramID, arg.ProgramID, arg.Limit, arg.Offset)
rows, err := q.db.Query(ctx, ListCoursesByProgramID,
arg.ProgramID,
arg.Limit,
arg.Offset,
arg.PublishedOnly,
)
if err != nil {
return nil, err
}
@ -241,6 +260,7 @@ func (q *Queries) ListCoursesByProgramID(ctx context.Context, arg ListCoursesByP
&i.Description,
&i.Thumbnail,
&i.SortOrder,
&i.PublishStatus,
&i.CreatedAt,
&i.UpdatedAt,
&i.ModuleCount,
@ -258,6 +278,39 @@ func (q *Queries) ListCoursesByProgramID(ctx context.Context, arg ListCoursesByP
return items, nil
}
const ListPublishedCourseIDsByProgram = `-- name: ListPublishedCourseIDsByProgram :many
SELECT
c.id
FROM
courses AS c
WHERE
c.program_id = $1
AND c.publish_status = 'PUBLISHED'
ORDER BY
c.id
`
// Published courses only, for learner-facing progress rollups.
func (q *Queries) ListPublishedCourseIDsByProgram(ctx context.Context, programID int64) ([]int64, error) {
rows, err := q.db.Query(ctx, ListPublishedCourseIDsByProgram, programID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []int64
for rows.Next() {
var id int64
if err := rows.Scan(&id); err != nil {
return nil, err
}
items = append(items, id)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const UpdateCourse = `-- name: UpdateCourse :one
UPDATE courses
SET
@ -265,11 +318,12 @@ SET
description = COALESCE($2::text, description),
thumbnail = COALESCE($3::text, thumbnail),
sort_order = coalesce($4::int, sort_order),
publish_status = COALESCE($5::varchar, publish_status),
updated_at = CURRENT_TIMESTAMP
WHERE
id = $5
id = $6
RETURNING
id, program_id, name, description, thumbnail, created_at, updated_at, sort_order
id, program_id, name, description, thumbnail, created_at, updated_at, sort_order, publish_status
`
type UpdateCourseParams struct {
@ -277,6 +331,7 @@ type UpdateCourseParams struct {
Description pgtype.Text `json:"description"`
Thumbnail pgtype.Text `json:"thumbnail"`
SortOrder pgtype.Int4 `json:"sort_order"`
PublishStatus pgtype.Text `json:"publish_status"`
ID int64 `json:"id"`
}
@ -286,6 +341,7 @@ func (q *Queries) UpdateCourse(ctx context.Context, arg UpdateCourseParams) (Cou
arg.Description,
arg.Thumbnail,
arg.SortOrder,
arg.PublishStatus,
arg.ID,
)
var i Course
@ -298,6 +354,7 @@ func (q *Queries) UpdateCourse(ctx context.Context, arg UpdateCourseParams) (Cou
&i.CreatedAt,
&i.UpdatedAt,
&i.SortOrder,
&i.PublishStatus,
)
return i, err
}

View File

@ -12,7 +12,7 @@ import (
)
const CreateModule = `-- name: CreateModule :one
INSERT INTO modules (program_id, course_id, name, description, icon, sort_order)
INSERT INTO modules (program_id, course_id, name, description, icon, sort_order, publish_status)
SELECT
$1,
$2,
@ -25,9 +25,10 @@ SELECT
max(m.sort_order)
FROM modules m
WHERE
m.course_id = $2), 0) + 1)
m.course_id = $2), 0) + 1),
$7
RETURNING
id, program_id, course_id, name, description, icon, created_at, updated_at, sort_order
id, program_id, course_id, name, description, icon, created_at, updated_at, sort_order, publish_status
`
type CreateModuleParams struct {
@ -37,6 +38,7 @@ type CreateModuleParams struct {
Description pgtype.Text `json:"description"`
Icon pgtype.Text `json:"icon"`
SortOrder pgtype.Int4 `json:"sort_order"`
PublishStatus string `json:"publish_status"`
}
func (q *Queries) CreateModule(ctx context.Context, arg CreateModuleParams) (Module, error) {
@ -47,6 +49,7 @@ func (q *Queries) CreateModule(ctx context.Context, arg CreateModuleParams) (Mod
arg.Description,
arg.Icon,
arg.SortOrder,
arg.PublishStatus,
)
var i Module
err := row.Scan(
@ -59,6 +62,7 @@ func (q *Queries) CreateModule(ctx context.Context, arg CreateModuleParams) (Mod
&i.CreatedAt,
&i.UpdatedAt,
&i.SortOrder,
&i.PublishStatus,
)
return i, err
}
@ -75,7 +79,7 @@ func (q *Queries) DeleteModule(ctx context.Context, id int64) error {
const GetModuleByID = `-- name: GetModuleByID :one
SELECT
m.id, m.program_id, m.course_id, m.name, m.description, m.icon, m.created_at, m.updated_at, m.sort_order,
m.id, m.program_id, m.course_id, m.name, m.description, m.icon, m.created_at, m.updated_at, m.sort_order, m.publish_status,
EXISTS (
SELECT 1
FROM lms_practices p
@ -98,6 +102,7 @@ type GetModuleByIDRow struct {
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
SortOrder int32 `json:"sort_order"`
PublishStatus string `json:"publish_status"`
HasPractice bool `json:"has_practice"`
}
@ -114,6 +119,7 @@ func (q *Queries) GetModuleByID(ctx context.Context, id int64) (GetModuleByIDRow
&i.CreatedAt,
&i.UpdatedAt,
&i.SortOrder,
&i.PublishStatus,
&i.HasPractice,
)
return i, err
@ -160,6 +166,7 @@ SELECT
m.description,
m.icon,
m.sort_order,
m.publish_status,
m.created_at,
m.updated_at,
EXISTS (
@ -174,6 +181,10 @@ FROM
WHERE
m.program_id = $1
AND m.course_id = $2
AND (
$5::boolean = FALSE
OR m.publish_status = 'PUBLISHED'::TEXT
)
ORDER BY
m.sort_order ASC,
m.id ASC
@ -186,6 +197,7 @@ type ListModulesByProgramAndCourseParams struct {
CourseID int64 `json:"course_id"`
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
PublishedOnly bool `json:"published_only"`
}
type ListModulesByProgramAndCourseRow struct {
@ -197,6 +209,7 @@ type ListModulesByProgramAndCourseRow struct {
Description pgtype.Text `json:"description"`
Icon pgtype.Text `json:"icon"`
SortOrder int32 `json:"sort_order"`
PublishStatus string `json:"publish_status"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
HasPractice bool `json:"has_practice"`
@ -208,6 +221,7 @@ func (q *Queries) ListModulesByProgramAndCourse(ctx context.Context, arg ListMod
arg.CourseID,
arg.Limit,
arg.Offset,
arg.PublishedOnly,
)
if err != nil {
return nil, err
@ -225,6 +239,7 @@ func (q *Queries) ListModulesByProgramAndCourse(ctx context.Context, arg ListMod
&i.Description,
&i.Icon,
&i.SortOrder,
&i.PublishStatus,
&i.CreatedAt,
&i.UpdatedAt,
&i.HasPractice,
@ -239,6 +254,39 @@ func (q *Queries) ListModulesByProgramAndCourse(ctx context.Context, arg ListMod
return items, nil
}
const ListPublishedModuleIDsByCourse = `-- name: ListPublishedModuleIDsByCourse :many
SELECT
m.id
FROM
modules AS m
WHERE
m.course_id = $1
AND m.publish_status = 'PUBLISHED'
ORDER BY
m.id
`
// Published modules only, for learner-facing progress rollups.
func (q *Queries) ListPublishedModuleIDsByCourse(ctx context.Context, courseID int64) ([]int64, error) {
rows, err := q.db.Query(ctx, ListPublishedModuleIDsByCourse, courseID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []int64
for rows.Next() {
var id int64
if err := rows.Scan(&id); err != nil {
return nil, err
}
items = append(items, id)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const UpdateModule = `-- name: UpdateModule :one
UPDATE modules
SET
@ -246,11 +294,12 @@ SET
description = COALESCE($2::text, description),
icon = COALESCE($3::text, icon),
sort_order = coalesce($4::int, sort_order),
publish_status = COALESCE($5::varchar, publish_status),
updated_at = CURRENT_TIMESTAMP
WHERE
id = $5
id = $6
RETURNING
id, program_id, course_id, name, description, icon, created_at, updated_at, sort_order
id, program_id, course_id, name, description, icon, created_at, updated_at, sort_order, publish_status
`
type UpdateModuleParams struct {
@ -258,6 +307,7 @@ type UpdateModuleParams struct {
Description pgtype.Text `json:"description"`
Icon pgtype.Text `json:"icon"`
SortOrder pgtype.Int4 `json:"sort_order"`
PublishStatus pgtype.Text `json:"publish_status"`
ID int64 `json:"id"`
}
@ -267,6 +317,7 @@ func (q *Queries) UpdateModule(ctx context.Context, arg UpdateModuleParams) (Mod
arg.Description,
arg.Icon,
arg.SortOrder,
arg.PublishStatus,
arg.ID,
)
var i Module
@ -280,6 +331,7 @@ func (q *Queries) UpdateModule(ctx context.Context, arg UpdateModuleParams) (Mod
&i.CreatedAt,
&i.UpdatedAt,
&i.SortOrder,
&i.PublishStatus,
)
return i, err
}

View File

@ -18,6 +18,7 @@ FROM
courses
WHERE
program_id = $1
AND publish_status = 'PUBLISHED'
`
func (q *Queries) CountCoursesInProgram(ctx context.Context, programID int64) (int32, error) {
@ -90,6 +91,7 @@ FROM
modules
WHERE
course_id = $1
AND publish_status = 'PUBLISHED'
`
func (q *Queries) CountModulesInCourse(ctx context.Context, courseID int64) (int32, error) {
@ -156,7 +158,8 @@ WHERE
FROM
modules
WHERE
course_id = $1)
course_id = $1
AND publish_status = 'PUBLISHED')
OR lp.lesson_id IN (
SELECT
l.id
@ -164,7 +167,9 @@ WHERE
lessons l
INNER JOIN modules m ON m.id = l.module_id
WHERE
m.course_id = $1))
m.course_id = $1
AND l.publish_status = 'PUBLISHED'
AND m.publish_status = 'PUBLISHED'))
AND qs.set_type = 'PRACTICE'
AND qs.status = 'PUBLISHED'
AND lp.publish_status = 'PUBLISHED'
@ -212,13 +217,14 @@ WHERE
FROM
lessons
WHERE
module_id = $1))
module_id = $1
AND publish_status = 'PUBLISHED'))
AND qs.set_type = 'PRACTICE'
AND qs.status = 'PUBLISHED'
AND lp.publish_status = 'PUBLISHED'
`
// Published practices in a module (direct module practices and practices on lessons in the module).
// Published practices in a module (direct module practices and practices on published lessons in the module).
func (q *Queries) CountPublishedPracticesInModule(ctx context.Context, moduleID pgtype.Int8) (int32, error) {
row := q.db.QueryRow(ctx, CountPublishedPracticesInModule, moduleID)
var n int32
@ -240,7 +246,8 @@ WHERE
FROM
courses c
WHERE
c.program_id = $1)
c.program_id = $1
AND c.publish_status = 'PUBLISHED')
OR lp.module_id IN (
SELECT
m.id
@ -248,7 +255,9 @@ WHERE
modules m
INNER JOIN courses c ON c.id = m.course_id
WHERE
c.program_id = $1)
c.program_id = $1
AND m.publish_status = 'PUBLISHED'
AND c.publish_status = 'PUBLISHED')
OR lp.lesson_id IN (
SELECT
l.id
@ -257,7 +266,10 @@ WHERE
INNER JOIN modules m ON m.id = l.module_id
INNER JOIN courses c ON c.id = m.course_id
WHERE
c.program_id = $1))
c.program_id = $1
AND l.publish_status = 'PUBLISHED'
AND m.publish_status = 'PUBLISHED'
AND c.publish_status = 'PUBLISHED'))
AND qs.set_type = 'PRACTICE'
AND qs.status = 'PUBLISHED'
AND lp.publish_status = 'PUBLISHED'
@ -279,6 +291,7 @@ FROM
WHERE
c.program_id = $1
AND ucp.user_id = $2
AND c.publish_status = 'PUBLISHED'
`
type CountUserCompletedCoursesInProgramParams struct {
@ -377,6 +390,7 @@ FROM
WHERE
m.course_id = $1
AND ump.user_id = $2
AND m.publish_status = 'PUBLISHED'
`
type CountUserCompletedModulesInCourseParams struct {
@ -463,7 +477,8 @@ WHERE
FROM
modules
WHERE
course_id = $1)
course_id = $1
AND publish_status = 'PUBLISHED')
OR lp.lesson_id IN (
SELECT
l.id
@ -471,7 +486,9 @@ WHERE
lessons l
INNER JOIN modules m ON m.id = l.module_id
WHERE
m.course_id = $1))
m.course_id = $1
AND l.publish_status = 'PUBLISHED'
AND m.publish_status = 'PUBLISHED'))
AND upp.user_id = $2
AND upp.completed_at IS NOT NULL
AND qs.set_type = 'PRACTICE'
@ -535,7 +552,8 @@ WHERE
FROM
lessons
WHERE
module_id = $1))
module_id = $1
AND publish_status = 'PUBLISHED'))
AND upp.user_id = $2
AND upp.completed_at IS NOT NULL
AND qs.set_type = 'PRACTICE'
@ -570,7 +588,8 @@ WHERE
FROM
courses c
WHERE
c.program_id = $1)
c.program_id = $1
AND c.publish_status = 'PUBLISHED')
OR lp.module_id IN (
SELECT
m.id
@ -578,7 +597,9 @@ WHERE
modules m
INNER JOIN courses c ON c.id = m.course_id
WHERE
c.program_id = $1)
c.program_id = $1
AND m.publish_status = 'PUBLISHED'
AND c.publish_status = 'PUBLISHED')
OR lp.lesson_id IN (
SELECT
l.id
@ -587,7 +608,10 @@ WHERE
INNER JOIN modules m ON m.id = l.module_id
INNER JOIN courses c ON c.id = m.course_id
WHERE
c.program_id = $1))
c.program_id = $1
AND l.publish_status = 'PUBLISHED'
AND m.publish_status = 'PUBLISHED'
AND c.publish_status = 'PUBLISHED'))
AND upp.user_id = $2
AND upp.completed_at IS NOT NULL
AND qs.set_type = 'PRACTICE'
@ -643,10 +667,11 @@ func (q *Queries) GetPracticeScopeByQuestionSetID(ctx context.Context, questionS
const GetPreviousCourseInProgram = `-- name: GetPreviousCourseInProgram :one
SELECT
c2.id, c2.program_id, c2.name, c2.description, c2.thumbnail, c2.created_at, c2.updated_at, c2.sort_order
c2.id, c2.program_id, c2.name, c2.description, c2.thumbnail, c2.created_at, c2.updated_at, c2.sort_order, c2.publish_status
FROM
courses AS c1
INNER JOIN courses AS c2 ON c2.program_id = c1.program_id
AND c2.publish_status = 'PUBLISHED'
AND (
c2.sort_order < c1.sort_order
OR (
@ -674,6 +699,7 @@ func (q *Queries) GetPreviousCourseInProgram(ctx context.Context, id int64) (Cou
&i.CreatedAt,
&i.UpdatedAt,
&i.SortOrder,
&i.PublishStatus,
)
return i, err
}
@ -721,10 +747,11 @@ func (q *Queries) GetPreviousLessonInModule(ctx context.Context, id int64) (Less
const GetPreviousModuleInCourse = `-- name: GetPreviousModuleInCourse :one
SELECT
m2.id, m2.program_id, m2.course_id, m2.name, m2.description, m2.icon, m2.created_at, m2.updated_at, m2.sort_order
m2.id, m2.program_id, m2.course_id, m2.name, m2.description, m2.icon, m2.created_at, m2.updated_at, m2.sort_order, m2.publish_status
FROM
modules AS m1
INNER JOIN modules AS m2 ON m2.course_id = m1.course_id
AND m2.publish_status = 'PUBLISHED'
AND (
m2.sort_order < m1.sort_order
OR (
@ -753,16 +780,18 @@ func (q *Queries) GetPreviousModuleInCourse(ctx context.Context, id int64) (Modu
&i.CreatedAt,
&i.UpdatedAt,
&i.SortOrder,
&i.PublishStatus,
)
return i, err
}
const GetPreviousProgram = `-- name: GetPreviousProgram :one
SELECT
p2.id, p2.name, p2.description, p2.thumbnail, p2.created_at, p2.updated_at, p2.sort_order, p2.category
p2.id, p2.name, p2.description, p2.thumbnail, p2.created_at, p2.updated_at, p2.sort_order, p2.category, p2.publish_status
FROM
programs AS p1
INNER JOIN programs AS p2 ON p2.category = p1.category
AND p2.publish_status = 'PUBLISHED'
AND (
p2.sort_order < p1.sort_order
OR (
@ -791,6 +820,7 @@ func (q *Queries) GetPreviousProgram(ctx context.Context, id int64) (Program, er
&i.UpdatedAt,
&i.SortOrder,
&i.Category,
&i.PublishStatus,
)
return i, err
}

View File

@ -31,6 +31,7 @@ type Course struct {
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
SortOrder int32 `json:"sort_order"`
PublishStatus string `json:"publish_status"`
}
type Device struct {
@ -66,6 +67,7 @@ type ExamPrepCatalogCourse struct {
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
Category string `json:"category"`
PublishStatus string `json:"publish_status"`
}
type ExamPrepLessonPractice struct {
@ -91,6 +93,7 @@ type ExamPrepUnit struct {
SortOrder int32 `json:"sort_order"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
PublishStatus string `json:"publish_status"`
}
type ExamPrepUnitModule struct {
@ -103,6 +106,7 @@ type ExamPrepUnitModule struct {
SortOrder int32 `json:"sort_order"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
PublishStatus string `json:"publish_status"`
}
type ExamPrepUnitModuleLesson struct {
@ -115,6 +119,7 @@ type ExamPrepUnitModuleLesson struct {
SortOrder int32 `json:"sort_order"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
PublishStatus string `json:"publish_status"`
}
type Faq struct {
@ -239,6 +244,7 @@ type Module struct {
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
SortOrder int32 `json:"sort_order"`
PublishStatus string `json:"publish_status"`
}
type ModuleToSubCourse struct {
@ -311,6 +317,7 @@ type Program struct {
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
SortOrder int32 `json:"sort_order"`
Category string `json:"category"`
PublishStatus string `json:"publish_status"`
}
type Question struct {

View File

@ -12,7 +12,7 @@ import (
)
const CreateProgram = `-- name: CreateProgram :one
INSERT INTO programs (name, description, category, thumbnail, sort_order)
INSERT INTO programs (name, description, category, thumbnail, sort_order, publish_status)
SELECT
$1,
$2,
@ -21,9 +21,10 @@ SELECT
COALESCE($5::int, COALESCE((
SELECT
max(p.sort_order)
FROM programs AS p), 0) + 1)
FROM programs AS p), 0) + 1),
$6
RETURNING
id, name, description, thumbnail, created_at, updated_at, sort_order, category
id, name, description, thumbnail, created_at, updated_at, sort_order, category, publish_status
`
type CreateProgramParams struct {
@ -32,6 +33,7 @@ type CreateProgramParams struct {
Category string `json:"category"`
Thumbnail pgtype.Text `json:"thumbnail"`
SortOrder pgtype.Int4 `json:"sort_order"`
PublishStatus string `json:"publish_status"`
}
func (q *Queries) CreateProgram(ctx context.Context, arg CreateProgramParams) (Program, error) {
@ -41,6 +43,7 @@ func (q *Queries) CreateProgram(ctx context.Context, arg CreateProgramParams) (P
arg.Category,
arg.Thumbnail,
arg.SortOrder,
arg.PublishStatus,
)
var i Program
err := row.Scan(
@ -52,6 +55,7 @@ func (q *Queries) CreateProgram(ctx context.Context, arg CreateProgramParams) (P
&i.UpdatedAt,
&i.SortOrder,
&i.Category,
&i.PublishStatus,
)
return i, err
}
@ -67,7 +71,7 @@ func (q *Queries) DeleteProgram(ctx context.Context, id int64) error {
}
const GetProgramByID = `-- name: GetProgramByID :one
SELECT id, name, description, thumbnail, created_at, updated_at, sort_order, category
SELECT id, name, description, thumbnail, created_at, updated_at, sort_order, category, publish_status
FROM programs
WHERE id = $1
`
@ -84,6 +88,7 @@ func (q *Queries) GetProgramByID(ctx context.Context, id int64) (Program, error)
&i.UpdatedAt,
&i.SortOrder,
&i.Category,
&i.PublishStatus,
)
return i, err
}
@ -126,9 +131,14 @@ SELECT
p.category,
p.thumbnail,
p.sort_order,
p.publish_status,
p.created_at,
p.updated_at
FROM programs p
WHERE (
$3::boolean = FALSE
OR p.publish_status = 'PUBLISHED'::TEXT
)
ORDER BY p.sort_order ASC, p.id ASC
LIMIT $1 OFFSET $2
`
@ -136,6 +146,7 @@ LIMIT $1 OFFSET $2
type ListProgramsParams struct {
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
PublishedOnly bool `json:"published_only"`
}
type ListProgramsRow struct {
@ -146,12 +157,13 @@ type ListProgramsRow struct {
Category string `json:"category"`
Thumbnail pgtype.Text `json:"thumbnail"`
SortOrder int32 `json:"sort_order"`
PublishStatus string `json:"publish_status"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
func (q *Queries) ListPrograms(ctx context.Context, arg ListProgramsParams) ([]ListProgramsRow, error) {
rows, err := q.db.Query(ctx, ListPrograms, arg.Limit, arg.Offset)
rows, err := q.db.Query(ctx, ListPrograms, arg.Limit, arg.Offset, arg.PublishedOnly)
if err != nil {
return nil, err
}
@ -167,6 +179,7 @@ func (q *Queries) ListPrograms(ctx context.Context, arg ListProgramsParams) ([]L
&i.Category,
&i.Thumbnail,
&i.SortOrder,
&i.PublishStatus,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
@ -188,11 +201,12 @@ SET
category = COALESCE($3::varchar, category),
thumbnail = COALESCE($4::text, thumbnail),
sort_order = coalesce($5::int, sort_order),
publish_status = COALESCE($6::varchar, publish_status),
updated_at = CURRENT_TIMESTAMP
WHERE
id = $6
id = $7
RETURNING
id, name, description, thumbnail, created_at, updated_at, sort_order, category
id, name, description, thumbnail, created_at, updated_at, sort_order, category, publish_status
`
type UpdateProgramParams struct {
@ -201,6 +215,7 @@ type UpdateProgramParams struct {
Category pgtype.Text `json:"category"`
Thumbnail pgtype.Text `json:"thumbnail"`
SortOrder pgtype.Int4 `json:"sort_order"`
PublishStatus pgtype.Text `json:"publish_status"`
ID int64 `json:"id"`
}
@ -211,6 +226,7 @@ func (q *Queries) UpdateProgram(ctx context.Context, arg UpdateProgramParams) (P
arg.Category,
arg.Thumbnail,
arg.SortOrder,
arg.PublishStatus,
arg.ID,
)
var i Program
@ -223,6 +239,7 @@ func (q *Queries) UpdateProgram(ctx context.Context, arg UpdateProgramParams) (P
&i.UpdatedAt,
&i.SortOrder,
&i.Category,
&i.PublishStatus,
)
return i, err
}

View File

@ -0,0 +1,31 @@
package domain
import "strings"
// ContentPublishStatus controls learner visibility for LMS hierarchy entities (programs,
// courses, modules) and exam-prep hierarchy entities (catalog courses, units, unit modules,
// unit module lessons). Mirrors LessonPublishStatus / PracticePublishStatus.
type ContentPublishStatus string
const (
ContentPublishDraft ContentPublishStatus = "DRAFT"
ContentPublishPublished ContentPublishStatus = "PUBLISHED"
)
// ContentPublishStatusFromDB normalizes persisted values.
func ContentPublishStatusFromDB(raw string) ContentPublishStatus {
switch strings.TrimSpace(strings.ToUpper(raw)) {
case string(ContentPublishPublished):
return ContentPublishPublished
default:
return ContentPublishDraft
}
}
// ContentPublishStatusFromCreateInput resolves create body: omit → draft; explicit value validated separately.
func ContentPublishStatusFromCreateInput(raw *string) ContentPublishStatus {
if raw == nil || strings.TrimSpace(*raw) == "" {
return ContentPublishDraft
}
return ContentPublishStatusFromDB(*raw)
}

View File

@ -21,6 +21,7 @@ type Course struct {
Description *string `json:"description,omitempty"`
Thumbnail *string `json:"thumbnail,omitempty"`
SortOrder int `json:"sort_order"`
PublishStatus ContentPublishStatus `json:"publish_status"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at,omitempty"`
// Populated on list-by-program. Practice count: lms_practices rows with course_id = course only
@ -32,12 +33,19 @@ type Course struct {
Access *LMSEntityAccess `json:"access,omitempty"`
}
// VisibleToLearners is true when the course appears in subscriber/catalog LMS APIs.
func (c Course) VisibleToLearners() bool {
return c.PublishStatus == ContentPublishPublished
}
type CreateCourseInput struct {
Name string `json:"name" validate:"required"`
Description *string `json:"description,omitempty"`
Thumbnail *string `json:"thumbnail,omitempty"`
// SortOrder within the program when set; omit to append after current max within program_id (uniqueness is per-program).
SortOrder *int `json:"sort_order,omitempty" validate:"omitempty,min=0"`
// Omit or empty defaults to DRAFT; set PUBLISHED to make visible to learners immediately.
PublishStatus *string `json:"publish_status,omitempty" validate:"omitempty,oneof=DRAFT draft PUBLISHED published"`
}
type UpdateCourseInput struct {
@ -45,4 +53,5 @@ type UpdateCourseInput struct {
Description *string `json:"description,omitempty"`
Thumbnail *string `json:"thumbnail,omitempty"`
SortOrder *int `json:"sort_order,omitempty"`
PublishStatus *string `json:"publish_status,omitempty" validate:"omitempty,oneof=DRAFT draft PUBLISHED published"`
}

View File

@ -10,6 +10,7 @@ type ExamPrepCatalogCourse struct {
Category string `json:"category"`
Thumbnail *string `json:"thumbnail,omitempty"`
SortOrder int `json:"sort_order"`
PublishStatus ContentPublishStatus `json:"publish_status"`
UnitsCount *int64 `json:"units_count,omitempty"`
ModulesCount *int64 `json:"modules_count,omitempty"`
LessonsCount *int64 `json:"lessons_count,omitempty"`
@ -19,11 +20,18 @@ type ExamPrepCatalogCourse struct {
UpdatedAt *time.Time `json:"updated_at,omitempty"`
}
// VisibleToLearners is true when the catalog course appears in student/learner exam-prep APIs.
func (c ExamPrepCatalogCourse) VisibleToLearners() bool {
return c.PublishStatus == ContentPublishPublished
}
type CreateExamPrepCatalogCourseInput struct {
Name string `json:"name" validate:"required"`
Description *string `json:"description,omitempty"`
Category string `json:"category" validate:"required,oneof=IELTS DUOLINGO"`
Thumbnail *string `json:"thumbnail,omitempty"`
// Omit or empty defaults to DRAFT; set PUBLISHED to make visible to learners immediately.
PublishStatus *string `json:"publish_status,omitempty" validate:"omitempty,oneof=DRAFT draft PUBLISHED published"`
}
type UpdateExamPrepCatalogCourseInput struct {
@ -32,4 +40,5 @@ type UpdateExamPrepCatalogCourseInput struct {
Category *string `json:"category,omitempty" validate:"omitempty,oneof=IELTS DUOLINGO"`
Thumbnail *string `json:"thumbnail,omitempty"`
SortOrder *int `json:"sort_order,omitempty"`
PublishStatus *string `json:"publish_status,omitempty" validate:"omitempty,oneof=DRAFT draft PUBLISHED published"`
}

View File

@ -11,17 +11,25 @@ type ExamPrepLesson struct {
Thumbnail *string `json:"thumbnail,omitempty"`
Description *string `json:"description,omitempty"`
SortOrder int `json:"sort_order"`
PublishStatus ContentPublishStatus `json:"publish_status"`
HasPractice bool `json:"has_practice"`
Access *LMSEntityAccess `json:"access,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at,omitempty"`
}
// VisibleToLearners is true when the lesson appears in student/learner exam-prep APIs.
func (l ExamPrepLesson) VisibleToLearners() bool {
return l.PublishStatus == ContentPublishPublished
}
type CreateExamPrepLessonInput struct {
Title string `json:"title" validate:"required"`
VideoURL *string `json:"video_url,omitempty"`
Thumbnail *string `json:"thumbnail,omitempty"`
Description *string `json:"description,omitempty"`
// Omit or empty defaults to DRAFT; set PUBLISHED to make visible to learners immediately.
PublishStatus *string `json:"publish_status,omitempty" validate:"omitempty,oneof=DRAFT draft PUBLISHED published"`
}
type UpdateExamPrepLessonInput struct {
@ -30,4 +38,5 @@ type UpdateExamPrepLessonInput struct {
Thumbnail *string `json:"thumbnail,omitempty"`
Description *string `json:"description,omitempty"`
SortOrder *int `json:"sort_order,omitempty"`
PublishStatus *string `json:"publish_status,omitempty" validate:"omitempty,oneof=DRAFT draft PUBLISHED published"`
}

View File

@ -11,6 +11,7 @@ type ExamPrepModule struct {
Thumbnail *string `json:"thumbnail,omitempty"`
Icon *string `json:"icon,omitempty"`
SortOrder int `json:"sort_order"`
PublishStatus ContentPublishStatus `json:"publish_status"`
LessonsCount *int64 `json:"lessons_count,omitempty"`
PracticesCount *int64 `json:"practices_count,omitempty"`
HasPractice bool `json:"has_practice"`
@ -19,11 +20,18 @@ type ExamPrepModule struct {
UpdatedAt *time.Time `json:"updated_at,omitempty"`
}
// VisibleToLearners is true when the module appears in student/learner exam-prep APIs.
func (m ExamPrepModule) VisibleToLearners() bool {
return m.PublishStatus == ContentPublishPublished
}
type CreateExamPrepModuleInput struct {
Name string `json:"name" validate:"required"`
Description *string `json:"description,omitempty"`
Thumbnail *string `json:"thumbnail,omitempty"`
Icon *string `json:"icon,omitempty"`
// Omit or empty defaults to DRAFT; set PUBLISHED to make visible to learners immediately.
PublishStatus *string `json:"publish_status,omitempty" validate:"omitempty,oneof=DRAFT draft PUBLISHED published"`
}
type UpdateExamPrepModuleInput struct {
@ -32,4 +40,5 @@ type UpdateExamPrepModuleInput struct {
Thumbnail *string `json:"thumbnail,omitempty"`
Icon *string `json:"icon,omitempty"`
SortOrder *int `json:"sort_order,omitempty"`
PublishStatus *string `json:"publish_status,omitempty" validate:"omitempty,oneof=DRAFT draft PUBLISHED published"`
}

View File

@ -10,6 +10,7 @@ type ExamPrepUnit struct {
Description *string `json:"description,omitempty"`
Thumbnail *string `json:"thumbnail,omitempty"`
SortOrder int `json:"sort_order"`
PublishStatus ContentPublishStatus `json:"publish_status"`
ModulesCount *int64 `json:"modules_count,omitempty"`
LessonsCount *int64 `json:"lessons_count,omitempty"`
PracticesCount *int64 `json:"practices_count,omitempty"`
@ -19,12 +20,19 @@ type ExamPrepUnit struct {
UpdatedAt *time.Time `json:"updated_at,omitempty"`
}
// VisibleToLearners is true when the unit appears in student/learner exam-prep APIs.
func (u ExamPrepUnit) VisibleToLearners() bool {
return u.PublishStatus == ContentPublishPublished
}
type CreateExamPrepUnitInput struct {
Name string `json:"name" validate:"required"`
Description *string `json:"description,omitempty"`
Thumbnail *string `json:"thumbnail,omitempty"`
// SortOrder within the catalog course when set; omit to append after current max sort_order within catalog_course_id.
SortOrder *int `json:"sort_order,omitempty" validate:"omitempty,min=0"`
// Omit or empty defaults to DRAFT; set PUBLISHED to make visible to learners immediately.
PublishStatus *string `json:"publish_status,omitempty" validate:"omitempty,oneof=DRAFT draft PUBLISHED published"`
}
type UpdateExamPrepUnitInput struct {
@ -32,4 +40,5 @@ type UpdateExamPrepUnitInput struct {
Description *string `json:"description,omitempty"`
Thumbnail *string `json:"thumbnail,omitempty"`
SortOrder *int `json:"sort_order,omitempty"`
PublishStatus *string `json:"publish_status,omitempty" validate:"omitempty,oneof=DRAFT draft PUBLISHED published"`
}

View File

@ -11,18 +11,26 @@ type Module struct {
Description *string `json:"description,omitempty"`
Icon *string `json:"icon,omitempty"`
SortOrder int `json:"sort_order"`
PublishStatus ContentPublishStatus `json:"publish_status"`
HasPractice bool `json:"has_practice"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at,omitempty"`
Access *LMSEntityAccess `json:"access,omitempty"`
}
// VisibleToLearners is true when the module appears in subscriber/catalog LMS APIs.
func (m Module) VisibleToLearners() bool {
return m.PublishStatus == ContentPublishPublished
}
type CreateModuleInput struct {
Name string `json:"name" validate:"required"`
Description *string `json:"description,omitempty"`
Icon *string `json:"icon,omitempty"`
// SortOrder within the course when set; omit to append after current max within course_id (uniqueness is per-course).
SortOrder *int `json:"sort_order,omitempty" validate:"omitempty,min=0"`
// Omit or empty defaults to DRAFT; set PUBLISHED to make visible to learners immediately.
PublishStatus *string `json:"publish_status,omitempty" validate:"omitempty,oneof=DRAFT draft PUBLISHED published"`
}
type UpdateModuleInput struct {
@ -30,4 +38,5 @@ type UpdateModuleInput struct {
Description *string `json:"description,omitempty"`
Icon *string `json:"icon,omitempty"`
SortOrder *int `json:"sort_order,omitempty"`
PublishStatus *string `json:"publish_status,omitempty" validate:"omitempty,oneof=DRAFT draft PUBLISHED published"`
}

View File

@ -10,11 +10,17 @@ type Program struct {
Category string `json:"category"`
Thumbnail *string `json:"thumbnail,omitempty"`
SortOrder int `json:"sort_order"`
PublishStatus ContentPublishStatus `json:"publish_status"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at,omitempty"`
Access *LMSEntityAccess `json:"access,omitempty"`
}
// VisibleToLearners is true when the program appears in subscriber/catalog LMS APIs.
func (p Program) VisibleToLearners() bool {
return p.PublishStatus == ContentPublishPublished
}
type CreateProgramInput struct {
Name string `json:"name" validate:"required"`
Description *string `json:"description,omitempty"`
@ -22,6 +28,8 @@ type CreateProgramInput struct {
Thumbnail *string `json:"thumbnail,omitempty"`
// SortOrder inserts at this global program order when set; omit to append after current max (sort_order uniqueness is enforced).
SortOrder *int `json:"sort_order,omitempty" validate:"omitempty,min=0"`
// Omit or empty defaults to DRAFT; set PUBLISHED to make visible to learners immediately.
PublishStatus *string `json:"publish_status,omitempty" validate:"omitempty,oneof=DRAFT draft PUBLISHED published"`
}
type UpdateProgramInput struct {
@ -30,4 +38,5 @@ type UpdateProgramInput struct {
Category *string `json:"category,omitempty" validate:"omitempty,oneof=LEARN_ENGLISH IELTS DUOLINGO"`
Thumbnail *string `json:"thumbnail,omitempty"`
SortOrder *int `json:"sort_order,omitempty"`
PublishStatus *string `json:"publish_status,omitempty" validate:"omitempty,oneof=DRAFT draft PUBLISHED published"`
}

View File

@ -284,6 +284,63 @@ func resolveDefinitionForRuntimeAUDIO(catalog []QuestionTypeDefinition) Question
return candidates[0]
}
// ResolveEffectiveQuestionTypeDefinitionID returns the definition ID clients should use when
// rendering a question. Stored FKs are preferred; legacy runtime types and unlinked DYNAMIC
// payloads are resolved against the active catalog when possible.
func ResolveEffectiveQuestionTypeDefinitionID(
storedID *int64,
runtimeQuestionType string,
catalog []QuestionTypeDefinition,
payload *DynamicQuestionPayload,
) *int64 {
if storedID != nil && *storedID > 0 {
return storedID
}
def := ResolveQuestionTypeDefinitionForQuestion(nil, runtimeQuestionType, catalog)
if def.ID > 0 {
id := def.ID
return &id
}
if payload != nil && UsesDynamicQuestionPayload(runtimeQuestionType) {
if matched := resolveDefinitionForDynamicPayload(*payload, catalog); matched.ID > 0 {
id := matched.ID
return &id
}
}
return nil
}
func resolveDefinitionForDynamicPayload(payload DynamicQuestionPayload, catalog []QuestionTypeDefinition) QuestionTypeDefinition {
var matches []QuestionTypeDefinition
for _, d := range catalog {
if status := strings.ToUpper(strings.TrimSpace(d.Status)); status != "" && status != "ACTIVE" {
continue
}
if err := ValidateDynamicPayloadAgainstDefinition(payload, d); err != nil {
continue
}
matches = append(matches, d)
}
if len(matches) == 0 {
return QuestionTypeDefinition{}
}
sort.Slice(matches, func(i, j int) bool {
if matches[i].IsSystem != matches[j].IsSystem {
return !matches[i].IsSystem
}
si := len(matches[i].StimulusSchema) + len(matches[i].ResponseSchema)
sj := len(matches[j].StimulusSchema) + len(matches[j].ResponseSchema)
if si != sj {
return si > sj
}
return matches[i].ID < matches[j].ID
})
return matches[0]
}
func humanizeDefinitionKey(key string) string {
parts := strings.Split(strings.ReplaceAll(key, "-", "_"), "_")
for i, p := range parts {

View File

@ -201,6 +201,52 @@ func TestResolveQuestionTypeDefinitionForQuestion_linkedDefinition(t *testing.T)
}
}
func TestResolveEffectiveQuestionTypeDefinitionID_legacyAUDIO(t *testing.T) {
got := ResolveEffectiveQuestionTypeDefinitionID(nil, "AUDIO", testQuestionTypeCatalog(), nil)
if got == nil || *got != 10 {
t.Fatalf("expected audio_conversation_type id 10, got %v", got)
}
}
func TestResolveEffectiveQuestionTypeDefinitionID_storedIDPreferred(t *testing.T) {
stored := int64(2)
got := ResolveEffectiveQuestionTypeDefinitionID(&stored, "AUDIO", testQuestionTypeCatalog(), nil)
if got == nil || *got != 2 {
t.Fatalf("expected stored id 2, got %v", got)
}
}
func TestResolveEffectiveQuestionTypeDefinitionID_dynamicPayloadMatch(t *testing.T) {
catalog := append(testQuestionTypeCatalog(), QuestionTypeDefinition{
ID: 42,
Key: "audio_with_timer",
DisplayName: "Audio With Timer",
StimulusComponentKinds: []string{"AUDIO_PROMPT"},
ResponseComponentKinds: []string{"ANSWER_TIMER", "AUDIO_RESPONSE"},
StimulusSchema: []DynamicElementDefinition{
{ID: "audio_prompt_1", Kind: "AUDIO_PROMPT", Required: true},
},
ResponseSchema: []DynamicElementDefinition{
{ID: "answer_timer_1", Kind: "ANSWER_TIMER", Required: true},
{ID: "audio_response_1", Kind: "AUDIO_RESPONSE", Required: true},
},
Status: "ACTIVE",
})
payload := &DynamicQuestionPayload{
Stimulus: []DynamicElementInstance{
{ID: "audio_prompt_1", Kind: "AUDIO_PROMPT", Value: "https://example.com/prompt.mp3"},
},
Response: []DynamicElementInstance{
{ID: "answer_timer_1", Kind: "ANSWER_TIMER", Value: map[string]interface{}{"seconds": 20}},
{ID: "audio_response_1", Kind: "AUDIO_RESPONSE", Value: "https://example.com/answer.mp3"},
},
}
got := ResolveEffectiveQuestionTypeDefinitionID(nil, "DYNAMIC", catalog, payload)
if got == nil || *got != 42 {
t.Fatalf("expected matched definition id 42, got %v", got)
}
}
func TestValidateQuestionTextNotAllowedForDynamic(t *testing.T) {
if err := ValidateQuestionTextNotAllowedForDynamic("DYNAMIC", "nope"); err == nil {
t.Fatal("expected error when question_text sent for DYNAMIC")

View File

@ -9,7 +9,7 @@ import (
type ExamPrepCatalogCourseStore interface {
CreateExamPrepCatalogCourse(ctx context.Context, input domain.CreateExamPrepCatalogCourseInput) (domain.ExamPrepCatalogCourse, error)
GetExamPrepCatalogCourseByID(ctx context.Context, id int64) (domain.ExamPrepCatalogCourse, error)
ListExamPrepCatalogCourses(ctx context.Context, limit, offset int32) ([]domain.ExamPrepCatalogCourse, int64, error)
ListExamPrepCatalogCourses(ctx context.Context, publishedOnly bool, limit, offset int32) ([]domain.ExamPrepCatalogCourse, int64, error)
ListAllExamPrepCatalogCourseIDs(ctx context.Context) ([]int64, error)
UpdateExamPrepCatalogCourse(ctx context.Context, id int64, input domain.UpdateExamPrepCatalogCourseInput) (domain.ExamPrepCatalogCourse, error)
DeleteExamPrepCatalogCourse(ctx context.Context, id int64) error

View File

@ -9,8 +9,9 @@ import (
type ExamPrepLessonStore interface {
CreateExamPrepUnitModuleLesson(ctx context.Context, unitModuleID int64, input domain.CreateExamPrepLessonInput) (domain.ExamPrepLesson, error)
GetExamPrepUnitModuleLessonByID(ctx context.Context, id int64) (domain.ExamPrepLesson, error)
ListExamPrepUnitModuleLessonsByUnitModuleID(ctx context.Context, unitModuleID int64, limit, offset int32) ([]domain.ExamPrepLesson, int64, error)
ListExamPrepUnitModuleLessonsByUnitModuleID(ctx context.Context, unitModuleID int64, publishedOnly bool, limit, offset int32) ([]domain.ExamPrepLesson, int64, error)
ListExamPrepUnitModuleLessonIDsByUnitModule(ctx context.Context, unitModuleID int64) ([]int64, error)
ListPublishedExamPrepUnitModuleLessonIDsByUnitModule(ctx context.Context, unitModuleID int64) ([]int64, error)
UpdateExamPrepUnitModuleLesson(ctx context.Context, id int64, input domain.UpdateExamPrepLessonInput) (domain.ExamPrepLesson, error)
DeleteExamPrepUnitModuleLesson(ctx context.Context, id int64) error
ReorderExamPrepUnitModuleLessonsInUnitModule(ctx context.Context, unitModuleID int64, orderedIDs []int64) error

View File

@ -9,8 +9,9 @@ import (
type ExamPrepModuleStore interface {
CreateExamPrepUnitModule(ctx context.Context, unitID int64, input domain.CreateExamPrepModuleInput) (domain.ExamPrepModule, error)
GetExamPrepUnitModuleByID(ctx context.Context, id int64) (domain.ExamPrepModule, error)
ListExamPrepUnitModulesByUnit(ctx context.Context, unitID int64, limit, offset int32) ([]domain.ExamPrepModule, int64, error)
ListExamPrepUnitModulesByUnit(ctx context.Context, unitID int64, publishedOnly bool, limit, offset int32) ([]domain.ExamPrepModule, int64, error)
ListExamPrepUnitModuleIDsByUnit(ctx context.Context, unitID int64) ([]int64, error)
ListPublishedExamPrepUnitModuleIDsByUnit(ctx context.Context, unitID int64) ([]int64, error)
UpdateExamPrepUnitModule(ctx context.Context, id int64, input domain.UpdateExamPrepModuleInput) (domain.ExamPrepModule, error)
DeleteExamPrepUnitModule(ctx context.Context, id int64) error
ReorderExamPrepUnitModulesInUnit(ctx context.Context, unitID int64, orderedIDs []int64) error

View File

@ -9,8 +9,9 @@ import (
type ExamPrepUnitStore interface {
CreateExamPrepUnit(ctx context.Context, catalogCourseID int64, input domain.CreateExamPrepUnitInput) (domain.ExamPrepUnit, error)
GetExamPrepUnitByID(ctx context.Context, id int64) (domain.ExamPrepUnit, error)
ListExamPrepUnitsByCatalogCourse(ctx context.Context, catalogCourseID int64, limit, offset int32) ([]domain.ExamPrepUnit, int64, error)
ListExamPrepUnitsByCatalogCourse(ctx context.Context, catalogCourseID int64, publishedOnly bool, limit, offset int32) ([]domain.ExamPrepUnit, int64, error)
ListExamPrepUnitIDsByCatalogCourse(ctx context.Context, catalogCourseID int64) ([]int64, error)
ListPublishedExamPrepUnitIDsByCatalogCourse(ctx context.Context, catalogCourseID int64) ([]int64, error)
UpdateExamPrepUnit(ctx context.Context, id int64, input domain.UpdateExamPrepUnitInput) (domain.ExamPrepUnit, error)
DeleteExamPrepUnit(ctx context.Context, id int64) error
ReorderExamPrepUnitsInCatalogCourse(ctx context.Context, catalogCourseID int64, orderedIDs []int64) error

View File

@ -8,8 +8,9 @@ import (
type CourseStore interface {
CreateCourse(ctx context.Context, programID int64, input domain.CreateCourseInput) (domain.Course, error)
GetCourseByID(ctx context.Context, id int64) (domain.Course, error)
ListCoursesByProgramID(ctx context.Context, programID int64, limit, offset int32) ([]domain.Course, int64, error)
ListCoursesByProgramID(ctx context.Context, programID int64, publishedOnly bool, limit, offset int32) ([]domain.Course, int64, error)
ListCourseIDsByProgram(ctx context.Context, programID int64) ([]int64, error)
ListPublishedCourseIDsByProgram(ctx context.Context, programID int64) ([]int64, error)
ReorderCoursesInProgram(ctx context.Context, programID int64, orderedIDs []int64) error
UpdateCourse(ctx context.Context, id int64, input domain.UpdateCourseInput) (domain.Course, error)
DeleteCourse(ctx context.Context, id int64) error

View File

@ -8,8 +8,9 @@ import (
type ModuleStore interface {
CreateModule(ctx context.Context, programID, courseID int64, input domain.CreateModuleInput) (domain.Module, error)
GetModuleByID(ctx context.Context, id int64) (domain.Module, error)
ListModulesByProgramAndCourse(ctx context.Context, programID, courseID int64, limit, offset int32) ([]domain.Module, int64, error)
ListModulesByProgramAndCourse(ctx context.Context, programID, courseID int64, publishedOnly bool, limit, offset int32) ([]domain.Module, int64, error)
ListModuleIDsByCourse(ctx context.Context, courseID int64) ([]int64, error)
ListPublishedModuleIDsByCourse(ctx context.Context, courseID int64) ([]int64, error)
ReorderModulesInCourse(ctx context.Context, courseID int64, orderedIDs []int64) error
UpdateModule(ctx context.Context, id int64, input domain.UpdateModuleInput) (domain.Module, error)
DeleteModule(ctx context.Context, id int64) error

View File

@ -8,7 +8,7 @@ import (
type ProgramStore interface {
CreateProgram(ctx context.Context, input domain.CreateProgramInput) (domain.Program, error)
GetProgramByID(ctx context.Context, id int64) (domain.Program, error)
ListPrograms(ctx context.Context, limit, offset int32) ([]domain.Program, int64, error)
ListPrograms(ctx context.Context, publishedOnly bool, limit, offset int32) ([]domain.Program, int64, error)
ListAllProgramIDs(ctx context.Context) ([]int64, error)
ReorderPrograms(ctx context.Context, orderedIDs []int64) error
UpdateProgram(ctx context.Context, id int64, input domain.UpdateProgramInput) (domain.Program, error)

View File

@ -17,6 +17,7 @@ func examPrepCatalogCourseToDomain(c dbgen.ExamPrepCatalogCourse) domain.ExamPre
Name: c.Name,
Category: c.Category,
SortOrder: int(c.SortOrder),
PublishStatus: domain.ContentPublishStatusFromDB(c.PublishStatus),
}
out.Description = fromPgText(c.Description)
out.Thumbnail = fromPgText(c.Thumbnail)
@ -34,6 +35,7 @@ func (s *Store) CreateExamPrepCatalogCourse(ctx context.Context, input domain.Cr
Description: toPgText(input.Description),
Category: input.Category,
Thumbnail: toPgText(input.Thumbnail),
PublishStatus: string(domain.ContentPublishStatusFromCreateInput(input.PublishStatus)),
})
if err != nil {
return domain.ExamPrepCatalogCourse{}, err
@ -58,15 +60,17 @@ func (s *Store) GetExamPrepCatalogCourseByID(ctx context.Context, id int64) (dom
SortOrder: c.SortOrder,
CreatedAt: c.CreatedAt,
UpdatedAt: c.UpdatedAt,
PublishStatus: c.PublishStatus,
})
out.HasPractice = c.HasPractice
return out, nil
}
func (s *Store) ListExamPrepCatalogCourses(ctx context.Context, limit, offset int32) ([]domain.ExamPrepCatalogCourse, int64, error) {
func (s *Store) ListExamPrepCatalogCourses(ctx context.Context, publishedOnly bool, limit, offset int32) ([]domain.ExamPrepCatalogCourse, int64, error) {
rows, err := s.queries.ExamPrepListCatalogCourses(ctx, dbgen.ExamPrepListCatalogCoursesParams{
Limit: limit,
Offset: offset,
PublishedOnly: publishedOnly,
})
if err != nil {
return nil, 0, err
@ -89,6 +93,7 @@ func (s *Store) ListExamPrepCatalogCourses(ctx context.Context, limit, offset in
SortOrder: r.SortOrder,
CreatedAt: r.CreatedAt,
UpdatedAt: r.UpdatedAt,
PublishStatus: r.PublishStatus,
})
item.UnitsCount = &r.UnitsCount
item.ModulesCount = &r.ModulesCount
@ -117,6 +122,7 @@ func (s *Store) UpdateExamPrepCatalogCourse(ctx context.Context, id int64, input
Category: optionalTextUpdate(input.Category),
Thumbnail: optionalTextUpdate(input.Thumbnail),
SortOrder: optionalInt4Update(input.SortOrder),
PublishStatus: optionalPublishStatusUpdate(input.PublishStatus),
})
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {

View File

@ -17,6 +17,7 @@ func examPrepLessonToDomain(l dbgen.ExamPrepUnitModuleLesson) domain.ExamPrepLes
UnitModuleID: l.UnitModuleID,
Title: l.Title,
SortOrder: int(l.SortOrder),
PublishStatus: domain.ContentPublishStatusFromDB(l.PublishStatus),
}
out.VideoURL = fromPgText(l.VideoUrl)
out.Thumbnail = fromPgText(l.Thumbnail)
@ -36,6 +37,7 @@ func (s *Store) CreateExamPrepUnitModuleLesson(ctx context.Context, unitModuleID
VideoUrl: toPgText(input.VideoURL),
Thumbnail: toPgText(input.Thumbnail),
Description: toPgText(input.Description),
PublishStatus: string(domain.ContentPublishStatusFromCreateInput(input.PublishStatus)),
})
if err != nil {
return domain.ExamPrepLesson{}, err
@ -61,16 +63,18 @@ func (s *Store) GetExamPrepUnitModuleLessonByID(ctx context.Context, id int64) (
SortOrder: l.SortOrder,
CreatedAt: l.CreatedAt,
UpdatedAt: l.UpdatedAt,
PublishStatus: l.PublishStatus,
})
out.HasPractice = l.HasPractice
return out, nil
}
func (s *Store) ListExamPrepUnitModuleLessonsByUnitModuleID(ctx context.Context, unitModuleID int64, limit, offset int32) ([]domain.ExamPrepLesson, int64, error) {
func (s *Store) ListExamPrepUnitModuleLessonsByUnitModuleID(ctx context.Context, unitModuleID int64, publishedOnly bool, limit, offset int32) ([]domain.ExamPrepLesson, int64, error) {
rows, err := s.queries.ExamPrepListUnitModuleLessonsByUnitModuleID(ctx, dbgen.ExamPrepListUnitModuleLessonsByUnitModuleIDParams{
UnitModuleID: unitModuleID,
Limit: limit,
Offset: offset,
PublishedOnly: publishedOnly,
})
if err != nil {
return nil, 0, err
@ -94,6 +98,7 @@ func (s *Store) ListExamPrepUnitModuleLessonsByUnitModuleID(ctx context.Context,
SortOrder: r.SortOrder,
CreatedAt: r.CreatedAt,
UpdatedAt: r.UpdatedAt,
PublishStatus: r.PublishStatus,
})
item.HasPractice = r.HasPractice
out = append(out, item)
@ -105,6 +110,10 @@ func (s *Store) ListExamPrepUnitModuleLessonIDsByUnitModule(ctx context.Context,
return s.queries.ExamPrepListUnitModuleLessonIDsByUnitModule(ctx, unitModuleID)
}
func (s *Store) ListPublishedExamPrepUnitModuleLessonIDsByUnitModule(ctx context.Context, unitModuleID int64) ([]int64, error) {
return s.queries.ExamPrepListPublishedUnitModuleLessonIDsByUnitModule(ctx, unitModuleID)
}
func (s *Store) UpdateExamPrepUnitModuleLesson(ctx context.Context, id int64, input domain.UpdateExamPrepLessonInput) (domain.ExamPrepLesson, error) {
var titleText pgtype.Text
if input.Title != nil {
@ -119,6 +128,7 @@ func (s *Store) UpdateExamPrepUnitModuleLesson(ctx context.Context, id int64, in
Thumbnail: optionalTextUpdate(input.Thumbnail),
Description: optionalTextUpdate(input.Description),
SortOrder: optionalInt4Update(input.SortOrder),
PublishStatus: optionalPublishStatusUpdate(input.PublishStatus),
})
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {

View File

@ -17,6 +17,7 @@ func examPrepModuleToDomain(m dbgen.ExamPrepUnitModule) domain.ExamPrepModule {
UnitID: m.UnitID,
Name: m.Name,
SortOrder: int(m.SortOrder),
PublishStatus: domain.ContentPublishStatusFromDB(m.PublishStatus),
}
out.Description = fromPgText(m.Description)
out.Thumbnail = fromPgText(m.Thumbnail)
@ -36,6 +37,7 @@ func (s *Store) CreateExamPrepUnitModule(ctx context.Context, unitID int64, inpu
Description: toPgText(input.Description),
Thumbnail: toPgText(input.Thumbnail),
Icon: toPgText(input.Icon),
PublishStatus: string(domain.ContentPublishStatusFromCreateInput(input.PublishStatus)),
})
if err != nil {
return domain.ExamPrepModule{}, err
@ -61,16 +63,18 @@ func (s *Store) GetExamPrepUnitModuleByID(ctx context.Context, id int64) (domain
SortOrder: m.SortOrder,
CreatedAt: m.CreatedAt,
UpdatedAt: m.UpdatedAt,
PublishStatus: m.PublishStatus,
})
out.HasPractice = m.HasPractice
return out, nil
}
func (s *Store) ListExamPrepUnitModulesByUnit(ctx context.Context, unitID int64, limit, offset int32) ([]domain.ExamPrepModule, int64, error) {
func (s *Store) ListExamPrepUnitModulesByUnit(ctx context.Context, unitID int64, publishedOnly bool, limit, offset int32) ([]domain.ExamPrepModule, int64, error) {
rows, err := s.queries.ExamPrepListUnitModulesByUnit(ctx, dbgen.ExamPrepListUnitModulesByUnitParams{
UnitID: unitID,
Limit: limit,
Offset: offset,
PublishedOnly: publishedOnly,
})
if err != nil {
return nil, 0, err
@ -94,6 +98,7 @@ func (s *Store) ListExamPrepUnitModulesByUnit(ctx context.Context, unitID int64,
SortOrder: r.SortOrder,
CreatedAt: r.CreatedAt,
UpdatedAt: r.UpdatedAt,
PublishStatus: r.PublishStatus,
})
item.LessonsCount = &r.LessonsCount
item.PracticesCount = &r.PracticesCount
@ -107,6 +112,10 @@ func (s *Store) ListExamPrepUnitModuleIDsByUnit(ctx context.Context, unitID int6
return s.queries.ExamPrepListUnitModuleIDsByUnit(ctx, unitID)
}
func (s *Store) ListPublishedExamPrepUnitModuleIDsByUnit(ctx context.Context, unitID int64) ([]int64, error) {
return s.queries.ExamPrepListPublishedUnitModuleIDsByUnit(ctx, unitID)
}
func (s *Store) UpdateExamPrepUnitModule(ctx context.Context, id int64, input domain.UpdateExamPrepModuleInput) (domain.ExamPrepModule, error) {
var nameText pgtype.Text
if input.Name != nil {
@ -121,6 +130,7 @@ func (s *Store) UpdateExamPrepUnitModule(ctx context.Context, id int64, input do
Thumbnail: optionalTextUpdate(input.Thumbnail),
Icon: optionalTextUpdate(input.Icon),
SortOrder: optionalInt4Update(input.SortOrder),
PublishStatus: optionalPublishStatusUpdate(input.PublishStatus),
})
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {

View File

@ -17,6 +17,7 @@ func examPrepUnitToDomain(u dbgen.ExamPrepUnit) domain.ExamPrepUnit {
CatalogCourseID: u.CatalogCourseID,
Name: u.Name,
SortOrder: int(u.SortOrder),
PublishStatus: domain.ContentPublishStatusFromDB(u.PublishStatus),
}
out.Description = fromPgText(u.Description)
out.Thumbnail = fromPgText(u.Thumbnail)
@ -29,6 +30,7 @@ func examPrepUnitToDomain(u dbgen.ExamPrepUnit) domain.ExamPrepUnit {
}
func (s *Store) CreateExamPrepUnit(ctx context.Context, catalogCourseID int64, input domain.CreateExamPrepUnitInput) (domain.ExamPrepUnit, error) {
pub := string(domain.ContentPublishStatusFromCreateInput(input.PublishStatus))
if input.SortOrder != nil {
q, tx, err := s.BeginTx(ctx)
if err != nil {
@ -48,6 +50,7 @@ func (s *Store) CreateExamPrepUnit(ctx context.Context, catalogCourseID int64, i
Description: toPgText(input.Description),
Thumbnail: toPgText(input.Thumbnail),
SortOrder: pgtype.Int4{Int32: target, Valid: true},
PublishStatus: pub,
})
if err != nil {
return domain.ExamPrepUnit{}, err
@ -64,6 +67,7 @@ func (s *Store) CreateExamPrepUnit(ctx context.Context, catalogCourseID int64, i
Description: toPgText(input.Description),
Thumbnail: toPgText(input.Thumbnail),
SortOrder: pgtype.Int4{Valid: false},
PublishStatus: pub,
})
if err != nil {
return domain.ExamPrepUnit{}, err
@ -88,16 +92,18 @@ func (s *Store) GetExamPrepUnitByID(ctx context.Context, id int64) (domain.ExamP
SortOrder: u.SortOrder,
CreatedAt: u.CreatedAt,
UpdatedAt: u.UpdatedAt,
PublishStatus: u.PublishStatus,
})
out.HasPractice = u.HasPractice
return out, nil
}
func (s *Store) ListExamPrepUnitsByCatalogCourse(ctx context.Context, catalogCourseID int64, limit, offset int32) ([]domain.ExamPrepUnit, int64, error) {
func (s *Store) ListExamPrepUnitsByCatalogCourse(ctx context.Context, catalogCourseID int64, publishedOnly bool, limit, offset int32) ([]domain.ExamPrepUnit, int64, error) {
rows, err := s.queries.ExamPrepListUnitsByCatalogCourse(ctx, dbgen.ExamPrepListUnitsByCatalogCourseParams{
CatalogCourseID: catalogCourseID,
Limit: limit,
Offset: offset,
PublishedOnly: publishedOnly,
})
if err != nil {
return nil, 0, err
@ -120,6 +126,7 @@ func (s *Store) ListExamPrepUnitsByCatalogCourse(ctx context.Context, catalogCou
SortOrder: r.SortOrder,
CreatedAt: r.CreatedAt,
UpdatedAt: r.UpdatedAt,
PublishStatus: r.PublishStatus,
})
item.ModulesCount = &r.ModulesCount
item.LessonsCount = &r.LessonsCount
@ -134,6 +141,10 @@ func (s *Store) ListExamPrepUnitIDsByCatalogCourse(ctx context.Context, catalogC
return s.queries.ExamPrepListUnitIDsByCatalogCourse(ctx, catalogCourseID)
}
func (s *Store) ListPublishedExamPrepUnitIDsByCatalogCourse(ctx context.Context, catalogCourseID int64) ([]int64, error) {
return s.queries.ExamPrepListPublishedUnitIDsByCatalogCourse(ctx, catalogCourseID)
}
func (s *Store) UpdateExamPrepUnit(ctx context.Context, id int64, input domain.UpdateExamPrepUnitInput) (domain.ExamPrepUnit, error) {
var nameText pgtype.Text
if input.Name != nil {
@ -147,6 +158,7 @@ func (s *Store) UpdateExamPrepUnit(ctx context.Context, id int64, input domain.U
Description: optionalTextUpdate(input.Description),
Thumbnail: optionalTextUpdate(input.Thumbnail),
SortOrder: optionalInt4Update(input.SortOrder),
PublishStatus: optionalPublishStatusUpdate(input.PublishStatus),
})
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {

View File

@ -16,6 +16,7 @@ func courseToDomain(c dbgen.Course) domain.Course {
ID: c.ID,
ProgramID: c.ProgramID,
Name: c.Name,
PublishStatus: domain.ContentPublishStatusFromDB(c.PublishStatus),
}
out.Description = fromPgText(c.Description)
out.Thumbnail = fromPgText(c.Thumbnail)
@ -29,6 +30,7 @@ func courseToDomain(c dbgen.Course) domain.Course {
}
func (s *Store) CreateCourse(ctx context.Context, programID int64, input domain.CreateCourseInput) (domain.Course, error) {
pub := string(domain.ContentPublishStatusFromCreateInput(input.PublishStatus))
if input.SortOrder != nil {
q, tx, err := s.BeginTx(ctx)
if err != nil {
@ -48,6 +50,7 @@ func (s *Store) CreateCourse(ctx context.Context, programID int64, input domain.
Description: toPgText(input.Description),
Thumbnail: toPgText(input.Thumbnail),
SortOrder: pgtype.Int4{Int32: target, Valid: true},
PublishStatus: pub,
})
if err != nil {
return domain.Course{}, err
@ -64,6 +67,7 @@ func (s *Store) CreateCourse(ctx context.Context, programID int64, input domain.
Description: toPgText(input.Description),
Thumbnail: toPgText(input.Thumbnail),
SortOrder: pgtype.Int4{Valid: false},
PublishStatus: pub,
})
if err != nil {
return domain.Course{}, err
@ -75,6 +79,10 @@ func (s *Store) ListCourseIDsByProgram(ctx context.Context, programID int64) ([]
return s.queries.ListCourseIDsByProgram(ctx, programID)
}
func (s *Store) ListPublishedCourseIDsByProgram(ctx context.Context, programID int64) ([]int64, error) {
return s.queries.ListPublishedCourseIDsByProgram(ctx, programID)
}
func (s *Store) GetCourseByID(ctx context.Context, id int64) (domain.Course, error) {
c, err := s.queries.GetCourseByID(ctx, id)
if err != nil {
@ -92,16 +100,18 @@ func (s *Store) GetCourseByID(ctx context.Context, id int64) (domain.Course, err
SortOrder: c.SortOrder,
CreatedAt: c.CreatedAt,
UpdatedAt: c.UpdatedAt,
PublishStatus: c.PublishStatus,
})
out.HasPractice = c.HasPractice
return out, nil
}
func (s *Store) ListCoursesByProgramID(ctx context.Context, programID int64, limit, offset int32) ([]domain.Course, int64, error) {
func (s *Store) ListCoursesByProgramID(ctx context.Context, programID int64, publishedOnly bool, limit, offset int32) ([]domain.Course, int64, error) {
rows, err := s.queries.ListCoursesByProgramID(ctx, dbgen.ListCoursesByProgramIDParams{
ProgramID: programID,
Limit: limit,
Offset: offset,
PublishedOnly: publishedOnly,
})
if err != nil {
return nil, 0, err
@ -124,6 +134,7 @@ func (s *Store) ListCoursesByProgramID(ctx context.Context, programID int64, lim
CreatedAt: r.CreatedAt,
UpdatedAt: r.UpdatedAt,
SortOrder: r.SortOrder,
PublishStatus: r.PublishStatus,
})
co.ModuleCount = int(r.ModuleCount)
co.LessonCount = int(r.LessonCount)
@ -165,6 +176,7 @@ func (s *Store) UpdateCourse(ctx context.Context, id int64, input domain.UpdateC
Description: optionalTextUpdate(input.Description),
Thumbnail: optionalTextUpdate(input.Thumbnail),
SortOrder: pgtype.Int4{Valid: false},
PublishStatus: optionalPublishStatusUpdate(input.PublishStatus),
})
if err != nil {
return domain.Course{}, err
@ -185,6 +197,7 @@ func (s *Store) UpdateCourse(ctx context.Context, id int64, input domain.UpdateC
Description: optionalTextUpdate(input.Description),
Thumbnail: optionalTextUpdate(input.Thumbnail),
SortOrder: sortParam,
PublishStatus: optionalPublishStatusUpdate(input.PublishStatus),
})
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {

View File

@ -17,6 +17,7 @@ func moduleToDomain(m dbgen.Module) domain.Module {
ProgramID: m.ProgramID,
CourseID: m.CourseID,
Name: m.Name,
PublishStatus: domain.ContentPublishStatusFromDB(m.PublishStatus),
}
out.Description = fromPgText(m.Description)
out.Icon = fromPgText(m.Icon)
@ -30,6 +31,7 @@ func moduleToDomain(m dbgen.Module) domain.Module {
}
func (s *Store) CreateModule(ctx context.Context, programID, courseID int64, input domain.CreateModuleInput) (domain.Module, error) {
pub := string(domain.ContentPublishStatusFromCreateInput(input.PublishStatus))
if input.SortOrder != nil {
q, tx, err := s.BeginTx(ctx)
if err != nil {
@ -50,6 +52,7 @@ func (s *Store) CreateModule(ctx context.Context, programID, courseID int64, inp
Description: toPgText(input.Description),
Icon: toPgText(input.Icon),
SortOrder: pgtype.Int4{Int32: target, Valid: true},
PublishStatus: pub,
})
if err != nil {
return domain.Module{}, err
@ -67,6 +70,7 @@ func (s *Store) CreateModule(ctx context.Context, programID, courseID int64, inp
Description: toPgText(input.Description),
Icon: toPgText(input.Icon),
SortOrder: pgtype.Int4{Valid: false},
PublishStatus: pub,
})
if err != nil {
return domain.Module{}, err
@ -78,6 +82,10 @@ func (s *Store) ListModuleIDsByCourse(ctx context.Context, courseID int64) ([]in
return s.queries.ListModuleIDsByCourse(ctx, courseID)
}
func (s *Store) ListPublishedModuleIDsByCourse(ctx context.Context, courseID int64) ([]int64, error) {
return s.queries.ListPublishedModuleIDsByCourse(ctx, courseID)
}
func (s *Store) GetModuleByID(ctx context.Context, id int64) (domain.Module, error) {
m, err := s.queries.GetModuleByID(ctx, id)
if err != nil {
@ -96,17 +104,19 @@ func (s *Store) GetModuleByID(ctx context.Context, id int64) (domain.Module, err
SortOrder: m.SortOrder,
CreatedAt: m.CreatedAt,
UpdatedAt: m.UpdatedAt,
PublishStatus: m.PublishStatus,
})
out.HasPractice = m.HasPractice
return out, nil
}
func (s *Store) ListModulesByProgramAndCourse(ctx context.Context, programID, courseID int64, limit, offset int32) ([]domain.Module, int64, error) {
func (s *Store) ListModulesByProgramAndCourse(ctx context.Context, programID, courseID int64, publishedOnly bool, limit, offset int32) ([]domain.Module, int64, error) {
rows, err := s.queries.ListModulesByProgramAndCourse(ctx, dbgen.ListModulesByProgramAndCourseParams{
ProgramID: programID,
CourseID: courseID,
Limit: limit,
Offset: offset,
PublishedOnly: publishedOnly,
})
if err != nil {
return nil, 0, err
@ -130,6 +140,7 @@ func (s *Store) ListModulesByProgramAndCourse(ctx context.Context, programID, co
CreatedAt: r.CreatedAt,
UpdatedAt: r.UpdatedAt,
SortOrder: r.SortOrder,
PublishStatus: r.PublishStatus,
})
mod.HasPractice = r.HasPractice
out = append(out, mod)
@ -173,6 +184,7 @@ func (s *Store) UpdateModule(ctx context.Context, id int64, input domain.UpdateM
Description: optionalTextUpdate(input.Description),
Icon: optionalTextUpdate(input.Icon),
SortOrder: sortParam,
PublishStatus: optionalPublishStatusUpdate(input.PublishStatus),
})
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {

View File

@ -17,6 +17,7 @@ func programToDomain(p dbgen.Program) domain.Program {
ID: p.ID,
Name: p.Name,
Category: p.Category,
PublishStatus: domain.ContentPublishStatusFromDB(p.PublishStatus),
}
out.Description = fromPgText(p.Description)
out.Thumbnail = fromPgText(p.Thumbnail)
@ -30,6 +31,7 @@ func programToDomain(p dbgen.Program) domain.Program {
}
func (s *Store) CreateProgram(ctx context.Context, input domain.CreateProgramInput) (domain.Program, error) {
pub := string(domain.ContentPublishStatusFromCreateInput(input.PublishStatus))
if input.SortOrder != nil {
q, tx, err := s.BeginTx(ctx)
if err != nil {
@ -46,6 +48,7 @@ func (s *Store) CreateProgram(ctx context.Context, input domain.CreateProgramInp
Category: input.Category,
Thumbnail: toPgText(input.Thumbnail),
SortOrder: pgtype.Int4{Int32: target, Valid: true},
PublishStatus: pub,
})
if err != nil {
return domain.Program{}, err
@ -62,6 +65,7 @@ func (s *Store) CreateProgram(ctx context.Context, input domain.CreateProgramInp
Category: input.Category,
Thumbnail: toPgText(input.Thumbnail),
SortOrder: pgtype.Int4{Valid: false},
PublishStatus: pub,
})
if err != nil {
return domain.Program{}, err
@ -84,10 +88,11 @@ func (s *Store) GetProgramByID(ctx context.Context, id int64) (domain.Program, e
return programToDomain(p), nil
}
func (s *Store) ListPrograms(ctx context.Context, limit, offset int32) ([]domain.Program, int64, error) {
func (s *Store) ListPrograms(ctx context.Context, publishedOnly bool, limit, offset int32) ([]domain.Program, int64, error) {
rows, err := s.queries.ListPrograms(ctx, dbgen.ListProgramsParams{
Limit: limit,
Offset: offset,
PublishedOnly: publishedOnly,
})
if err != nil {
return nil, 0, err
@ -110,6 +115,7 @@ func (s *Store) ListPrograms(ctx context.Context, limit, offset int32) ([]domain
CreatedAt: r.CreatedAt,
UpdatedAt: r.UpdatedAt,
SortOrder: r.SortOrder,
PublishStatus: r.PublishStatus,
}))
}
return out, total, nil
@ -173,6 +179,7 @@ func (s *Store) UpdateProgram(ctx context.Context, id int64, input domain.Update
Category: optionalTextUpdate(input.Category),
Thumbnail: optionalTextUpdate(input.Thumbnail),
SortOrder: pgtype.Int4{Valid: false},
PublishStatus: optionalPublishStatusUpdate(input.PublishStatus),
})
if err != nil {
return domain.Program{}, err
@ -198,6 +205,7 @@ func (s *Store) UpdateProgram(ctx context.Context, id int64, input domain.Update
Category: optionalTextUpdate(input.Category),
Thumbnail: optionalTextUpdate(input.Thumbnail),
SortOrder: sortParam,
PublishStatus: optionalPublishStatusUpdate(input.PublishStatus),
})
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {

View File

@ -42,7 +42,7 @@ func (s *Service) GetByID(ctx context.Context, id int64) (domain.Course, error)
return c, nil
}
func (s *Service) ListByProgram(ctx context.Context, programID int64, limit, offset int32) ([]domain.Course, int64, error) {
func (s *Service) ListByProgram(ctx context.Context, programID int64, publishedOnly bool, limit, offset int32) ([]domain.Course, int64, error) {
if _, err := s.programs.GetProgramByID(ctx, programID); err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, 0, programs.ErrProgramNotFound
@ -58,7 +58,7 @@ func (s *Service) ListByProgram(ctx context.Context, programID int64, limit, off
if offset < 0 {
offset = 0
}
return s.courses.ListCoursesByProgramID(ctx, programID, limit, offset)
return s.courses.ListCoursesByProgramID(ctx, programID, publishedOnly, limit, offset)
}
func (s *Service) Update(ctx context.Context, id int64, input domain.UpdateCourseInput) (domain.Course, error) {

View File

@ -59,7 +59,7 @@ func (s *Service) GetCatalogCourseByID(ctx context.Context, id int64) (domain.Ex
return c, nil
}
func (s *Service) ListCatalogCourses(ctx context.Context, limit, offset int32) ([]domain.ExamPrepCatalogCourse, int64, error) {
func (s *Service) ListCatalogCourses(ctx context.Context, publishedOnly bool, limit, offset int32) ([]domain.ExamPrepCatalogCourse, int64, error) {
if limit <= 0 {
limit = 20
}
@ -69,7 +69,7 @@ func (s *Service) ListCatalogCourses(ctx context.Context, limit, offset int32) (
if offset < 0 {
offset = 0
}
return s.store.ListExamPrepCatalogCourses(ctx, limit, offset)
return s.store.ListExamPrepCatalogCourses(ctx, publishedOnly, limit, offset)
}
func (s *Service) UpdateCatalogCourse(ctx context.Context, id int64, input domain.UpdateExamPrepCatalogCourseInput) (domain.ExamPrepCatalogCourse, error) {
@ -124,7 +124,7 @@ func (s *Service) CreateUnit(ctx context.Context, catalogCourseID int64, input d
return s.store.CreateExamPrepUnit(ctx, catalogCourseID, input)
}
func (s *Service) ListUnitsByCatalogCourse(ctx context.Context, catalogCourseID int64, limit, offset int32) ([]domain.ExamPrepUnit, int64, error) {
func (s *Service) ListUnitsByCatalogCourse(ctx context.Context, catalogCourseID int64, publishedOnly bool, limit, offset int32) ([]domain.ExamPrepUnit, int64, error) {
if err := s.ensureCatalogCourse(ctx, catalogCourseID); err != nil {
return nil, 0, err
}
@ -137,7 +137,7 @@ func (s *Service) ListUnitsByCatalogCourse(ctx context.Context, catalogCourseID
if offset < 0 {
offset = 0
}
return s.store.ListExamPrepUnitsByCatalogCourse(ctx, catalogCourseID, limit, offset)
return s.store.ListExamPrepUnitsByCatalogCourse(ctx, catalogCourseID, publishedOnly, limit, offset)
}
func (s *Service) GetUnitByID(ctx context.Context, id int64) (domain.ExamPrepUnit, error) {
@ -206,7 +206,7 @@ func (s *Service) CreateModule(ctx context.Context, unitID int64, input domain.C
return s.store.CreateExamPrepUnitModule(ctx, unitID, input)
}
func (s *Service) ListModulesByUnit(ctx context.Context, unitID int64, limit, offset int32) ([]domain.ExamPrepModule, int64, error) {
func (s *Service) ListModulesByUnit(ctx context.Context, unitID int64, publishedOnly bool, limit, offset int32) ([]domain.ExamPrepModule, int64, error) {
if err := s.ensureUnit(ctx, unitID); err != nil {
return nil, 0, err
}
@ -219,7 +219,7 @@ func (s *Service) ListModulesByUnit(ctx context.Context, unitID int64, limit, of
if offset < 0 {
offset = 0
}
return s.store.ListExamPrepUnitModulesByUnit(ctx, unitID, limit, offset)
return s.store.ListExamPrepUnitModulesByUnit(ctx, unitID, publishedOnly, limit, offset)
}
func (s *Service) GetModuleByID(ctx context.Context, id int64) (domain.ExamPrepModule, error) {
@ -288,7 +288,7 @@ func (s *Service) CreateLesson(ctx context.Context, unitModuleID int64, input do
return s.store.CreateExamPrepUnitModuleLesson(ctx, unitModuleID, input)
}
func (s *Service) ListLessonsByUnitModule(ctx context.Context, unitModuleID int64, limit, offset int32) ([]domain.ExamPrepLesson, int64, error) {
func (s *Service) ListLessonsByUnitModule(ctx context.Context, unitModuleID int64, publishedOnly bool, limit, offset int32) ([]domain.ExamPrepLesson, int64, error) {
if err := s.ensureModule(ctx, unitModuleID); err != nil {
return nil, 0, err
}
@ -301,7 +301,7 @@ func (s *Service) ListLessonsByUnitModule(ctx context.Context, unitModuleID int6
if offset < 0 {
offset = 0
}
return s.store.ListExamPrepUnitModuleLessonsByUnitModuleID(ctx, unitModuleID, limit, offset)
return s.store.ListExamPrepUnitModuleLessonsByUnitModuleID(ctx, unitModuleID, publishedOnly, limit, offset)
}
func (s *Service) GetLessonByID(ctx context.Context, id int64) (domain.ExamPrepLesson, error) {

View File

@ -345,7 +345,7 @@ func (s *Service) lmsCourseProgress(ctx context.Context, userID, courseID int64)
return 1, true, 1, 1, nil
}
moduleIDs, err := s.store.ListModuleIDsByCourse(ctx, courseID)
moduleIDs, err := s.store.ListPublishedModuleIDsByCourse(ctx, courseID)
if err != nil {
return 0, false, 0, 0, err
}
@ -373,7 +373,7 @@ func (s *Service) lmsCourseProgress(ctx context.Context, userID, courseID int64)
}
func (s *Service) lmsProgramProgress(ctx context.Context, userID, programID int64) (fraction float64, done bool, completed, total int32, err error) {
courseIDs, err := s.store.ListCourseIDsByProgram(ctx, programID)
courseIDs, err := s.store.ListPublishedCourseIDsByProgram(ctx, programID)
if err != nil {
return 0, false, 0, 0, err
}
@ -428,7 +428,7 @@ func (s *Service) examPrepLessonProgress(ctx context.Context, userID, lessonID i
}
func (s *Service) examPrepModuleProgress(ctx context.Context, userID, moduleID int64) (fraction float64, done bool, completed, total int32, err error) {
lessonIDs, err := s.store.ListExamPrepUnitModuleLessonIDsByUnitModule(ctx, moduleID)
lessonIDs, err := s.store.ListPublishedExamPrepUnitModuleLessonIDsByUnitModule(ctx, moduleID)
if err != nil {
return 0, false, 0, 0, err
}
@ -456,7 +456,7 @@ func (s *Service) examPrepModuleProgress(ctx context.Context, userID, moduleID i
}
func (s *Service) examPrepUnitProgress(ctx context.Context, userID, unitID int64) (fraction float64, done bool, completed, total int32, err error) {
moduleIDs, err := s.store.ListExamPrepUnitModuleIDsByUnit(ctx, unitID)
moduleIDs, err := s.store.ListPublishedExamPrepUnitModuleIDsByUnit(ctx, unitID)
if err != nil {
return 0, false, 0, 0, err
}
@ -484,7 +484,7 @@ func (s *Service) examPrepUnitProgress(ctx context.Context, userID, unitID int64
}
func (s *Service) examPrepCatalogCourseProgress(ctx context.Context, userID, catalogCourseID int64) (fraction float64, done bool, completed, total int32, err error) {
unitIDs, err := s.store.ListExamPrepUnitIDsByCatalogCourse(ctx, catalogCourseID)
unitIDs, err := s.store.ListPublishedExamPrepUnitIDsByCatalogCourse(ctx, catalogCourseID)
if err != nil {
return 0, false, 0, 0, err
}

View File

@ -53,7 +53,7 @@ func (s *Service) GetByID(ctx context.Context, id int64) (domain.Module, error)
}
// ListByCourse loads the course and lists modules for its program_id and course_id.
func (s *Service) ListByCourse(ctx context.Context, courseID int64, limit, offset int32) ([]domain.Module, int64, error) {
func (s *Service) ListByCourse(ctx context.Context, courseID int64, publishedOnly bool, limit, offset int32) ([]domain.Module, int64, error) {
c, err := s.getCourseOrErr(ctx, courseID)
if err != nil {
return nil, 0, err
@ -67,7 +67,7 @@ func (s *Service) ListByCourse(ctx context.Context, courseID int64, limit, offse
if offset < 0 {
offset = 0
}
return s.modules.ListModulesByProgramAndCourse(ctx, c.ProgramID, courseID, limit, offset)
return s.modules.ListModulesByProgramAndCourse(ctx, c.ProgramID, courseID, publishedOnly, limit, offset)
}
func (s *Service) Update(ctx context.Context, id int64, input domain.UpdateModuleInput) (domain.Module, error) {

View File

@ -34,7 +34,7 @@ func (s *Service) GetByID(ctx context.Context, id int64) (domain.Program, error)
return p, nil
}
func (s *Service) List(ctx context.Context, limit, offset int32) ([]domain.Program, int64, error) {
func (s *Service) List(ctx context.Context, publishedOnly bool, limit, offset int32) ([]domain.Program, int64, error) {
if limit <= 0 {
limit = 20
}
@ -44,7 +44,7 @@ func (s *Service) List(ctx context.Context, limit, offset int32) ([]domain.Progr
if offset < 0 {
offset = 0
}
return s.store.ListPrograms(ctx, limit, offset)
return s.store.ListPrograms(ctx, publishedOnly, limit, offset)
}
func (s *Service) Update(ctx context.Context, id int64, input domain.UpdateProgramInput) (domain.Program, error) {

View File

@ -44,6 +44,12 @@ func (s *Service) ListQuestionTypeDefinitions(ctx context.Context, status *strin
return s.questionStore.ListQuestionTypeDefinitions(ctx, status, includeSystem, limit, offset)
}
func (s *Service) ActiveQuestionTypeDefinitionCatalog(ctx context.Context) ([]domain.QuestionTypeDefinition, error) {
active := "ACTIVE"
catalog, _, err := s.questionStore.ListQuestionTypeDefinitions(ctx, &active, true, 0, 0)
return catalog, err
}
func (s *Service) UpdateQuestionTypeDefinition(ctx context.Context, id int64, input domain.UpdateQuestionTypeDefinitionInput) error {
return s.questionStore.UpdateQuestionTypeDefinition(ctx, id, input)
}

View File

@ -0,0 +1,45 @@
package handlers
import (
"Yimaru-Backend/internal/domain"
"github.com/gofiber/fiber/v2"
)
// Manage helpers for hierarchy publish gating: staff who can create or update an entity see
// drafts; everyone else only sees PUBLISHED rows (mirrors canManageLessons / practices).
func (h *Handler) canManagePrograms(c *fiber.Ctx) bool {
rn := string(c.Locals("role").(domain.Role))
return h.rbacSvc.HasPermission(rn, "programs.create") || h.rbacSvc.HasPermission(rn, "programs.update")
}
func (h *Handler) canManageLMSCourses(c *fiber.Ctx) bool {
rn := string(c.Locals("role").(domain.Role))
return h.rbacSvc.HasPermission(rn, "courses.create") || h.rbacSvc.HasPermission(rn, "courses.update")
}
func (h *Handler) canManageLMSModules(c *fiber.Ctx) bool {
rn := string(c.Locals("role").(domain.Role))
return h.rbacSvc.HasPermission(rn, "modules.create") || h.rbacSvc.HasPermission(rn, "modules.update")
}
func (h *Handler) canManageExamPrepCatalogCourses(c *fiber.Ctx) bool {
rn := string(c.Locals("role").(domain.Role))
return h.rbacSvc.HasPermission(rn, "exam_prep.catalog_courses.create") || h.rbacSvc.HasPermission(rn, "exam_prep.catalog_courses.update")
}
func (h *Handler) canManageExamPrepUnits(c *fiber.Ctx) bool {
rn := string(c.Locals("role").(domain.Role))
return h.rbacSvc.HasPermission(rn, "exam_prep.units.create") || h.rbacSvc.HasPermission(rn, "exam_prep.units.update")
}
func (h *Handler) canManageExamPrepModules(c *fiber.Ctx) bool {
rn := string(c.Locals("role").(domain.Role))
return h.rbacSvc.HasPermission(rn, "exam_prep.modules.create") || h.rbacSvc.HasPermission(rn, "exam_prep.modules.update")
}
func (h *Handler) canManageExamPrepLessons(c *fiber.Ctx) bool {
rn := string(c.Locals("role").(domain.Role))
return h.rbacSvc.HasPermission(rn, "exam_prep.lessons.create") || h.rbacSvc.HasPermission(rn, "exam_prep.lessons.update")
}

View File

@ -92,7 +92,18 @@ func (h *Handler) ListCoursesByProgram(c *fiber.Ctx) error {
}
limit, _ := strconv.Atoi(c.Query("limit", "20"))
offset, _ := strconv.Atoi(c.Query("offset", "0"))
items, total, err := h.courseSvc.ListByProgram(c.Context(), programID, int32(limit), int32(offset))
publishedOnly := !h.canManageLMSCourses(c)
if publishedOnly {
// Draft programs hide their courses from non-managers.
p, err := h.programSvc.GetByID(c.Context(), programID)
if err == nil && !p.VisibleToLearners() {
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
Message: "Program not found",
Error: programs.ErrProgramNotFound.Error(),
})
}
}
items, total, err := h.courseSvc.ListByProgram(c.Context(), programID, publishedOnly, int32(limit), int32(offset))
if err != nil {
if errors.Is(err, programs.ErrProgramNotFound) {
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
@ -157,6 +168,12 @@ func (h *Handler) GetCourse(c *fiber.Ctx) error {
Error: err.Error(),
})
}
if !course.VisibleToLearners() && !h.canManageLMSCourses(c) {
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
Message: "Course not found",
Error: courses.ErrCourseNotFound.Error(),
})
}
uid := c.Locals("user_id").(int64)
role := c.Locals("role").(domain.Role)
if err := h.lmsProgressSvc.ApplyAccessCourse(c.Context(), role, uid, &course); err != nil {

View File

@ -59,6 +59,7 @@ func (h *Handler) CreateExamPrepCatalogCourse(c *fiber.Ctx) error {
func (h *Handler) ListExamPrepCatalogCourses(c *fiber.Ctx) error {
limit, _ := strconv.Atoi(c.Query("limit", "20"))
offset, _ := strconv.Atoi(c.Query("offset", "0"))
publishedOnly := !h.canManageExamPrepCatalogCourses(c)
role, _ := c.Locals("role").(domain.Role)
if role.IsCustomerLearnerRole() && !domain.CategorySubscriptionGateDisabled {
@ -84,7 +85,7 @@ func (h *Handler) ListExamPrepCatalogCourses(c *fiber.Ctx) error {
})
}
allItems, _, err := h.examPrepSvc.ListCatalogCourses(c.Context(), 200, 0)
allItems, _, err := h.examPrepSvc.ListCatalogCourses(c.Context(), publishedOnly, 200, 0)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to list catalog courses",
@ -137,7 +138,7 @@ func (h *Handler) ListExamPrepCatalogCourses(c *fiber.Ctx) error {
})
}
items, total, err := h.examPrepSvc.ListCatalogCourses(c.Context(), int32(limit), int32(offset))
items, total, err := h.examPrepSvc.ListCatalogCourses(c.Context(), publishedOnly, int32(limit), int32(offset))
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to list catalog courses",
@ -232,6 +233,12 @@ func (h *Handler) GetExamPrepCatalogCourseByID(c *fiber.Ctx) error {
Error: err.Error(),
})
}
if !out.VisibleToLearners() && !h.canManageExamPrepCatalogCourses(c) {
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
Message: "Catalog course not found",
Error: examprep.ErrCatalogCourseNotFound.Error(),
})
}
if err := h.applyExamPrepAccessCatalogCourse(c.Context(), c, &out); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to build catalog course",

View File

@ -74,7 +74,18 @@ func (h *Handler) ListExamPrepLessonsByUnitModule(c *fiber.Ctx) error {
}
limit, _ := strconv.Atoi(c.Query("limit", "20"))
offset, _ := strconv.Atoi(c.Query("offset", "0"))
items, total, err := h.examPrepSvc.ListLessonsByUnitModule(c.Context(), moduleID, int32(limit), int32(offset))
publishedOnly := !h.canManageExamPrepLessons(c)
if publishedOnly {
// Draft modules hide their lessons from non-managers.
m, err := h.examPrepSvc.GetModuleByID(c.Context(), moduleID)
if err == nil && !m.VisibleToLearners() {
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
Message: "Module not found",
Error: examprep.ErrModuleNotFound.Error(),
})
}
}
items, total, err := h.examPrepSvc.ListLessonsByUnitModule(c.Context(), moduleID, publishedOnly, int32(limit), int32(offset))
if err != nil {
if errors.Is(err, examprep.ErrModuleNotFound) {
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
@ -181,6 +192,12 @@ func (h *Handler) GetExamPrepLessonByID(c *fiber.Ctx) error {
Error: err.Error(),
})
}
if !les.VisibleToLearners() && !h.canManageExamPrepLessons(c) {
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
Message: "Lesson not found",
Error: examprep.ErrLessonNotFound.Error(),
})
}
if err := h.applyExamPrepAccessLesson(c.Context(), c, &les); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to build lesson",

View File

@ -72,7 +72,18 @@ func (h *Handler) ListExamPrepModulesByUnit(c *fiber.Ctx) error {
}
limit, _ := strconv.Atoi(c.Query("limit", "20"))
offset, _ := strconv.Atoi(c.Query("offset", "0"))
items, total, err := h.examPrepSvc.ListModulesByUnit(c.Context(), unitID, int32(limit), int32(offset))
publishedOnly := !h.canManageExamPrepModules(c)
if publishedOnly {
// Draft units hide their modules from non-managers.
u, err := h.examPrepSvc.GetUnitByID(c.Context(), unitID)
if err == nil && !u.VisibleToLearners() {
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
Message: "Unit not found",
Error: examprep.ErrUnitNotFound.Error(),
})
}
}
items, total, err := h.examPrepSvc.ListModulesByUnit(c.Context(), unitID, publishedOnly, int32(limit), int32(offset))
if err != nil {
if errors.Is(err, examprep.ErrUnitNotFound) {
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
@ -181,6 +192,12 @@ func (h *Handler) GetExamPrepModuleByID(c *fiber.Ctx) error {
Error: err.Error(),
})
}
if !out.VisibleToLearners() && !h.canManageExamPrepModules(c) {
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
Message: "Module not found",
Error: examprep.ErrModuleNotFound.Error(),
})
}
if err := h.applyExamPrepAccessModule(c.Context(), c, &out); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to build module",

View File

@ -79,7 +79,18 @@ func (h *Handler) ListExamPrepUnitsByCatalogCourse(c *fiber.Ctx) error {
}
limit, _ := strconv.Atoi(c.Query("limit", "20"))
offset, _ := strconv.Atoi(c.Query("offset", "0"))
items, total, err := h.examPrepSvc.ListUnitsByCatalogCourse(c.Context(), catalogCourseID, int32(limit), int32(offset))
publishedOnly := !h.canManageExamPrepUnits(c)
if publishedOnly {
// Draft catalog courses hide their units from non-managers.
cc, err := h.examPrepSvc.GetCatalogCourseByID(c.Context(), catalogCourseID)
if err == nil && !cc.VisibleToLearners() {
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
Message: "Catalog course not found",
Error: examprep.ErrCatalogCourseNotFound.Error(),
})
}
}
items, total, err := h.examPrepSvc.ListUnitsByCatalogCourse(c.Context(), catalogCourseID, publishedOnly, int32(limit), int32(offset))
if err != nil {
if errors.Is(err, examprep.ErrCatalogCourseNotFound) {
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
@ -189,6 +200,12 @@ func (h *Handler) GetExamPrepUnitByID(c *fiber.Ctx) error {
Error: err.Error(),
})
}
if !out.VisibleToLearners() && !h.canManageExamPrepUnits(c) {
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
Message: "Unit not found",
Error: examprep.ErrUnitNotFound.Error(),
})
}
if err := h.applyExamPrepAccessUnit(c.Context(), c, &out); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to build unit",

View File

@ -171,7 +171,7 @@ func (h *Handler) AdminGetUserRecentActivity(c *fiber.Ctx) error {
}
func (h *Handler) buildLMSProgressSummary(ctx context.Context, role domain.Role, userID int64, publishedOnly bool) (domain.LMSProgressSummary, error) {
programs, err := h.listAllPrograms(ctx)
programs, err := h.listAllPrograms(ctx, publishedOnly)
if err != nil {
return domain.LMSProgressSummary{}, err
}
@ -183,7 +183,7 @@ func (h *Handler) buildLMSProgressSummary(ctx context.Context, role domain.Role,
if err := h.lmsProgressSvc.ApplyAccessProgram(ctx, role, userID, &programs[i]); err != nil {
return domain.LMSProgressSummary{}, err
}
courses, err := h.listAllCoursesByProgram(ctx, programs[i].ID)
courses, err := h.listAllCoursesByProgram(ctx, programs[i].ID, publishedOnly)
if err != nil {
return domain.LMSProgressSummary{}, err
}
@ -198,7 +198,7 @@ func (h *Handler) buildLMSProgressSummary(ctx context.Context, role domain.Role,
if err := h.lmsProgressSvc.ApplyAccessCourse(ctx, role, userID, &courses[j]); err != nil {
return domain.LMSProgressSummary{}, err
}
modules, err := h.listAllModulesByCourse(ctx, courses[j].ID)
modules, err := h.listAllModulesByCourse(ctx, courses[j].ID, publishedOnly)
if err != nil {
return domain.LMSProgressSummary{}, err
}
@ -251,13 +251,13 @@ func (h *Handler) buildLMSProgressSummary(ctx context.Context, role domain.Role,
return summary, nil
}
func (h *Handler) listAllPrograms(ctx context.Context) ([]domain.Program, error) {
func (h *Handler) listAllPrograms(ctx context.Context, publishedOnly bool) ([]domain.Program, error) {
var (
all []domain.Program
offset int32
)
for {
items, total, err := h.programSvc.List(ctx, lmsProgressSummaryPageSize, offset)
items, total, err := h.programSvc.List(ctx, publishedOnly, lmsProgressSummaryPageSize, offset)
if err != nil {
return nil, err
}
@ -272,13 +272,13 @@ func (h *Handler) listAllPrograms(ctx context.Context) ([]domain.Program, error)
}
}
func (h *Handler) listAllCoursesByProgram(ctx context.Context, programID int64) ([]domain.Course, error) {
func (h *Handler) listAllCoursesByProgram(ctx context.Context, programID int64, publishedOnly bool) ([]domain.Course, error) {
var (
all []domain.Course
offset int32
)
for {
items, total, err := h.courseSvc.ListByProgram(ctx, programID, lmsProgressSummaryPageSize, offset)
items, total, err := h.courseSvc.ListByProgram(ctx, programID, publishedOnly, lmsProgressSummaryPageSize, offset)
if err != nil {
return nil, err
}
@ -293,13 +293,13 @@ func (h *Handler) listAllCoursesByProgram(ctx context.Context, programID int64)
}
}
func (h *Handler) listAllModulesByCourse(ctx context.Context, courseID int64) ([]domain.Module, error) {
func (h *Handler) listAllModulesByCourse(ctx context.Context, courseID int64, publishedOnly bool) ([]domain.Module, error) {
var (
all []domain.Module
offset int32
)
for {
items, total, err := h.moduleSvc.ListByCourse(ctx, courseID, lmsProgressSummaryPageSize, offset)
items, total, err := h.moduleSvc.ListByCourse(ctx, courseID, publishedOnly, lmsProgressSummaryPageSize, offset)
if err != nil {
return nil, err
}

View File

@ -90,7 +90,18 @@ func (h *Handler) ListModulesByCourse(c *fiber.Ctx) error {
}
limit, _ := strconv.Atoi(c.Query("limit", "20"))
offset, _ := strconv.Atoi(c.Query("offset", "0"))
items, total, err := h.moduleSvc.ListByCourse(c.Context(), courseID, int32(limit), int32(offset))
publishedOnly := !h.canManageLMSModules(c)
if publishedOnly {
// Draft courses hide their modules from non-managers.
course, err := h.courseSvc.GetByID(c.Context(), courseID)
if err == nil && !course.VisibleToLearners() {
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
Message: "Course not found",
Error: courses.ErrCourseNotFound.Error(),
})
}
}
items, total, err := h.moduleSvc.ListByCourse(c.Context(), courseID, publishedOnly, int32(limit), int32(offset))
if err != nil {
if errors.Is(err, courses.ErrCourseNotFound) {
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
@ -152,6 +163,12 @@ func (h *Handler) GetModule(c *fiber.Ctx) error {
Error: err.Error(),
})
}
if !mod.VisibleToLearners() && !h.canManageLMSModules(c) {
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
Message: "Module not found",
Error: modules.ErrModuleNotFound.Error(),
})
}
uid := c.Locals("user_id").(int64)
role := c.Locals("role").(domain.Role)
if err := h.lmsProgressSvc.ApplyAccessModule(c.Context(), role, uid, &mod); err != nil {

View File

@ -9,61 +9,19 @@ import (
"github.com/gofiber/fiber/v2"
)
func fullUpdatePracticeQuestionResponses(questions []domain.QuestionWithDetails) []questionRes {
func fullUpdatePracticeQuestionResponses(questions []domain.QuestionWithDetails, catalog []domain.QuestionTypeDefinition) []questionRes {
out := make([]questionRes, 0, len(questions))
for _, question := range questions {
options := make([]optionRes, 0, len(question.Options))
for _, opt := range question.Options {
options = append(options, optionRes{
ID: opt.ID,
OptionText: opt.OptionText,
OptionOrder: opt.OptionOrder,
IsCorrect: opt.IsCorrect,
})
}
shortAnswers := make([]shortAnswerRes, 0, len(question.ShortAnswers))
for _, sa := range question.ShortAnswers {
shortAnswers = append(shortAnswers, shortAnswerRes{
ID: sa.ID,
AcceptableAnswer: sa.AcceptableAnswer,
MatchType: sa.MatchType,
})
}
var audioCorrectAnswerText *string
if question.AudioAnswer != nil {
audioCorrectAnswerText = &question.AudioAnswer.CorrectAnswerText
}
out = append(out, questionRes{
ID: question.ID,
QuestionText: questionTextField(question.QuestionType, question.QuestionText),
QuestionType: question.QuestionType,
QuestionTypeDefinitionID: question.QuestionTypeDefinitionID,
DynamicPayload: question.DynamicPayload,
DifficultyLevel: question.DifficultyLevel,
Points: question.Points,
Explanation: question.Explanation,
Tips: question.Tips,
VoicePrompt: question.VoicePrompt,
SampleAnswerVoicePrompt: question.SampleAnswerVoicePrompt,
ImageURL: question.ImageURL,
Status: question.Status,
CreatedAt: question.CreatedAt.String(),
Options: options,
ShortAnswers: shortAnswers,
AudioCorrectAnswerText: audioCorrectAnswerText,
})
out = append(out, buildQuestionRes(question, catalog))
}
return out
}
func fullUpdatePracticeResponse(result domain.FullUpdatePracticeResult) fiber.Map {
func fullUpdatePracticeResponse(result domain.FullUpdatePracticeResult, catalog []domain.QuestionTypeDefinition) fiber.Map {
return fiber.Map{
"practice": result.Practice,
"question_set": questionSetResFromDomain(result.QuestionSet),
"questions": fullUpdatePracticeQuestionResponses(result.Questions),
"questions": fullUpdatePracticeQuestionResponses(result.Questions, catalog),
}
}
@ -141,9 +99,17 @@ func (h *Handler) UpdateLmsPracticeFull(c *fiber.Ctx) error {
})
}
catalog, err := h.activeQuestionTypeDefinitionCatalog(c.Context())
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to load question type catalog",
Error: err.Error(),
})
}
return c.JSON(domain.Response{
Message: "Practice updated successfully",
Data: fullUpdatePracticeResponse(result),
Data: fullUpdatePracticeResponse(result, catalog),
Success: true,
StatusCode: fiber.StatusOK,
})
@ -187,9 +153,17 @@ func (h *Handler) UpdateExamPrepPracticeFull(c *fiber.Ctx) error {
})
}
catalog, err := h.activeQuestionTypeDefinitionCatalog(c.Context())
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to load question type catalog",
Error: err.Error(),
})
}
return c.JSON(domain.Response{
Message: "Practice updated successfully",
Data: fullUpdatePracticeResponse(result),
Data: fullUpdatePracticeResponse(result, catalog),
Success: true,
StatusCode: fiber.StatusOK,
})

View File

@ -69,7 +69,8 @@ func (h *Handler) CreateProgram(c *fiber.Ctx) error {
func (h *Handler) ListPrograms(c *fiber.Ctx) error {
limit, _ := strconv.Atoi(c.Query("limit", "20"))
offset, _ := strconv.Atoi(c.Query("offset", "0"))
items, total, err := h.programSvc.List(c.Context(), int32(limit), int32(offset))
publishedOnly := !h.canManagePrograms(c)
items, total, err := h.programSvc.List(c.Context(), publishedOnly, int32(limit), int32(offset))
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to list programs",
@ -128,6 +129,12 @@ func (h *Handler) GetProgram(c *fiber.Ctx) error {
Error: err.Error(),
})
}
if !p.VisibleToLearners() && !h.canManagePrograms(c) {
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
Message: "Program not found",
Error: programs.ErrProgramNotFound.Error(),
})
}
uid := c.Locals("user_id").(int64)
role := c.Locals("role").(domain.Role)
if err := h.lmsProgressSvc.ApplyAccessProgram(c.Context(), role, uid, &p); err != nil {

View File

@ -81,6 +81,65 @@ type listQuestionsRes struct {
TotalCount int64 `json:"total_count"`
}
func (h *Handler) activeQuestionTypeDefinitionCatalog(ctx context.Context) ([]domain.QuestionTypeDefinition, error) {
return h.questionsSvc.ActiveQuestionTypeDefinitionCatalog(ctx)
}
func buildQuestionRes(q domain.QuestionWithDetails, catalog []domain.QuestionTypeDefinition) questionRes {
options := make([]optionRes, 0, len(q.Options))
for _, opt := range q.Options {
options = append(options, optionRes{
ID: opt.ID,
OptionText: opt.OptionText,
OptionOrder: opt.OptionOrder,
IsCorrect: opt.IsCorrect,
})
}
shortAnswers := make([]shortAnswerRes, 0, len(q.ShortAnswers))
for _, sa := range q.ShortAnswers {
shortAnswers = append(shortAnswers, shortAnswerRes{
ID: sa.ID,
AcceptableAnswer: sa.AcceptableAnswer,
MatchType: sa.MatchType,
})
}
var audioCorrectAnswerText *string
if q.AudioAnswer != nil {
audioCorrectAnswerText = &q.AudioAnswer.CorrectAnswerText
}
return questionRes{
ID: q.ID,
QuestionText: questionTextField(q.QuestionType, q.QuestionText),
QuestionType: q.QuestionType,
QuestionTypeDefinitionID: domain.ResolveEffectiveQuestionTypeDefinitionID(
q.QuestionTypeDefinitionID,
q.QuestionType,
catalog,
q.DynamicPayload,
),
DynamicPayload: q.DynamicPayload,
DifficultyLevel: q.DifficultyLevel,
Points: q.Points,
Explanation: q.Explanation,
Tips: q.Tips,
VoicePrompt: q.VoicePrompt,
SampleAnswerVoicePrompt: q.SampleAnswerVoicePrompt,
ImageURL: q.ImageURL,
Status: q.Status,
CreatedAt: q.CreatedAt.String(),
Options: options,
ShortAnswers: shortAnswers,
AudioCorrectAnswerText: audioCorrectAnswerText,
}
}
func buildQuestionResFromQuestion(q domain.Question, catalog []domain.QuestionTypeDefinition) questionRes {
return buildQuestionRes(domain.QuestionWithDetails{Question: q}, catalog)
}
func resolveQuestionTypeFromDefinition(def domain.QuestionTypeDefinition) string {
return domain.ResolveRuntimeQuestionTypeFromDefinition(def.Key, def.ResponseComponentKinds)
}
@ -264,24 +323,17 @@ func (h *Handler) CreateQuestion(c *fiber.Ctx) error {
})
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionQuestionCreated, domain.ResourceQuestion, &question.ID, "Created question: "+activityLogQuestionSummary(question.QuestionType, question.QuestionText, question.DynamicPayload), meta, &ip, &ua)
catalog, err := h.activeQuestionTypeDefinitionCatalog(c.Context())
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to load question type catalog",
Error: err.Error(),
})
}
return c.Status(fiber.StatusCreated).JSON(domain.Response{
Message: "Question created successfully",
Data: questionRes{
ID: question.ID,
QuestionText: questionTextField(question.QuestionType, question.QuestionText),
QuestionType: question.QuestionType,
QuestionTypeDefinitionID: question.QuestionTypeDefinitionID,
DynamicPayload: question.DynamicPayload,
DifficultyLevel: question.DifficultyLevel,
Points: question.Points,
Explanation: question.Explanation,
Tips: question.Tips,
VoicePrompt: question.VoicePrompt,
SampleAnswerVoicePrompt: question.SampleAnswerVoicePrompt,
ImageURL: question.ImageURL,
Status: question.Status,
CreatedAt: question.CreatedAt.String(),
},
Data: buildQuestionResFromQuestion(question, catalog),
})
}
@ -313,51 +365,17 @@ func (h *Handler) GetQuestionByID(c *fiber.Ctx) error {
})
}
var options []optionRes
for _, opt := range question.Options {
options = append(options, optionRes{
ID: opt.ID,
OptionText: opt.OptionText,
OptionOrder: opt.OptionOrder,
IsCorrect: opt.IsCorrect,
catalog, err := h.activeQuestionTypeDefinitionCatalog(c.Context())
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to load question type catalog",
Error: err.Error(),
})
}
var shortAnswers []shortAnswerRes
for _, sa := range question.ShortAnswers {
shortAnswers = append(shortAnswers, shortAnswerRes{
ID: sa.ID,
AcceptableAnswer: sa.AcceptableAnswer,
MatchType: sa.MatchType,
})
}
var audioCorrectAnswerText *string
if question.AudioAnswer != nil {
audioCorrectAnswerText = &question.AudioAnswer.CorrectAnswerText
}
return c.JSON(domain.Response{
Message: "Question retrieved successfully",
Data: questionRes{
ID: question.ID,
QuestionText: questionTextField(question.QuestionType, question.QuestionText),
QuestionType: question.QuestionType,
QuestionTypeDefinitionID: question.QuestionTypeDefinitionID,
DynamicPayload: question.DynamicPayload,
DifficultyLevel: question.DifficultyLevel,
Points: question.Points,
Explanation: question.Explanation,
Tips: question.Tips,
VoicePrompt: question.VoicePrompt,
SampleAnswerVoicePrompt: question.SampleAnswerVoicePrompt,
ImageURL: question.ImageURL,
Status: question.Status,
CreatedAt: question.CreatedAt.String(),
Options: options,
ShortAnswers: shortAnswers,
AudioCorrectAnswerText: audioCorrectAnswerText,
},
Data: buildQuestionRes(question, catalog),
})
}
@ -404,24 +422,19 @@ func (h *Handler) ListQuestions(c *fiber.Ctx) error {
})
}
var questionResponses []questionRes
for _, q := range questions {
questionResponses = append(questionResponses, questionRes{
ID: q.ID,
QuestionText: questionTextField(q.QuestionType, q.QuestionText),
QuestionType: q.QuestionType,
QuestionTypeDefinitionID: q.QuestionTypeDefinitionID,
DynamicPayload: q.DynamicPayload,
DifficultyLevel: q.DifficultyLevel,
Points: q.Points,
Explanation: q.Explanation,
Tips: q.Tips,
VoicePrompt: q.VoicePrompt,
Status: q.Status,
CreatedAt: q.CreatedAt.String(),
catalog, err := h.activeQuestionTypeDefinitionCatalog(c.Context())
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to load question type catalog",
Error: err.Error(),
})
}
questionResponses := make([]questionRes, 0, len(questions))
for _, q := range questions {
questionResponses = append(questionResponses, buildQuestionResFromQuestion(q, catalog))
}
return c.JSON(domain.Response{
Message: "Questions retrieved successfully",
Data: listQuestionsRes{
@ -464,21 +477,19 @@ func (h *Handler) SearchQuestions(c *fiber.Ctx) error {
})
}
var questionResponses []questionRes
for _, q := range questions {
questionResponses = append(questionResponses, questionRes{
ID: q.ID,
QuestionText: questionTextField(q.QuestionType, q.QuestionText),
QuestionType: q.QuestionType,
QuestionTypeDefinitionID: q.QuestionTypeDefinitionID,
DynamicPayload: q.DynamicPayload,
DifficultyLevel: q.DifficultyLevel,
Points: q.Points,
Status: q.Status,
CreatedAt: q.CreatedAt.String(),
catalog, err := h.activeQuestionTypeDefinitionCatalog(c.Context())
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to load question type catalog",
Error: err.Error(),
})
}
questionResponses := make([]questionRes, 0, len(questions))
for _, q := range questions {
questionResponses = append(questionResponses, buildQuestionResFromQuestion(q, catalog))
}
return c.JSON(domain.Response{
Message: "Questions retrieved successfully",
Data: listQuestionsRes{
@ -1458,6 +1469,14 @@ func (h *Handler) GetQuestionsInSet(c *fiber.Ctx) error {
})
}
catalog, err := h.activeQuestionTypeDefinitionCatalog(c.Context())
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to load question type catalog",
Error: err.Error(),
})
}
questionResponses := make([]questionRes, 0, len(items))
for _, item := range items {
question, err := h.questionsSvc.GetQuestionWithDetails(c.Context(), item.QuestionID)
@ -1468,49 +1487,7 @@ func (h *Handler) GetQuestionsInSet(c *fiber.Ctx) error {
})
}
options := make([]optionRes, 0, len(question.Options))
for _, opt := range question.Options {
options = append(options, optionRes{
ID: opt.ID,
OptionText: opt.OptionText,
OptionOrder: opt.OptionOrder,
IsCorrect: opt.IsCorrect,
})
}
shortAnswers := make([]shortAnswerRes, 0, len(question.ShortAnswers))
for _, sa := range question.ShortAnswers {
shortAnswers = append(shortAnswers, shortAnswerRes{
ID: sa.ID,
AcceptableAnswer: sa.AcceptableAnswer,
MatchType: sa.MatchType,
})
}
var audioCorrectAnswerText *string
if question.AudioAnswer != nil {
audioCorrectAnswerText = &question.AudioAnswer.CorrectAnswerText
}
questionResponses = append(questionResponses, questionRes{
ID: question.ID,
QuestionText: questionTextField(question.QuestionType, question.QuestionText),
QuestionType: question.QuestionType,
QuestionTypeDefinitionID: question.QuestionTypeDefinitionID,
DynamicPayload: question.DynamicPayload,
DifficultyLevel: question.DifficultyLevel,
Points: question.Points,
Explanation: question.Explanation,
Tips: question.Tips,
VoicePrompt: question.VoicePrompt,
SampleAnswerVoicePrompt: question.SampleAnswerVoicePrompt,
ImageURL: question.ImageURL,
Status: question.Status,
CreatedAt: question.CreatedAt.String(),
Options: options,
ShortAnswers: shortAnswers,
AudioCorrectAnswerText: audioCorrectAnswerText,
})
questionResponses = append(questionResponses, buildQuestionRes(question, catalog))
}
return c.JSON(domain.Response{