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,16 +21,18 @@ 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 {
Name string `json:"name"`
Description pgtype.Text `json:"description"`
Category string `json:"category"`
Thumbnail pgtype.Text `json:"thumbnail"`
Name string `json:"name"`
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,21 +78,26 @@ 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
`
type ExamPrepGetCatalogCourseByIDRow struct {
ID int64 `json:"id"`
Name string `json:"name"`
Description pgtype.Text `json:"description"`
Thumbnail pgtype.Text `json:"thumbnail"`
SortOrder int32 `json:"sort_order"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
Category string `json:"category"`
HasPractice bool `json:"has_practice"`
ID int64 `json:"id"`
Name string `json:"name"`
Description pgtype.Text `json:"description"`
Thumbnail pgtype.Text `json:"thumbnail"`
SortOrder int32 `json:"sort_order"`
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"`
}
func (q *Queries) ExamPrepGetCatalogCourseByID(ctx context.Context, id int64) (ExamPrepGetCatalogCourseByIDRow, error) {
@ -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,38 +179,48 @@ 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
`
type ExamPrepListCatalogCoursesParams struct {
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
PublishedOnly bool `json:"published_only"`
}
type ExamPrepListCatalogCoursesRow struct {
TotalCount int64 `json:"total_count"`
ID int64 `json:"id"`
Name string `json:"name"`
Description pgtype.Text `json:"description"`
Category string `json:"category"`
Thumbnail pgtype.Text `json:"thumbnail"`
SortOrder int32 `json:"sort_order"`
UnitsCount int64 `json:"units_count"`
ModulesCount int64 `json:"modules_count"`
LessonsCount int64 `json:"lessons_count"`
HasPractice bool `json:"has_practice"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
TotalCount int64 `json:"total_count"`
ID int64 `json:"id"`
Name string `json:"name"`
Description pgtype.Text `json:"description"`
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"`
HasPractice bool `json:"has_practice"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
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,19 +262,21 @@ 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 {
Name pgtype.Text `json:"name"`
Description pgtype.Text `json:"description"`
Category pgtype.Text `json:"category"`
Thumbnail pgtype.Text `json:"thumbnail"`
SortOrder pgtype.Int4 `json:"sort_order"`
ID int64 `json:"id"`
Name pgtype.Text `json:"name"`
Description pgtype.Text `json:"description"`
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"`
}
func (q *Queries) ExamPrepUpdateCatalogCourse(ctx context.Context, arg ExamPrepUpdateCatalogCourseParams) (ExamPrepCatalogCourse, error) {
@ -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,17 +24,19 @@ 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 {
UnitModuleID int64 `json:"unit_module_id"`
Title string `json:"title"`
VideoUrl pgtype.Text `json:"video_url"`
Thumbnail pgtype.Text `json:"thumbnail"`
Description pgtype.Text `json:"description"`
UnitModuleID int64 `json:"unit_module_id"`
Title string `json:"title"`
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,27 +76,29 @@ 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
`
type ExamPrepGetUnitModuleLessonByIDRow struct {
ID int64 `json:"id"`
UnitModuleID int64 `json:"unit_module_id"`
Title string `json:"title"`
VideoUrl pgtype.Text `json:"video_url"`
Thumbnail pgtype.Text `json:"thumbnail"`
Description pgtype.Text `json:"description"`
SortOrder int32 `json:"sort_order"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
HasPractice bool `json:"has_practice"`
ID int64 `json:"id"`
UnitModuleID int64 `json:"unit_module_id"`
Title string `json:"title"`
VideoUrl pgtype.Text `json:"video_url"`
Thumbnail pgtype.Text `json:"thumbnail"`
Description pgtype.Text `json:"description"`
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"`
}
func (q *Queries) ExamPrepGetUnitModuleLessonByID(ctx context.Context, id int64) (ExamPrepGetUnitModuleLessonByIDRow, error) {
@ -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
@ -171,27 +216,34 @@ OFFSET $3
`
type ExamPrepListUnitModuleLessonsByUnitModuleIDParams struct {
UnitModuleID int64 `json:"unit_module_id"`
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
UnitModuleID int64 `json:"unit_module_id"`
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
PublishedOnly bool `json:"published_only"`
}
type ExamPrepListUnitModuleLessonsByUnitModuleIDRow struct {
TotalCount int64 `json:"total_count"`
ID int64 `json:"id"`
UnitModuleID int64 `json:"unit_module_id"`
Title string `json:"title"`
VideoUrl pgtype.Text `json:"video_url"`
Thumbnail pgtype.Text `json:"thumbnail"`
Description pgtype.Text `json:"description"`
SortOrder int32 `json:"sort_order"`
HasPractice bool `json:"has_practice"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
TotalCount int64 `json:"total_count"`
ID int64 `json:"id"`
UnitModuleID int64 `json:"unit_module_id"`
Title string `json:"title"`
VideoUrl pgtype.Text `json:"video_url"`
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,19 +283,21 @@ 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 {
Title pgtype.Text `json:"title"`
VideoUrl pgtype.Text `json:"video_url"`
Thumbnail pgtype.Text `json:"thumbnail"`
Description pgtype.Text `json:"description"`
SortOrder pgtype.Int4 `json:"sort_order"`
ID int64 `json:"id"`
Title pgtype.Text `json:"title"`
VideoUrl pgtype.Text `json:"video_url"`
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"`
}
func (q *Queries) ExamPrepUpdateUnitModuleLesson(ctx context.Context, arg ExamPrepUpdateUnitModuleLessonParams) (ExamPrepUnitModuleLesson, error) {
@ -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,17 +24,19 @@ 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 {
UnitID int64 `json:"unit_id"`
Name string `json:"name"`
Description pgtype.Text `json:"description"`
Thumbnail pgtype.Text `json:"thumbnail"`
Icon pgtype.Text `json:"icon"`
UnitID int64 `json:"unit_id"`
Name string `json:"name"`
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,28 +76,31 @@ 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
`
type ExamPrepGetUnitModuleByIDRow struct {
ID int64 `json:"id"`
UnitID int64 `json:"unit_id"`
Name string `json:"name"`
Description pgtype.Text `json:"description"`
Thumbnail pgtype.Text `json:"thumbnail"`
Icon pgtype.Text `json:"icon"`
SortOrder int32 `json:"sort_order"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
HasPractice bool `json:"has_practice"`
ID int64 `json:"id"`
UnitID int64 `json:"unit_id"`
Name string `json:"name"`
Description pgtype.Text `json:"description"`
Thumbnail pgtype.Text `json:"thumbnail"`
Icon pgtype.Text `json:"icon"`
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"`
}
func (q *Queries) ExamPrepGetUnitModuleByID(ctx context.Context, id int64) (ExamPrepGetUnitModuleByIDRow, error) {
@ -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
@ -181,9 +228,10 @@ OFFSET $3
`
type ExamPrepListUnitModulesByUnitParams struct {
UnitID int64 `json:"unit_id"`
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
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,19 +299,21 @@ 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 {
Name pgtype.Text `json:"name"`
Description pgtype.Text `json:"description"`
Thumbnail pgtype.Text `json:"thumbnail"`
Icon pgtype.Text `json:"icon"`
SortOrder pgtype.Int4 `json:"sort_order"`
ID int64 `json:"id"`
Name pgtype.Text `json:"name"`
Description pgtype.Text `json:"description"`
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"`
}
func (q *Queries) ExamPrepUpdateUnitModule(ctx context.Context, arg ExamPrepUpdateUnitModuleParams) (ExamPrepUnitModule, error) {
@ -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,18 +300,20 @@ 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 {
Name pgtype.Text `json:"name"`
Description pgtype.Text `json:"description"`
Thumbnail pgtype.Text `json:"thumbnail"`
SortOrder pgtype.Int4 `json:"sort_order"`
ID int64 `json:"id"`
Name pgtype.Text `json:"name"`
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"`
}
func (q *Queries) ExamPrepUpdateUnit(ctx context.Context, arg ExamPrepUpdateUnitParams) (ExamPrepUnit, error) {
@ -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,17 +24,19 @@ 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 {
ProgramID int64 `json:"program_id"`
Name string `json:"name"`
Description pgtype.Text `json:"description"`
Thumbnail pgtype.Text `json:"thumbnail"`
SortOrder pgtype.Int4 `json:"sort_order"`
ProgramID int64 `json:"program_id"`
Name string `json:"name"`
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
@ -86,15 +90,16 @@ WHERE c.id = $1
`
type GetCourseByIDRow struct {
ID int64 `json:"id"`
ProgramID int64 `json:"program_id"`
Name string `json:"name"`
Description pgtype.Text `json:"description"`
Thumbnail pgtype.Text `json:"thumbnail"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
SortOrder int32 `json:"sort_order"`
HasPractice bool `json:"has_practice"`
ID int64 `json:"id"`
ProgramID int64 `json:"program_id"`
Name string `json:"name"`
Description pgtype.Text `json:"description"`
Thumbnail pgtype.Text `json:"thumbnail"`
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"`
}
func (q *Queries) GetCourseByID(ctx context.Context, id int64) (GetCourseByIDRow, error) {
@ -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
@ -203,9 +215,10 @@ LIMIT $2 OFFSET $3
`
type ListCoursesByProgramIDParams struct {
ProgramID int64 `json:"program_id"`
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
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,19 +318,21 @@ 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 {
Name pgtype.Text `json:"name"`
Description pgtype.Text `json:"description"`
Thumbnail pgtype.Text `json:"thumbnail"`
SortOrder pgtype.Int4 `json:"sort_order"`
ID int64 `json:"id"`
Name pgtype.Text `json:"name"`
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"`
}
func (q *Queries) UpdateCourse(ctx context.Context, arg UpdateCourseParams) (Course, error) {
@ -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,18 +25,20 @@ 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 {
ProgramID int64 `json:"program_id"`
CourseID int64 `json:"course_id"`
Name string `json:"name"`
Description pgtype.Text `json:"description"`
Icon pgtype.Text `json:"icon"`
SortOrder pgtype.Int4 `json:"sort_order"`
ProgramID int64 `json:"program_id"`
CourseID int64 `json:"course_id"`
Name string `json:"name"`
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
@ -89,16 +93,17 @@ WHERE m.id = $1
`
type GetModuleByIDRow struct {
ID int64 `json:"id"`
ProgramID int64 `json:"program_id"`
CourseID int64 `json:"course_id"`
Name string `json:"name"`
Description pgtype.Text `json:"description"`
Icon pgtype.Text `json:"icon"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
SortOrder int32 `json:"sort_order"`
HasPractice bool `json:"has_practice"`
ID int64 `json:"id"`
ProgramID int64 `json:"program_id"`
CourseID int64 `json:"course_id"`
Name string `json:"name"`
Description pgtype.Text `json:"description"`
Icon pgtype.Text `json:"icon"`
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"`
}
func (q *Queries) GetModuleByID(ctx context.Context, id int64) (GetModuleByIDRow, error) {
@ -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
@ -182,24 +193,26 @@ OFFSET $4
`
type ListModulesByProgramAndCourseParams struct {
ProgramID int64 `json:"program_id"`
CourseID int64 `json:"course_id"`
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
ProgramID int64 `json:"program_id"`
CourseID int64 `json:"course_id"`
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
PublishedOnly bool `json:"published_only"`
}
type ListModulesByProgramAndCourseRow struct {
TotalCount int64 `json:"total_count"`
ID int64 `json:"id"`
ProgramID int64 `json:"program_id"`
CourseID int64 `json:"course_id"`
Name string `json:"name"`
Description pgtype.Text `json:"description"`
Icon pgtype.Text `json:"icon"`
SortOrder int32 `json:"sort_order"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
HasPractice bool `json:"has_practice"`
TotalCount int64 `json:"total_count"`
ID int64 `json:"id"`
ProgramID int64 `json:"program_id"`
CourseID int64 `json:"course_id"`
Name string `json:"name"`
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"`
}
func (q *Queries) ListModulesByProgramAndCourse(ctx context.Context, arg ListModulesByProgramAndCourseParams) ([]ListModulesByProgramAndCourseRow, error) {
@ -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,19 +294,21 @@ 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 {
Name pgtype.Text `json:"name"`
Description pgtype.Text `json:"description"`
Icon pgtype.Text `json:"icon"`
SortOrder pgtype.Int4 `json:"sort_order"`
ID int64 `json:"id"`
Name pgtype.Text `json:"name"`
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"`
}
func (q *Queries) UpdateModule(ctx context.Context, arg UpdateModuleParams) (Module, error) {
@ -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

@ -23,14 +23,15 @@ type ActivityLog struct {
}
type Course struct {
ID int64 `json:"id"`
ProgramID int64 `json:"program_id"`
Name string `json:"name"`
Description pgtype.Text `json:"description"`
Thumbnail pgtype.Text `json:"thumbnail"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
SortOrder int32 `json:"sort_order"`
ID int64 `json:"id"`
ProgramID int64 `json:"program_id"`
Name string `json:"name"`
Description pgtype.Text `json:"description"`
Thumbnail pgtype.Text `json:"thumbnail"`
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 {
@ -58,14 +59,15 @@ type EmailTemplate struct {
}
type ExamPrepCatalogCourse struct {
ID int64 `json:"id"`
Name string `json:"name"`
Description pgtype.Text `json:"description"`
Thumbnail pgtype.Text `json:"thumbnail"`
SortOrder int32 `json:"sort_order"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
Category string `json:"category"`
ID int64 `json:"id"`
Name string `json:"name"`
Description pgtype.Text `json:"description"`
Thumbnail pgtype.Text `json:"thumbnail"`
SortOrder int32 `json:"sort_order"`
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,30 +93,33 @@ 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 {
ID int64 `json:"id"`
UnitID int64 `json:"unit_id"`
Name string `json:"name"`
Description pgtype.Text `json:"description"`
Thumbnail pgtype.Text `json:"thumbnail"`
Icon pgtype.Text `json:"icon"`
SortOrder int32 `json:"sort_order"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
ID int64 `json:"id"`
UnitID int64 `json:"unit_id"`
Name string `json:"name"`
Description pgtype.Text `json:"description"`
Thumbnail pgtype.Text `json:"thumbnail"`
Icon pgtype.Text `json:"icon"`
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 {
ID int64 `json:"id"`
UnitModuleID int64 `json:"unit_module_id"`
Title string `json:"title"`
VideoUrl pgtype.Text `json:"video_url"`
Thumbnail pgtype.Text `json:"thumbnail"`
Description pgtype.Text `json:"description"`
SortOrder int32 `json:"sort_order"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
ID int64 `json:"id"`
UnitModuleID int64 `json:"unit_module_id"`
Title string `json:"title"`
VideoUrl pgtype.Text `json:"video_url"`
Thumbnail pgtype.Text `json:"thumbnail"`
Description pgtype.Text `json:"description"`
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 {
@ -230,15 +235,16 @@ type MobileAppVersion struct {
}
type Module struct {
ID int64 `json:"id"`
ProgramID int64 `json:"program_id"`
CourseID int64 `json:"course_id"`
Name string `json:"name"`
Description pgtype.Text `json:"description"`
Icon pgtype.Text `json:"icon"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
SortOrder int32 `json:"sort_order"`
ID int64 `json:"id"`
ProgramID int64 `json:"program_id"`
CourseID int64 `json:"course_id"`
Name string `json:"name"`
Description pgtype.Text `json:"description"`
Icon pgtype.Text `json:"icon"`
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 {
@ -303,14 +309,15 @@ type Permission struct {
}
type Program struct {
ID int64 `json:"id"`
Name string `json:"name"`
Description pgtype.Text `json:"description"`
Thumbnail pgtype.Text `json:"thumbnail"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
SortOrder int32 `json:"sort_order"`
Category string `json:"category"`
ID int64 `json:"id"`
Name string `json:"name"`
Description pgtype.Text `json:"description"`
Thumbnail pgtype.Text `json:"thumbnail"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
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,17 +21,19 @@ 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 {
Name string `json:"name"`
Description pgtype.Text `json:"description"`
Category string `json:"category"`
Thumbnail pgtype.Text `json:"thumbnail"`
SortOrder pgtype.Int4 `json:"sort_order"`
Name string `json:"name"`
Description pgtype.Text `json:"description"`
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,32 +131,39 @@ 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
`
type ListProgramsParams struct {
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
PublishedOnly bool `json:"published_only"`
}
type ListProgramsRow struct {
TotalCount int64 `json:"total_count"`
ID int64 `json:"id"`
Name string `json:"name"`
Description pgtype.Text `json:"description"`
Category string `json:"category"`
Thumbnail pgtype.Text `json:"thumbnail"`
SortOrder int32 `json:"sort_order"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
TotalCount int64 `json:"total_count"`
ID int64 `json:"id"`
Name string `json:"name"`
Description pgtype.Text `json:"description"`
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,20 +201,22 @@ 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 {
Name pgtype.Text `json:"name"`
Description pgtype.Text `json:"description"`
Category pgtype.Text `json:"category"`
Thumbnail pgtype.Text `json:"thumbnail"`
SortOrder pgtype.Int4 `json:"sort_order"`
ID int64 `json:"id"`
Name pgtype.Text `json:"name"`
Description pgtype.Text `json:"description"`
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"`
}
func (q *Queries) UpdateProgram(ctx context.Context, arg UpdateProgramParams) (Program, error) {
@ -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

@ -15,14 +15,15 @@ var DefaultCEFRCourseNames = []string{"A1", "A2", "B1", "B2", "C1", "C2"}
// Course belongs to a Program.
type Course struct {
ID int64 `json:"id"`
ProgramID int64 `json:"program_id"`
Name string `json:"name"`
Description *string `json:"description,omitempty"`
Thumbnail *string `json:"thumbnail,omitempty"`
SortOrder int `json:"sort_order"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at,omitempty"`
ID int64 `json:"id"`
ProgramID int64 `json:"program_id"`
Name string `json:"name"`
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
// (not practices attached to a module or lesson under this course).
ModuleCount int `json:"module_count"`
@ -32,17 +33,25 @@ 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 {
Name *string `json:"name,omitempty"`
Description *string `json:"description,omitempty"`
Thumbnail *string `json:"thumbnail,omitempty"`
SortOrder *int `json:"sort_order,omitempty"`
Name *string `json:"name,omitempty"`
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

@ -4,19 +4,25 @@ import "time"
// ExamPrepCatalogCourse is a top-level exam-prep track (e.g. DET, IELTS) in schema exam_prep — separate from LMS Learn English courses.
type ExamPrepCatalogCourse struct {
ID int64 `json:"id"`
Name string `json:"name"`
Description *string `json:"description,omitempty"`
Category string `json:"category"`
Thumbnail *string `json:"thumbnail,omitempty"`
SortOrder int `json:"sort_order"`
UnitsCount *int64 `json:"units_count,omitempty"`
ModulesCount *int64 `json:"modules_count,omitempty"`
LessonsCount *int64 `json:"lessons_count,omitempty"`
HasPractice bool `json:"has_practice"`
Access *LMSEntityAccess `json:"access,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at,omitempty"`
ID int64 `json:"id"`
Name string `json:"name"`
Description *string `json:"description,omitempty"`
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"`
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 catalog course appears in student/learner exam-prep APIs.
func (c ExamPrepCatalogCourse) VisibleToLearners() bool {
return c.PublishStatus == ContentPublishPublished
}
type CreateExamPrepCatalogCourseInput struct {
@ -24,12 +30,15 @@ type CreateExamPrepCatalogCourseInput struct {
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 {
Name *string `json:"name,omitempty"`
Description *string `json:"description,omitempty"`
Category *string `json:"category,omitempty" validate:"omitempty,oneof=IELTS DUOLINGO"`
Thumbnail *string `json:"thumbnail,omitempty"`
SortOrder *int `json:"sort_order,omitempty"`
Name *string `json:"name,omitempty"`
Description *string `json:"description,omitempty"`
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

@ -4,17 +4,23 @@ import "time"
// ExamPrepLesson is a video lesson under an exam-prep unit module (exam_prep.unit_module_lessons).
type ExamPrepLesson struct {
ID int64 `json:"id"`
UnitModuleID int64 `json:"unit_module_id"`
Title string `json:"title"`
VideoURL *string `json:"video_url,omitempty"`
Thumbnail *string `json:"thumbnail,omitempty"`
Description *string `json:"description,omitempty"`
SortOrder int `json:"sort_order"`
HasPractice bool `json:"has_practice"`
Access *LMSEntityAccess `json:"access,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at,omitempty"`
ID int64 `json:"id"`
UnitModuleID int64 `json:"unit_module_id"`
Title string `json:"title"`
VideoURL *string `json:"video_url,omitempty"`
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 {
@ -22,12 +28,15 @@ type CreateExamPrepLessonInput struct {
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 {
Title *string `json:"title,omitempty"`
VideoURL *string `json:"video_url,omitempty"`
Thumbnail *string `json:"thumbnail,omitempty"`
Description *string `json:"description,omitempty"`
SortOrder *int `json:"sort_order,omitempty"`
Title *string `json:"title,omitempty"`
VideoURL *string `json:"video_url,omitempty"`
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

@ -4,19 +4,25 @@ import "time"
// ExamPrepModule is a module under an exam-prep unit (stored in exam_prep.unit_modules).
type ExamPrepModule struct {
ID int64 `json:"id"`
UnitID int64 `json:"unit_id"`
Name string `json:"name"`
Description *string `json:"description,omitempty"`
Thumbnail *string `json:"thumbnail,omitempty"`
Icon *string `json:"icon,omitempty"`
SortOrder int `json:"sort_order"`
LessonsCount *int64 `json:"lessons_count,omitempty"`
PracticesCount *int64 `json:"practices_count,omitempty"`
HasPractice bool `json:"has_practice"`
Access *LMSEntityAccess `json:"access,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at,omitempty"`
ID int64 `json:"id"`
UnitID int64 `json:"unit_id"`
Name string `json:"name"`
Description *string `json:"description,omitempty"`
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"`
Access *LMSEntityAccess `json:"access,omitempty"`
CreatedAt time.Time `json:"created_at"`
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 {
@ -24,12 +30,15 @@ type CreateExamPrepModuleInput struct {
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 {
Name *string `json:"name,omitempty"`
Description *string `json:"description,omitempty"`
Thumbnail *string `json:"thumbnail,omitempty"`
Icon *string `json:"icon,omitempty"`
SortOrder *int `json:"sort_order,omitempty"`
Name *string `json:"name,omitempty"`
Description *string `json:"description,omitempty"`
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

@ -4,19 +4,25 @@ import "time"
// ExamPrepUnit is a chapter-like grouping under an exam-prep catalog course (schema exam_prep.units).
type ExamPrepUnit struct {
ID int64 `json:"id"`
CatalogCourseID int64 `json:"catalog_course_id"`
Name string `json:"name"`
Description *string `json:"description,omitempty"`
Thumbnail *string `json:"thumbnail,omitempty"`
SortOrder int `json:"sort_order"`
ModulesCount *int64 `json:"modules_count,omitempty"`
LessonsCount *int64 `json:"lessons_count,omitempty"`
PracticesCount *int64 `json:"practices_count,omitempty"`
HasPractice bool `json:"has_practice"`
Access *LMSEntityAccess `json:"access,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at,omitempty"`
ID int64 `json:"id"`
CatalogCourseID int64 `json:"catalog_course_id"`
Name string `json:"name"`
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"`
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 unit appears in student/learner exam-prep APIs.
func (u ExamPrepUnit) VisibleToLearners() bool {
return u.PublishStatus == ContentPublishPublished
}
type CreateExamPrepUnitInput struct {
@ -25,11 +31,14 @@ type CreateExamPrepUnitInput struct {
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 {
Name *string `json:"name,omitempty"`
Description *string `json:"description,omitempty"`
Thumbnail *string `json:"thumbnail,omitempty"`
SortOrder *int `json:"sort_order,omitempty"`
Name *string `json:"name,omitempty"`
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

@ -4,17 +4,23 @@ import "time"
// Module belongs to a Course. program_id is the courses program (stored for querying; not required from the client on create).
type Module struct {
ID int64 `json:"id"`
ProgramID int64 `json:"program_id"`
CourseID int64 `json:"course_id"`
Name string `json:"name"`
Description *string `json:"description,omitempty"`
Icon *string `json:"icon,omitempty"`
SortOrder int `json:"sort_order"`
HasPractice bool `json:"has_practice"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at,omitempty"`
Access *LMSEntityAccess `json:"access,omitempty"`
ID int64 `json:"id"`
ProgramID int64 `json:"program_id"`
CourseID int64 `json:"course_id"`
Name string `json:"name"`
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 {
@ -23,11 +29,14 @@ type CreateModuleInput struct {
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 {
Name *string `json:"name,omitempty"`
Description *string `json:"description,omitempty"`
Icon *string `json:"icon,omitempty"`
SortOrder *int `json:"sort_order,omitempty"`
Name *string `json:"name,omitempty"`
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

@ -4,15 +4,21 @@ import "time"
// Program is the top-level container in the LMS hierarchy (e.g. tracks like Beginner / Intermediate / Advanced).
type Program struct {
ID int64 `json:"id"`
Name string `json:"name"`
Description *string `json:"description,omitempty"`
Category string `json:"category"`
Thumbnail *string `json:"thumbnail,omitempty"`
SortOrder int `json:"sort_order"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at,omitempty"`
Access *LMSEntityAccess `json:"access,omitempty"`
ID int64 `json:"id"`
Name string `json:"name"`
Description *string `json:"description,omitempty"`
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 {
@ -22,12 +28,15 @@ 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 {
Name *string `json:"name,omitempty"`
Description *string `json:"description,omitempty"`
Category *string `json:"category,omitempty" validate:"omitempty,oneof=LEARN_ENGLISH IELTS DUOLINGO"`
Thumbnail *string `json:"thumbnail,omitempty"`
SortOrder *int `json:"sort_order,omitempty"`
Name *string `json:"name,omitempty"`
Description *string `json:"description,omitempty"`
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

@ -13,10 +13,11 @@ import (
func examPrepCatalogCourseToDomain(c dbgen.ExamPrepCatalogCourse) domain.ExamPrepCatalogCourse {
out := domain.ExamPrepCatalogCourse{
ID: c.ID,
Name: c.Name,
Category: c.Category,
SortOrder: int(c.SortOrder),
ID: c.ID,
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)
@ -30,10 +31,11 @@ func examPrepCatalogCourseToDomain(c dbgen.ExamPrepCatalogCourse) domain.ExamPre
func (s *Store) CreateExamPrepCatalogCourse(ctx context.Context, input domain.CreateExamPrepCatalogCourseInput) (domain.ExamPrepCatalogCourse, error) {
c, err := s.queries.ExamPrepCreateCatalogCourse(ctx, dbgen.ExamPrepCreateCatalogCourseParams{
Name: input.Name,
Description: toPgText(input.Description),
Category: input.Category,
Thumbnail: toPgText(input.Thumbnail),
Name: input.Name,
Description: toPgText(input.Description),
Category: input.Category,
Thumbnail: toPgText(input.Thumbnail),
PublishStatus: string(domain.ContentPublishStatusFromCreateInput(input.PublishStatus)),
})
if err != nil {
return domain.ExamPrepCatalogCourse{}, err
@ -50,23 +52,25 @@ func (s *Store) GetExamPrepCatalogCourseByID(ctx context.Context, id int64) (dom
return domain.ExamPrepCatalogCourse{}, err
}
out := examPrepCatalogCourseToDomain(dbgen.ExamPrepCatalogCourse{
ID: c.ID,
Name: c.Name,
Description: c.Description,
Category: c.Category,
Thumbnail: c.Thumbnail,
SortOrder: c.SortOrder,
CreatedAt: c.CreatedAt,
UpdatedAt: c.UpdatedAt,
ID: c.ID,
Name: c.Name,
Description: c.Description,
Category: c.Category,
Thumbnail: c.Thumbnail,
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,
Limit: limit,
Offset: offset,
PublishedOnly: publishedOnly,
})
if err != nil {
return nil, 0, err
@ -81,14 +85,15 @@ func (s *Store) ListExamPrepCatalogCourses(ctx context.Context, limit, offset in
total = r.TotalCount
}
item := examPrepCatalogCourseToDomain(dbgen.ExamPrepCatalogCourse{
ID: r.ID,
Name: r.Name,
Description: r.Description,
Category: r.Category,
Thumbnail: r.Thumbnail,
SortOrder: r.SortOrder,
CreatedAt: r.CreatedAt,
UpdatedAt: r.UpdatedAt,
ID: r.ID,
Name: r.Name,
Description: r.Description,
Category: r.Category,
Thumbnail: r.Thumbnail,
SortOrder: r.SortOrder,
CreatedAt: r.CreatedAt,
UpdatedAt: r.UpdatedAt,
PublishStatus: r.PublishStatus,
})
item.UnitsCount = &r.UnitsCount
item.ModulesCount = &r.ModulesCount
@ -111,12 +116,13 @@ func (s *Store) UpdateExamPrepCatalogCourse(ctx context.Context, id int64, input
nameText = pgtype.Text{Valid: false}
}
c, err := s.queries.ExamPrepUpdateCatalogCourse(ctx, dbgen.ExamPrepUpdateCatalogCourseParams{
ID: id,
Name: nameText,
Description: optionalTextUpdate(input.Description),
Category: optionalTextUpdate(input.Category),
Thumbnail: optionalTextUpdate(input.Thumbnail),
SortOrder: optionalInt4Update(input.SortOrder),
ID: id,
Name: nameText,
Description: optionalTextUpdate(input.Description),
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

@ -13,10 +13,11 @@ import (
func examPrepLessonToDomain(l dbgen.ExamPrepUnitModuleLesson) domain.ExamPrepLesson {
out := domain.ExamPrepLesson{
ID: l.ID,
UnitModuleID: l.UnitModuleID,
Title: l.Title,
SortOrder: int(l.SortOrder),
ID: l.ID,
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)
@ -31,11 +32,12 @@ func examPrepLessonToDomain(l dbgen.ExamPrepUnitModuleLesson) domain.ExamPrepLes
func (s *Store) CreateExamPrepUnitModuleLesson(ctx context.Context, unitModuleID int64, input domain.CreateExamPrepLessonInput) (domain.ExamPrepLesson, error) {
l, err := s.queries.ExamPrepCreateUnitModuleLesson(ctx, dbgen.ExamPrepCreateUnitModuleLessonParams{
UnitModuleID: unitModuleID,
Title: input.Title,
VideoUrl: toPgText(input.VideoURL),
Thumbnail: toPgText(input.Thumbnail),
Description: toPgText(input.Description),
UnitModuleID: unitModuleID,
Title: input.Title,
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
@ -52,25 +54,27 @@ func (s *Store) GetExamPrepUnitModuleLessonByID(ctx context.Context, id int64) (
return domain.ExamPrepLesson{}, err
}
out := examPrepLessonToDomain(dbgen.ExamPrepUnitModuleLesson{
ID: l.ID,
UnitModuleID: l.UnitModuleID,
Title: l.Title,
VideoUrl: l.VideoUrl,
Thumbnail: l.Thumbnail,
Description: l.Description,
SortOrder: l.SortOrder,
CreatedAt: l.CreatedAt,
UpdatedAt: l.UpdatedAt,
ID: l.ID,
UnitModuleID: l.UnitModuleID,
Title: l.Title,
VideoUrl: l.VideoUrl,
Thumbnail: l.Thumbnail,
Description: l.Description,
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,
UnitModuleID: unitModuleID,
Limit: limit,
Offset: offset,
PublishedOnly: publishedOnly,
})
if err != nil {
return nil, 0, err
@ -85,15 +89,16 @@ func (s *Store) ListExamPrepUnitModuleLessonsByUnitModuleID(ctx context.Context,
total = r.TotalCount
}
item := examPrepLessonToDomain(dbgen.ExamPrepUnitModuleLesson{
ID: r.ID,
UnitModuleID: r.UnitModuleID,
Title: r.Title,
VideoUrl: r.VideoUrl,
Thumbnail: r.Thumbnail,
Description: r.Description,
SortOrder: r.SortOrder,
CreatedAt: r.CreatedAt,
UpdatedAt: r.UpdatedAt,
ID: r.ID,
UnitModuleID: r.UnitModuleID,
Title: r.Title,
VideoUrl: r.VideoUrl,
Thumbnail: r.Thumbnail,
Description: r.Description,
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 {
@ -113,12 +122,13 @@ func (s *Store) UpdateExamPrepUnitModuleLesson(ctx context.Context, id int64, in
titleText = pgtype.Text{Valid: false}
}
l, err := s.queries.ExamPrepUpdateUnitModuleLesson(ctx, dbgen.ExamPrepUpdateUnitModuleLessonParams{
ID: id,
Title: titleText,
VideoUrl: optionalTextUpdate(input.VideoURL),
Thumbnail: optionalTextUpdate(input.Thumbnail),
Description: optionalTextUpdate(input.Description),
SortOrder: optionalInt4Update(input.SortOrder),
ID: id,
Title: titleText,
VideoUrl: optionalTextUpdate(input.VideoURL),
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

@ -13,10 +13,11 @@ import (
func examPrepModuleToDomain(m dbgen.ExamPrepUnitModule) domain.ExamPrepModule {
out := domain.ExamPrepModule{
ID: m.ID,
UnitID: m.UnitID,
Name: m.Name,
SortOrder: int(m.SortOrder),
ID: m.ID,
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)
@ -31,11 +32,12 @@ func examPrepModuleToDomain(m dbgen.ExamPrepUnitModule) domain.ExamPrepModule {
func (s *Store) CreateExamPrepUnitModule(ctx context.Context, unitID int64, input domain.CreateExamPrepModuleInput) (domain.ExamPrepModule, error) {
m, err := s.queries.ExamPrepCreateUnitModule(ctx, dbgen.ExamPrepCreateUnitModuleParams{
UnitID: unitID,
Name: input.Name,
Description: toPgText(input.Description),
Thumbnail: toPgText(input.Thumbnail),
Icon: toPgText(input.Icon),
UnitID: unitID,
Name: input.Name,
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
@ -52,25 +54,27 @@ func (s *Store) GetExamPrepUnitModuleByID(ctx context.Context, id int64) (domain
return domain.ExamPrepModule{}, err
}
out := examPrepModuleToDomain(dbgen.ExamPrepUnitModule{
ID: m.ID,
UnitID: m.UnitID,
Name: m.Name,
Description: m.Description,
Thumbnail: m.Thumbnail,
Icon: m.Icon,
SortOrder: m.SortOrder,
CreatedAt: m.CreatedAt,
UpdatedAt: m.UpdatedAt,
ID: m.ID,
UnitID: m.UnitID,
Name: m.Name,
Description: m.Description,
Thumbnail: m.Thumbnail,
Icon: m.Icon,
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,
UnitID: unitID,
Limit: limit,
Offset: offset,
PublishedOnly: publishedOnly,
})
if err != nil {
return nil, 0, err
@ -85,15 +89,16 @@ func (s *Store) ListExamPrepUnitModulesByUnit(ctx context.Context, unitID int64,
total = r.TotalCount
}
item := examPrepModuleToDomain(dbgen.ExamPrepUnitModule{
ID: r.ID,
UnitID: r.UnitID,
Name: r.Name,
Description: r.Description,
Thumbnail: r.Thumbnail,
Icon: r.Icon,
SortOrder: r.SortOrder,
CreatedAt: r.CreatedAt,
UpdatedAt: r.UpdatedAt,
ID: r.ID,
UnitID: r.UnitID,
Name: r.Name,
Description: r.Description,
Thumbnail: r.Thumbnail,
Icon: r.Icon,
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 {
@ -115,12 +124,13 @@ func (s *Store) UpdateExamPrepUnitModule(ctx context.Context, id int64, input do
nameText = pgtype.Text{Valid: false}
}
m, err := s.queries.ExamPrepUpdateUnitModule(ctx, dbgen.ExamPrepUpdateUnitModuleParams{
ID: id,
Name: nameText,
Description: optionalTextUpdate(input.Description),
Thumbnail: optionalTextUpdate(input.Thumbnail),
Icon: optionalTextUpdate(input.Icon),
SortOrder: optionalInt4Update(input.SortOrder),
ID: id,
Name: nameText,
Description: optionalTextUpdate(input.Description),
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 {
@ -142,11 +153,12 @@ func (s *Store) UpdateExamPrepUnit(ctx context.Context, id int64, input domain.U
nameText = pgtype.Text{Valid: false}
}
u, err := s.queries.ExamPrepUpdateUnit(ctx, dbgen.ExamPrepUpdateUnitParams{
ID: id,
Name: nameText,
Description: optionalTextUpdate(input.Description),
Thumbnail: optionalTextUpdate(input.Thumbnail),
SortOrder: optionalInt4Update(input.SortOrder),
ID: id,
Name: nameText,
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

@ -13,9 +13,10 @@ import (
func courseToDomain(c dbgen.Course) domain.Course {
out := domain.Course{
ID: c.ID,
ProgramID: c.ProgramID,
Name: c.Name,
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 {
@ -43,11 +45,12 @@ func (s *Store) CreateCourse(ctx context.Context, programID int64, input domain.
return domain.Course{}, err
}
c, err := q.CreateCourse(ctx, dbgen.CreateCourseParams{
ProgramID: programID,
Name: input.Name,
Description: toPgText(input.Description),
Thumbnail: toPgText(input.Thumbnail),
SortOrder: pgtype.Int4{Int32: target, Valid: true},
ProgramID: programID,
Name: input.Name,
Description: toPgText(input.Description),
Thumbnail: toPgText(input.Thumbnail),
SortOrder: pgtype.Int4{Int32: target, Valid: true},
PublishStatus: pub,
})
if err != nil {
return domain.Course{}, err
@ -59,11 +62,12 @@ func (s *Store) CreateCourse(ctx context.Context, programID int64, input domain.
}
c, err := s.queries.CreateCourse(ctx, dbgen.CreateCourseParams{
ProgramID: programID,
Name: input.Name,
Description: toPgText(input.Description),
Thumbnail: toPgText(input.Thumbnail),
SortOrder: pgtype.Int4{Valid: false},
ProgramID: programID,
Name: input.Name,
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 {
@ -84,24 +92,26 @@ func (s *Store) GetCourseByID(ctx context.Context, id int64) (domain.Course, err
return domain.Course{}, err
}
out := courseToDomain(dbgen.Course{
ID: c.ID,
ProgramID: c.ProgramID,
Name: c.Name,
Description: c.Description,
Thumbnail: c.Thumbnail,
SortOrder: c.SortOrder,
CreatedAt: c.CreatedAt,
UpdatedAt: c.UpdatedAt,
ID: c.ID,
ProgramID: c.ProgramID,
Name: c.Name,
Description: c.Description,
Thumbnail: c.Thumbnail,
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,
ProgramID: programID,
Limit: limit,
Offset: offset,
PublishedOnly: publishedOnly,
})
if err != nil {
return nil, 0, err
@ -116,14 +126,15 @@ func (s *Store) ListCoursesByProgramID(ctx context.Context, programID int64, lim
total = r.TotalCount
}
co := courseToDomain(dbgen.Course{
ID: r.ID,
ProgramID: r.ProgramID,
Name: r.Name,
Description: r.Description,
Thumbnail: r.Thumbnail,
CreatedAt: r.CreatedAt,
UpdatedAt: r.UpdatedAt,
SortOrder: r.SortOrder,
ID: r.ID,
ProgramID: r.ProgramID,
Name: r.Name,
Description: r.Description,
Thumbnail: r.Thumbnail,
CreatedAt: r.CreatedAt,
UpdatedAt: r.UpdatedAt,
SortOrder: r.SortOrder,
PublishStatus: r.PublishStatus,
})
co.ModuleCount = int(r.ModuleCount)
co.LessonCount = int(r.LessonCount)
@ -160,11 +171,12 @@ func (s *Store) UpdateCourse(ctx context.Context, id int64, input domain.UpdateC
return domain.Course{}, err
}
c, err := q.UpdateCourse(ctx, dbgen.UpdateCourseParams{
ID: id,
Name: nameText,
Description: optionalTextUpdate(input.Description),
Thumbnail: optionalTextUpdate(input.Thumbnail),
SortOrder: pgtype.Int4{Valid: false},
ID: id,
Name: nameText,
Description: optionalTextUpdate(input.Description),
Thumbnail: optionalTextUpdate(input.Thumbnail),
SortOrder: pgtype.Int4{Valid: false},
PublishStatus: optionalPublishStatusUpdate(input.PublishStatus),
})
if err != nil {
return domain.Course{}, err
@ -180,11 +192,12 @@ func (s *Store) UpdateCourse(ctx context.Context, id int64, input domain.UpdateC
}
c, err := s.queries.UpdateCourse(ctx, dbgen.UpdateCourseParams{
ID: id,
Name: nameText,
Description: optionalTextUpdate(input.Description),
Thumbnail: optionalTextUpdate(input.Thumbnail),
SortOrder: sortParam,
ID: id,
Name: nameText,
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

@ -13,10 +13,11 @@ import (
func moduleToDomain(m dbgen.Module) domain.Module {
out := domain.Module{
ID: m.ID,
ProgramID: m.ProgramID,
CourseID: m.CourseID,
Name: m.Name,
ID: m.ID,
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 {
@ -44,12 +46,13 @@ func (s *Store) CreateModule(ctx context.Context, programID, courseID int64, inp
return domain.Module{}, err
}
m, err := q.CreateModule(ctx, dbgen.CreateModuleParams{
ProgramID: programID,
CourseID: courseID,
Name: input.Name,
Description: toPgText(input.Description),
Icon: toPgText(input.Icon),
SortOrder: pgtype.Int4{Int32: target, Valid: true},
ProgramID: programID,
CourseID: courseID,
Name: input.Name,
Description: toPgText(input.Description),
Icon: toPgText(input.Icon),
SortOrder: pgtype.Int4{Int32: target, Valid: true},
PublishStatus: pub,
})
if err != nil {
return domain.Module{}, err
@ -61,12 +64,13 @@ func (s *Store) CreateModule(ctx context.Context, programID, courseID int64, inp
}
m, err := s.queries.CreateModule(ctx, dbgen.CreateModuleParams{
ProgramID: programID,
CourseID: courseID,
Name: input.Name,
Description: toPgText(input.Description),
Icon: toPgText(input.Icon),
SortOrder: pgtype.Int4{Valid: false},
ProgramID: programID,
CourseID: courseID,
Name: input.Name,
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 {
@ -87,26 +95,28 @@ func (s *Store) GetModuleByID(ctx context.Context, id int64) (domain.Module, err
return domain.Module{}, err
}
out := moduleToDomain(dbgen.Module{
ID: m.ID,
ProgramID: m.ProgramID,
CourseID: m.CourseID,
Name: m.Name,
Description: m.Description,
Icon: m.Icon,
SortOrder: m.SortOrder,
CreatedAt: m.CreatedAt,
UpdatedAt: m.UpdatedAt,
ID: m.ID,
ProgramID: m.ProgramID,
CourseID: m.CourseID,
Name: m.Name,
Description: m.Description,
Icon: m.Icon,
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,
ProgramID: programID,
CourseID: courseID,
Limit: limit,
Offset: offset,
PublishedOnly: publishedOnly,
})
if err != nil {
return nil, 0, err
@ -121,15 +131,16 @@ func (s *Store) ListModulesByProgramAndCourse(ctx context.Context, programID, co
total = r.TotalCount
}
mod := moduleToDomain(dbgen.Module{
ID: r.ID,
ProgramID: r.ProgramID,
CourseID: r.CourseID,
Name: r.Name,
Description: r.Description,
Icon: r.Icon,
CreatedAt: r.CreatedAt,
UpdatedAt: r.UpdatedAt,
SortOrder: r.SortOrder,
ID: r.ID,
ProgramID: r.ProgramID,
CourseID: r.CourseID,
Name: r.Name,
Description: r.Description,
Icon: r.Icon,
CreatedAt: r.CreatedAt,
UpdatedAt: r.UpdatedAt,
SortOrder: r.SortOrder,
PublishStatus: r.PublishStatus,
})
mod.HasPractice = r.HasPractice
out = append(out, mod)
@ -168,11 +179,12 @@ func (s *Store) UpdateModule(ctx context.Context, id int64, input domain.UpdateM
nameText = pgtype.Text{Valid: false}
}
m, err := q.UpdateModule(ctx, dbgen.UpdateModuleParams{
ID: id,
Name: nameText,
Description: optionalTextUpdate(input.Description),
Icon: optionalTextUpdate(input.Icon),
SortOrder: sortParam,
ID: id,
Name: nameText,
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

@ -14,9 +14,10 @@ import (
func programToDomain(p dbgen.Program) domain.Program {
out := domain.Program{
ID: p.ID,
Name: p.Name,
Category: p.Category,
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 {
@ -41,11 +43,12 @@ func (s *Store) CreateProgram(ctx context.Context, input domain.CreateProgramInp
return domain.Program{}, err
}
p, err := q.CreateProgram(ctx, dbgen.CreateProgramParams{
Name: input.Name,
Description: toPgText(input.Description),
Category: input.Category,
Thumbnail: toPgText(input.Thumbnail),
SortOrder: pgtype.Int4{Int32: target, Valid: true},
Name: input.Name,
Description: toPgText(input.Description),
Category: input.Category,
Thumbnail: toPgText(input.Thumbnail),
SortOrder: pgtype.Int4{Int32: target, Valid: true},
PublishStatus: pub,
})
if err != nil {
return domain.Program{}, err
@ -57,11 +60,12 @@ func (s *Store) CreateProgram(ctx context.Context, input domain.CreateProgramInp
}
p, err := s.queries.CreateProgram(ctx, dbgen.CreateProgramParams{
Name: input.Name,
Description: toPgText(input.Description),
Category: input.Category,
Thumbnail: toPgText(input.Thumbnail),
SortOrder: pgtype.Int4{Valid: false},
Name: input.Name,
Description: toPgText(input.Description),
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,
Limit: limit,
Offset: offset,
PublishedOnly: publishedOnly,
})
if err != nil {
return nil, 0, err
@ -102,14 +107,15 @@ func (s *Store) ListPrograms(ctx context.Context, limit, offset int32) ([]domain
total = r.TotalCount
}
out = append(out, programToDomain(dbgen.Program{
ID: r.ID,
Name: r.Name,
Description: r.Description,
Category: r.Category,
Thumbnail: r.Thumbnail,
CreatedAt: r.CreatedAt,
UpdatedAt: r.UpdatedAt,
SortOrder: r.SortOrder,
ID: r.ID,
Name: r.Name,
Description: r.Description,
Category: r.Category,
Thumbnail: r.Thumbnail,
CreatedAt: r.CreatedAt,
UpdatedAt: r.UpdatedAt,
SortOrder: r.SortOrder,
PublishStatus: r.PublishStatus,
}))
}
return out, total, nil
@ -167,12 +173,13 @@ func (s *Store) UpdateProgram(ctx context.Context, id int64, input domain.Update
nameText = pgtype.Text{Valid: false}
}
p, err := q.UpdateProgram(ctx, dbgen.UpdateProgramParams{
ID: id,
Name: nameText,
Description: optionalTextUpdate(input.Description),
Category: optionalTextUpdate(input.Category),
Thumbnail: optionalTextUpdate(input.Thumbnail),
SortOrder: pgtype.Int4{Valid: false},
ID: id,
Name: nameText,
Description: optionalTextUpdate(input.Description),
Category: optionalTextUpdate(input.Category),
Thumbnail: optionalTextUpdate(input.Thumbnail),
SortOrder: pgtype.Int4{Valid: false},
PublishStatus: optionalPublishStatusUpdate(input.PublishStatus),
})
if err != nil {
return domain.Program{}, err
@ -192,12 +199,13 @@ func (s *Store) UpdateProgram(ctx context.Context, id int64, input domain.Update
nameText = pgtype.Text{Valid: false}
}
p, err := s.queries.UpdateProgram(ctx, dbgen.UpdateProgramParams{
ID: id,
Name: nameText,
Description: optionalTextUpdate(input.Description),
Category: optionalTextUpdate(input.Category),
Thumbnail: optionalTextUpdate(input.Thumbnail),
SortOrder: sortParam,
ID: id,
Name: nameText,
Description: optionalTextUpdate(input.Description),
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{