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 -- 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 SELECT
$1, $1,
$2, $2,
@ -8,7 +8,8 @@ SELECT
coalesce(( coalesce((
SELECT SELECT
max(c.sort_order) max(c.sort_order)
FROM exam_prep.catalog_courses AS c), 0) + 1 FROM exam_prep.catalog_courses AS c), 0) + 1,
$5
RETURNING RETURNING
*; *;
@ -22,6 +23,10 @@ SELECT
INNER JOIN exam_prep.unit_modules m ON m.id = l.unit_module_id 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 INNER JOIN exam_prep.units u ON u.id = m.unit_id
WHERE u.catalog_course_id = c.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 ) AS has_practice
FROM exam_prep.catalog_courses c FROM exam_prep.catalog_courses c
WHERE c.id = $1; WHERE c.id = $1;
@ -35,7 +40,10 @@ WITH catalog_course_counts AS (
COUNT(DISTINCT l.id)::BIGINT AS lessons_count COUNT(DISTINCT l.id)::BIGINT AS lessons_count
FROM exam_prep.units u FROM exam_prep.units u
LEFT JOIN exam_prep.unit_modules m ON m.unit_id = u.id 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 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 GROUP BY u.catalog_course_id
) )
SELECT SELECT
@ -46,6 +54,7 @@ SELECT
c.category, c.category,
c.thumbnail, c.thumbnail,
c.sort_order, c.sort_order,
c.publish_status,
COALESCE(cc.units_count, 0)::BIGINT AS units_count, COALESCE(cc.units_count, 0)::BIGINT AS units_count,
COALESCE(cc.modules_count, 0)::BIGINT AS modules_count, COALESCE(cc.modules_count, 0)::BIGINT AS modules_count,
COALESCE(cc.lessons_count, 0)::BIGINT AS lessons_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.unit_modules m ON m.id = l.unit_module_id
INNER JOIN exam_prep.units u ON u.id = m.unit_id INNER JOIN exam_prep.units u ON u.id = m.unit_id
WHERE u.catalog_course_id = c.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, ) AS has_practice,
c.created_at, c.created_at,
c.updated_at c.updated_at
FROM exam_prep.catalog_courses c FROM exam_prep.catalog_courses c
LEFT JOIN catalog_course_counts cc ON cc.catalog_course_id = c.id 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 ORDER BY c.sort_order ASC, c.id ASC
LIMIT $1 OFFSET $2; LIMIT $1 OFFSET $2;
@ -78,6 +95,7 @@ SET
category = coalesce(sqlc.narg('category')::varchar, category), category = coalesce(sqlc.narg('category')::varchar, category),
thumbnail = coalesce(sqlc.narg('thumbnail')::text, thumbnail), thumbnail = coalesce(sqlc.narg('thumbnail')::text, thumbnail),
sort_order = coalesce(sqlc.narg('sort_order')::int, sort_order), sort_order = coalesce(sqlc.narg('sort_order')::int, sort_order),
publish_status = coalesce(sqlc.narg('publish_status')::varchar, publish_status),
updated_at = CURRENT_TIMESTAMP updated_at = CURRENT_TIMESTAMP
WHERE id = sqlc.arg('id') WHERE id = sqlc.arg('id')
RETURNING RETURNING

View File

@ -34,6 +34,7 @@ FROM
INNER JOIN question_sets qs ON qs.id = p.question_set_id INNER JOIN question_sets qs ON qs.id = p.question_set_id
WHERE WHERE
l.unit_module_id = $1 l.unit_module_id = $1
AND l.publish_status = 'PUBLISHED'
AND qs.set_type = 'PRACTICE' AND qs.set_type = 'PRACTICE'
AND qs.status = 'PUBLISHED' AND qs.status = 'PUBLISHED'
AND p.publish_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 INNER JOIN user_practice_progress upp ON upp.question_set_id = p.question_set_id
WHERE WHERE
l.unit_module_id = $1 l.unit_module_id = $1
AND l.publish_status = 'PUBLISHED'
AND upp.user_id = $2 AND upp.user_id = $2
AND upp.completed_at IS NOT NULL AND upp.completed_at IS NOT NULL
AND qs.set_type = 'PRACTICE' AND qs.set_type = 'PRACTICE'
@ -64,6 +66,8 @@ FROM
INNER JOIN question_sets qs ON qs.id = p.question_set_id INNER JOIN question_sets qs ON qs.id = p.question_set_id
WHERE WHERE
m.unit_id = $1 m.unit_id = $1
AND l.publish_status = 'PUBLISHED'
AND m.publish_status = 'PUBLISHED'
AND qs.set_type = 'PRACTICE' AND qs.set_type = 'PRACTICE'
AND qs.status = 'PUBLISHED' AND qs.status = 'PUBLISHED'
AND p.publish_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 INNER JOIN user_practice_progress upp ON upp.question_set_id = p.question_set_id
WHERE WHERE
m.unit_id = $1 m.unit_id = $1
AND l.publish_status = 'PUBLISHED'
AND m.publish_status = 'PUBLISHED'
AND upp.user_id = $2 AND upp.user_id = $2
AND upp.completed_at IS NOT NULL AND upp.completed_at IS NOT NULL
AND qs.set_type = 'PRACTICE' AND qs.set_type = 'PRACTICE'
@ -96,6 +102,9 @@ FROM
INNER JOIN question_sets qs ON qs.id = p.question_set_id INNER JOIN question_sets qs ON qs.id = p.question_set_id
WHERE WHERE
u.catalog_course_id = $1 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.set_type = 'PRACTICE'
AND qs.status = 'PUBLISHED' AND qs.status = 'PUBLISHED'
AND p.publish_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 INNER JOIN user_practice_progress upp ON upp.question_set_id = p.question_set_id
WHERE WHERE
u.catalog_course_id = $1 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.user_id = $2
AND upp.completed_at IS NOT NULL AND upp.completed_at IS NOT NULL
AND qs.set_type = 'PRACTICE' AND qs.set_type = 'PRACTICE'

View File

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

View File

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

View File

@ -1,5 +1,5 @@
-- name: ExamPrepCreateUnit :one -- 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 SELECT
sqlc.arg('catalog_course_id'), sqlc.arg('catalog_course_id'),
sqlc.arg('name'), sqlc.arg('name'),
@ -11,7 +11,8 @@ SELECT
max(u.sort_order) max(u.sort_order)
FROM exam_prep.units u FROM exam_prep.units u
WHERE 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 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_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 INNER JOIN exam_prep.unit_modules m ON m.id = l.unit_module_id
WHERE m.unit_id = u.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 ) AS has_practice
FROM exam_prep.units u FROM exam_prep.units u
WHERE u.id = $1; WHERE u.id = $1;
@ -37,6 +41,17 @@ WHERE
ORDER BY ORDER BY
u.id; 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 -- name: ExamPrepListUnitsByCatalogCourse :many
WITH unit_counts AS ( WITH unit_counts AS (
SELECT SELECT
@ -46,8 +61,11 @@ WITH unit_counts AS (
COUNT(DISTINCT p.id)::BIGINT AS practices_count COUNT(DISTINCT p.id)::BIGINT AS practices_count
FROM exam_prep.units u FROM exam_prep.units u
LEFT JOIN exam_prep.unit_modules m ON m.unit_id = u.id 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 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 LEFT JOIN exam_prep.lesson_practices p ON p.unit_module_lesson_id = l.id
AND p.publish_status = 'PUBLISHED'
GROUP BY u.id GROUP BY u.id
) )
SELECT SELECT
@ -58,6 +76,7 @@ SELECT
u.description, u.description,
u.thumbnail, u.thumbnail,
u.sort_order, u.sort_order,
u.publish_status,
COALESCE(uc.modules_count, 0)::BIGINT AS modules_count, COALESCE(uc.modules_count, 0)::BIGINT AS modules_count,
COALESCE(uc.lessons_count, 0)::BIGINT AS lessons_count, COALESCE(uc.lessons_count, 0)::BIGINT AS lessons_count,
COALESCE(uc.practices_count, 0)::BIGINT AS practices_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 LEFT JOIN unit_counts uc ON uc.unit_id = u.id
WHERE WHERE
u.catalog_course_id = $1 u.catalog_course_id = $1
AND (
sqlc.arg('published_only')::boolean = FALSE
OR u.publish_status = 'PUBLISHED'::TEXT
)
ORDER BY ORDER BY
u.sort_order ASC, u.sort_order ASC,
u.id ASC u.id ASC
@ -81,6 +104,7 @@ SET
description = coalesce(sqlc.narg('description')::text, description), description = coalesce(sqlc.narg('description')::text, description),
thumbnail = coalesce(sqlc.narg('thumbnail')::text, thumbnail), thumbnail = coalesce(sqlc.narg('thumbnail')::text, thumbnail),
sort_order = coalesce(sqlc.narg('sort_order')::int, sort_order), sort_order = coalesce(sqlc.narg('sort_order')::int, sort_order),
publish_status = coalesce(sqlc.narg('publish_status')::varchar, publish_status),
updated_at = CURRENT_TIMESTAMP updated_at = CURRENT_TIMESTAMP
WHERE id = sqlc.arg('id') WHERE id = sqlc.arg('id')
RETURNING RETURNING

View File

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

View File

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

View File

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

View File

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

View File

@ -12,7 +12,7 @@ import (
) )
const ExamPrepCreateCatalogCourse = `-- name: ExamPrepCreateCatalogCourse :one 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 SELECT
$1, $1,
$2, $2,
@ -21,16 +21,18 @@ SELECT
coalesce(( coalesce((
SELECT SELECT
max(c.sort_order) max(c.sort_order)
FROM exam_prep.catalog_courses AS c), 0) + 1 FROM exam_prep.catalog_courses AS c), 0) + 1,
$5
RETURNING 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 { type ExamPrepCreateCatalogCourseParams struct {
Name string `json:"name"` Name string `json:"name"`
Description pgtype.Text `json:"description"` Description pgtype.Text `json:"description"`
Category string `json:"category"` Category string `json:"category"`
Thumbnail pgtype.Text `json:"thumbnail"` Thumbnail pgtype.Text `json:"thumbnail"`
PublishStatus string `json:"publish_status"`
} }
func (q *Queries) ExamPrepCreateCatalogCourse(ctx context.Context, arg ExamPrepCreateCatalogCourseParams) (ExamPrepCatalogCourse, error) { 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.Description,
arg.Category, arg.Category,
arg.Thumbnail, arg.Thumbnail,
arg.PublishStatus,
) )
var i ExamPrepCatalogCourse var i ExamPrepCatalogCourse
err := row.Scan( err := row.Scan(
@ -50,6 +53,7 @@ func (q *Queries) ExamPrepCreateCatalogCourse(ctx context.Context, arg ExamPrepC
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.Category, &i.Category,
&i.PublishStatus,
) )
return i, err return i, err
} }
@ -66,7 +70,7 @@ func (q *Queries) ExamPrepDeleteCatalogCourse(ctx context.Context, id int64) err
const ExamPrepGetCatalogCourseByID = `-- name: ExamPrepGetCatalogCourseByID :one const ExamPrepGetCatalogCourseByID = `-- name: ExamPrepGetCatalogCourseByID :one
SELECT 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 ( EXISTS (
SELECT 1 SELECT 1
FROM exam_prep.lesson_practices p 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.unit_modules m ON m.id = l.unit_module_id
INNER JOIN exam_prep.units u ON u.id = m.unit_id INNER JOIN exam_prep.units u ON u.id = m.unit_id
WHERE u.catalog_course_id = c.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 ) AS has_practice
FROM exam_prep.catalog_courses c FROM exam_prep.catalog_courses c
WHERE c.id = $1 WHERE c.id = $1
` `
type ExamPrepGetCatalogCourseByIDRow struct { type ExamPrepGetCatalogCourseByIDRow struct {
ID int64 `json:"id"` ID int64 `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Description pgtype.Text `json:"description"` Description pgtype.Text `json:"description"`
Thumbnail pgtype.Text `json:"thumbnail"` Thumbnail pgtype.Text `json:"thumbnail"`
SortOrder int32 `json:"sort_order"` SortOrder int32 `json:"sort_order"`
CreatedAt pgtype.Timestamptz `json:"created_at"` CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"` UpdatedAt pgtype.Timestamptz `json:"updated_at"`
Category string `json:"category"` Category string `json:"category"`
HasPractice bool `json:"has_practice"` PublishStatus string `json:"publish_status"`
HasPractice bool `json:"has_practice"`
} }
func (q *Queries) ExamPrepGetCatalogCourseByID(ctx context.Context, id int64) (ExamPrepGetCatalogCourseByIDRow, error) { 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.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.Category, &i.Category,
&i.PublishStatus,
&i.HasPractice, &i.HasPractice,
) )
return i, err return i, err
@ -144,7 +154,10 @@ WITH catalog_course_counts AS (
COUNT(DISTINCT l.id)::BIGINT AS lessons_count COUNT(DISTINCT l.id)::BIGINT AS lessons_count
FROM exam_prep.units u FROM exam_prep.units u
LEFT JOIN exam_prep.unit_modules m ON m.unit_id = u.id 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 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 GROUP BY u.catalog_course_id
) )
SELECT SELECT
@ -155,6 +168,7 @@ SELECT
c.category, c.category,
c.thumbnail, c.thumbnail,
c.sort_order, c.sort_order,
c.publish_status,
COALESCE(cc.units_count, 0)::BIGINT AS units_count, COALESCE(cc.units_count, 0)::BIGINT AS units_count,
COALESCE(cc.modules_count, 0)::BIGINT AS modules_count, COALESCE(cc.modules_count, 0)::BIGINT AS modules_count,
COALESCE(cc.lessons_count, 0)::BIGINT AS lessons_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.unit_modules m ON m.id = l.unit_module_id
INNER JOIN exam_prep.units u ON u.id = m.unit_id INNER JOIN exam_prep.units u ON u.id = m.unit_id
WHERE u.catalog_course_id = c.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, ) AS has_practice,
c.created_at, c.created_at,
c.updated_at c.updated_at
FROM exam_prep.catalog_courses c FROM exam_prep.catalog_courses c
LEFT JOIN catalog_course_counts cc ON cc.catalog_course_id = c.id 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 ORDER BY c.sort_order ASC, c.id ASC
LIMIT $1 OFFSET $2 LIMIT $1 OFFSET $2
` `
type ExamPrepListCatalogCoursesParams struct { type ExamPrepListCatalogCoursesParams struct {
Limit int32 `json:"limit"` Limit int32 `json:"limit"`
Offset int32 `json:"offset"` Offset int32 `json:"offset"`
PublishedOnly bool `json:"published_only"`
} }
type ExamPrepListCatalogCoursesRow struct { type ExamPrepListCatalogCoursesRow struct {
TotalCount int64 `json:"total_count"` TotalCount int64 `json:"total_count"`
ID int64 `json:"id"` ID int64 `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Description pgtype.Text `json:"description"` Description pgtype.Text `json:"description"`
Category string `json:"category"` Category string `json:"category"`
Thumbnail pgtype.Text `json:"thumbnail"` Thumbnail pgtype.Text `json:"thumbnail"`
SortOrder int32 `json:"sort_order"` SortOrder int32 `json:"sort_order"`
UnitsCount int64 `json:"units_count"` PublishStatus string `json:"publish_status"`
ModulesCount int64 `json:"modules_count"` UnitsCount int64 `json:"units_count"`
LessonsCount int64 `json:"lessons_count"` ModulesCount int64 `json:"modules_count"`
HasPractice bool `json:"has_practice"` LessonsCount int64 `json:"lessons_count"`
CreatedAt pgtype.Timestamptz `json:"created_at"` HasPractice bool `json:"has_practice"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"` CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
} }
func (q *Queries) ExamPrepListCatalogCourses(ctx context.Context, arg ExamPrepListCatalogCoursesParams) ([]ExamPrepListCatalogCoursesRow, error) { 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 { if err != nil {
return nil, err return nil, err
} }
@ -212,6 +236,7 @@ func (q *Queries) ExamPrepListCatalogCourses(ctx context.Context, arg ExamPrepLi
&i.Category, &i.Category,
&i.Thumbnail, &i.Thumbnail,
&i.SortOrder, &i.SortOrder,
&i.PublishStatus,
&i.UnitsCount, &i.UnitsCount,
&i.ModulesCount, &i.ModulesCount,
&i.LessonsCount, &i.LessonsCount,
@ -237,19 +262,21 @@ SET
category = coalesce($3::varchar, category), category = coalesce($3::varchar, category),
thumbnail = coalesce($4::text, thumbnail), thumbnail = coalesce($4::text, thumbnail),
sort_order = coalesce($5::int, sort_order), sort_order = coalesce($5::int, sort_order),
publish_status = coalesce($6::varchar, publish_status),
updated_at = CURRENT_TIMESTAMP updated_at = CURRENT_TIMESTAMP
WHERE id = $6 WHERE id = $7
RETURNING 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 { type ExamPrepUpdateCatalogCourseParams struct {
Name pgtype.Text `json:"name"` Name pgtype.Text `json:"name"`
Description pgtype.Text `json:"description"` Description pgtype.Text `json:"description"`
Category pgtype.Text `json:"category"` Category pgtype.Text `json:"category"`
Thumbnail pgtype.Text `json:"thumbnail"` Thumbnail pgtype.Text `json:"thumbnail"`
SortOrder pgtype.Int4 `json:"sort_order"` SortOrder pgtype.Int4 `json:"sort_order"`
ID int64 `json:"id"` PublishStatus pgtype.Text `json:"publish_status"`
ID int64 `json:"id"`
} }
func (q *Queries) ExamPrepUpdateCatalogCourse(ctx context.Context, arg ExamPrepUpdateCatalogCourseParams) (ExamPrepCatalogCourse, error) { 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.Category,
arg.Thumbnail, arg.Thumbnail,
arg.SortOrder, arg.SortOrder,
arg.PublishStatus,
arg.ID, arg.ID,
) )
var i ExamPrepCatalogCourse var i ExamPrepCatalogCourse
@ -271,6 +299,7 @@ func (q *Queries) ExamPrepUpdateCatalogCourse(ctx context.Context, arg ExamPrepU
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.Category, &i.Category,
&i.PublishStatus,
) )
return i, err return i, err
} }

View File

@ -20,6 +20,9 @@ FROM
INNER JOIN question_sets qs ON qs.id = p.question_set_id INNER JOIN question_sets qs ON qs.id = p.question_set_id
WHERE WHERE
u.catalog_course_id = $1 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.set_type = 'PRACTICE'
AND qs.status = 'PUBLISHED' AND qs.status = 'PUBLISHED'
AND p.publish_status = 'PUBLISHED' AND p.publish_status = 'PUBLISHED'
@ -61,6 +64,7 @@ FROM
INNER JOIN question_sets qs ON qs.id = p.question_set_id INNER JOIN question_sets qs ON qs.id = p.question_set_id
WHERE WHERE
l.unit_module_id = $1 l.unit_module_id = $1
AND l.publish_status = 'PUBLISHED'
AND qs.set_type = 'PRACTICE' AND qs.set_type = 'PRACTICE'
AND qs.status = 'PUBLISHED' AND qs.status = 'PUBLISHED'
AND p.publish_status = 'PUBLISHED' AND p.publish_status = 'PUBLISHED'
@ -83,6 +87,8 @@ FROM
INNER JOIN question_sets qs ON qs.id = p.question_set_id INNER JOIN question_sets qs ON qs.id = p.question_set_id
WHERE WHERE
m.unit_id = $1 m.unit_id = $1
AND l.publish_status = 'PUBLISHED'
AND m.publish_status = 'PUBLISHED'
AND qs.set_type = 'PRACTICE' AND qs.set_type = 'PRACTICE'
AND qs.status = 'PUBLISHED' AND qs.status = 'PUBLISHED'
AND p.publish_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 INNER JOIN user_practice_progress upp ON upp.question_set_id = p.question_set_id
WHERE WHERE
u.catalog_course_id = $1 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.user_id = $2
AND upp.completed_at IS NOT NULL AND upp.completed_at IS NOT NULL
AND qs.set_type = 'PRACTICE' 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 INNER JOIN user_practice_progress upp ON upp.question_set_id = p.question_set_id
WHERE WHERE
l.unit_module_id = $1 l.unit_module_id = $1
AND l.publish_status = 'PUBLISHED'
AND upp.user_id = $2 AND upp.user_id = $2
AND upp.completed_at IS NOT NULL AND upp.completed_at IS NOT NULL
AND qs.set_type = 'PRACTICE' 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 INNER JOIN user_practice_progress upp ON upp.question_set_id = p.question_set_id
WHERE WHERE
m.unit_id = $1 m.unit_id = $1
AND l.publish_status = 'PUBLISHED'
AND m.publish_status = 'PUBLISHED'
AND upp.user_id = $2 AND upp.user_id = $2
AND upp.completed_at IS NOT NULL AND upp.completed_at IS NOT NULL
AND qs.set_type = 'PRACTICE' AND qs.set_type = 'PRACTICE'

View File

@ -12,7 +12,7 @@ import (
) )
const ExamPrepCreateUnitModuleLesson = `-- name: ExamPrepCreateUnitModuleLesson :one 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 SELECT
$1, $1,
$2, $2,
@ -24,17 +24,19 @@ SELECT
max(l.sort_order) max(l.sort_order)
FROM exam_prep.unit_module_lessons l FROM exam_prep.unit_module_lessons l
WHERE WHERE
l.unit_module_id = $1), 0) + 1 l.unit_module_id = $1), 0) + 1,
$6
RETURNING 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 { type ExamPrepCreateUnitModuleLessonParams struct {
UnitModuleID int64 `json:"unit_module_id"` UnitModuleID int64 `json:"unit_module_id"`
Title string `json:"title"` Title string `json:"title"`
VideoUrl pgtype.Text `json:"video_url"` VideoUrl pgtype.Text `json:"video_url"`
Thumbnail pgtype.Text `json:"thumbnail"` Thumbnail pgtype.Text `json:"thumbnail"`
Description pgtype.Text `json:"description"` Description pgtype.Text `json:"description"`
PublishStatus string `json:"publish_status"`
} }
func (q *Queries) ExamPrepCreateUnitModuleLesson(ctx context.Context, arg ExamPrepCreateUnitModuleLessonParams) (ExamPrepUnitModuleLesson, error) { 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.VideoUrl,
arg.Thumbnail, arg.Thumbnail,
arg.Description, arg.Description,
arg.PublishStatus,
) )
var i ExamPrepUnitModuleLesson var i ExamPrepUnitModuleLesson
err := row.Scan( err := row.Scan(
@ -56,6 +59,7 @@ func (q *Queries) ExamPrepCreateUnitModuleLesson(ctx context.Context, arg ExamPr
&i.SortOrder, &i.SortOrder,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.PublishStatus,
) )
return i, err return i, err
} }
@ -72,27 +76,29 @@ func (q *Queries) ExamPrepDeleteUnitModuleLesson(ctx context.Context, id int64)
const ExamPrepGetUnitModuleLessonByID = `-- name: ExamPrepGetUnitModuleLessonByID :one const ExamPrepGetUnitModuleLessonByID = `-- name: ExamPrepGetUnitModuleLessonByID :one
SELECT 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 ( EXISTS (
SELECT 1 SELECT 1
FROM exam_prep.lesson_practices p FROM exam_prep.lesson_practices p
WHERE p.unit_module_lesson_id = l.id WHERE p.unit_module_lesson_id = l.id
AND p.publish_status = 'PUBLISHED'
) AS has_practice ) AS has_practice
FROM exam_prep.unit_module_lessons l FROM exam_prep.unit_module_lessons l
WHERE l.id = $1 WHERE l.id = $1
` `
type ExamPrepGetUnitModuleLessonByIDRow struct { type ExamPrepGetUnitModuleLessonByIDRow struct {
ID int64 `json:"id"` ID int64 `json:"id"`
UnitModuleID int64 `json:"unit_module_id"` UnitModuleID int64 `json:"unit_module_id"`
Title string `json:"title"` Title string `json:"title"`
VideoUrl pgtype.Text `json:"video_url"` VideoUrl pgtype.Text `json:"video_url"`
Thumbnail pgtype.Text `json:"thumbnail"` Thumbnail pgtype.Text `json:"thumbnail"`
Description pgtype.Text `json:"description"` Description pgtype.Text `json:"description"`
SortOrder int32 `json:"sort_order"` SortOrder int32 `json:"sort_order"`
CreatedAt pgtype.Timestamptz `json:"created_at"` CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"` UpdatedAt pgtype.Timestamptz `json:"updated_at"`
HasPractice bool `json:"has_practice"` PublishStatus string `json:"publish_status"`
HasPractice bool `json:"has_practice"`
} }
func (q *Queries) ExamPrepGetUnitModuleLessonByID(ctx context.Context, id int64) (ExamPrepGetUnitModuleLessonByIDRow, error) { 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.SortOrder,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.PublishStatus,
&i.HasPractice, &i.HasPractice,
) )
return i, err 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 const ExamPrepListUnitModuleLessonIDsByUnitModule = `-- name: ExamPrepListUnitModuleLessonIDsByUnitModule :many
SELECT SELECT
l.id l.id
@ -153,16 +192,22 @@ SELECT
l.thumbnail, l.thumbnail,
l.description, l.description,
l.sort_order, l.sort_order,
l.publish_status,
EXISTS ( EXISTS (
SELECT 1 SELECT 1
FROM exam_prep.lesson_practices p FROM exam_prep.lesson_practices p
WHERE p.unit_module_lesson_id = l.id WHERE p.unit_module_lesson_id = l.id
AND p.publish_status = 'PUBLISHED'
) AS has_practice, ) AS has_practice,
l.created_at, l.created_at,
l.updated_at l.updated_at
FROM exam_prep.unit_module_lessons l FROM exam_prep.unit_module_lessons l
WHERE WHERE
l.unit_module_id = $1 l.unit_module_id = $1
AND (
$4::boolean = FALSE
OR l.publish_status = 'PUBLISHED'::TEXT
)
ORDER BY ORDER BY
l.sort_order ASC, l.sort_order ASC,
l.id ASC l.id ASC
@ -171,27 +216,34 @@ OFFSET $3
` `
type ExamPrepListUnitModuleLessonsByUnitModuleIDParams struct { type ExamPrepListUnitModuleLessonsByUnitModuleIDParams struct {
UnitModuleID int64 `json:"unit_module_id"` UnitModuleID int64 `json:"unit_module_id"`
Limit int32 `json:"limit"` Limit int32 `json:"limit"`
Offset int32 `json:"offset"` Offset int32 `json:"offset"`
PublishedOnly bool `json:"published_only"`
} }
type ExamPrepListUnitModuleLessonsByUnitModuleIDRow struct { type ExamPrepListUnitModuleLessonsByUnitModuleIDRow struct {
TotalCount int64 `json:"total_count"` TotalCount int64 `json:"total_count"`
ID int64 `json:"id"` ID int64 `json:"id"`
UnitModuleID int64 `json:"unit_module_id"` UnitModuleID int64 `json:"unit_module_id"`
Title string `json:"title"` Title string `json:"title"`
VideoUrl pgtype.Text `json:"video_url"` VideoUrl pgtype.Text `json:"video_url"`
Thumbnail pgtype.Text `json:"thumbnail"` Thumbnail pgtype.Text `json:"thumbnail"`
Description pgtype.Text `json:"description"` Description pgtype.Text `json:"description"`
SortOrder int32 `json:"sort_order"` SortOrder int32 `json:"sort_order"`
HasPractice bool `json:"has_practice"` PublishStatus string `json:"publish_status"`
CreatedAt pgtype.Timestamptz `json:"created_at"` HasPractice bool `json:"has_practice"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"` CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
} }
func (q *Queries) ExamPrepListUnitModuleLessonsByUnitModuleID(ctx context.Context, arg ExamPrepListUnitModuleLessonsByUnitModuleIDParams) ([]ExamPrepListUnitModuleLessonsByUnitModuleIDRow, error) { 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 { if err != nil {
return nil, err return nil, err
} }
@ -208,6 +260,7 @@ func (q *Queries) ExamPrepListUnitModuleLessonsByUnitModuleID(ctx context.Contex
&i.Thumbnail, &i.Thumbnail,
&i.Description, &i.Description,
&i.SortOrder, &i.SortOrder,
&i.PublishStatus,
&i.HasPractice, &i.HasPractice,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
@ -230,19 +283,21 @@ SET
thumbnail = coalesce($3::text, thumbnail), thumbnail = coalesce($3::text, thumbnail),
description = coalesce($4::text, description), description = coalesce($4::text, description),
sort_order = coalesce($5::int, sort_order), sort_order = coalesce($5::int, sort_order),
publish_status = coalesce($6::varchar, publish_status),
updated_at = CURRENT_TIMESTAMP updated_at = CURRENT_TIMESTAMP
WHERE id = $6 WHERE id = $7
RETURNING 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 { type ExamPrepUpdateUnitModuleLessonParams struct {
Title pgtype.Text `json:"title"` Title pgtype.Text `json:"title"`
VideoUrl pgtype.Text `json:"video_url"` VideoUrl pgtype.Text `json:"video_url"`
Thumbnail pgtype.Text `json:"thumbnail"` Thumbnail pgtype.Text `json:"thumbnail"`
Description pgtype.Text `json:"description"` Description pgtype.Text `json:"description"`
SortOrder pgtype.Int4 `json:"sort_order"` SortOrder pgtype.Int4 `json:"sort_order"`
ID int64 `json:"id"` PublishStatus pgtype.Text `json:"publish_status"`
ID int64 `json:"id"`
} }
func (q *Queries) ExamPrepUpdateUnitModuleLesson(ctx context.Context, arg ExamPrepUpdateUnitModuleLessonParams) (ExamPrepUnitModuleLesson, error) { 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.Thumbnail,
arg.Description, arg.Description,
arg.SortOrder, arg.SortOrder,
arg.PublishStatus,
arg.ID, arg.ID,
) )
var i ExamPrepUnitModuleLesson var i ExamPrepUnitModuleLesson
@ -265,6 +321,7 @@ func (q *Queries) ExamPrepUpdateUnitModuleLesson(ctx context.Context, arg ExamPr
&i.SortOrder, &i.SortOrder,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.PublishStatus,
) )
return i, err return i, err
} }

View File

@ -12,7 +12,7 @@ import (
) )
const ExamPrepCreateUnitModule = `-- name: ExamPrepCreateUnitModule :one 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 SELECT
$1, $1,
$2, $2,
@ -24,17 +24,19 @@ SELECT
max(m.sort_order) max(m.sort_order)
FROM exam_prep.unit_modules m FROM exam_prep.unit_modules m
WHERE WHERE
m.unit_id = $1), 0) + 1 m.unit_id = $1), 0) + 1,
$6
RETURNING 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 { type ExamPrepCreateUnitModuleParams struct {
UnitID int64 `json:"unit_id"` UnitID int64 `json:"unit_id"`
Name string `json:"name"` Name string `json:"name"`
Description pgtype.Text `json:"description"` Description pgtype.Text `json:"description"`
Thumbnail pgtype.Text `json:"thumbnail"` Thumbnail pgtype.Text `json:"thumbnail"`
Icon pgtype.Text `json:"icon"` Icon pgtype.Text `json:"icon"`
PublishStatus string `json:"publish_status"`
} }
func (q *Queries) ExamPrepCreateUnitModule(ctx context.Context, arg ExamPrepCreateUnitModuleParams) (ExamPrepUnitModule, error) { 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.Description,
arg.Thumbnail, arg.Thumbnail,
arg.Icon, arg.Icon,
arg.PublishStatus,
) )
var i ExamPrepUnitModule var i ExamPrepUnitModule
err := row.Scan( err := row.Scan(
@ -56,6 +59,7 @@ func (q *Queries) ExamPrepCreateUnitModule(ctx context.Context, arg ExamPrepCrea
&i.SortOrder, &i.SortOrder,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.PublishStatus,
) )
return i, err return i, err
} }
@ -72,28 +76,31 @@ func (q *Queries) ExamPrepDeleteUnitModule(ctx context.Context, id int64) error
const ExamPrepGetUnitModuleByID = `-- name: ExamPrepGetUnitModuleByID :one const ExamPrepGetUnitModuleByID = `-- name: ExamPrepGetUnitModuleByID :one
SELECT 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 ( EXISTS (
SELECT 1 SELECT 1
FROM exam_prep.lesson_practices p 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_module_lessons l ON l.id = p.unit_module_lesson_id
WHERE l.unit_module_id = m.id WHERE l.unit_module_id = m.id
AND p.publish_status = 'PUBLISHED'
AND l.publish_status = 'PUBLISHED'
) AS has_practice ) AS has_practice
FROM exam_prep.unit_modules m FROM exam_prep.unit_modules m
WHERE m.id = $1 WHERE m.id = $1
` `
type ExamPrepGetUnitModuleByIDRow struct { type ExamPrepGetUnitModuleByIDRow struct {
ID int64 `json:"id"` ID int64 `json:"id"`
UnitID int64 `json:"unit_id"` UnitID int64 `json:"unit_id"`
Name string `json:"name"` Name string `json:"name"`
Description pgtype.Text `json:"description"` Description pgtype.Text `json:"description"`
Thumbnail pgtype.Text `json:"thumbnail"` Thumbnail pgtype.Text `json:"thumbnail"`
Icon pgtype.Text `json:"icon"` Icon pgtype.Text `json:"icon"`
SortOrder int32 `json:"sort_order"` SortOrder int32 `json:"sort_order"`
CreatedAt pgtype.Timestamptz `json:"created_at"` CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"` UpdatedAt pgtype.Timestamptz `json:"updated_at"`
HasPractice bool `json:"has_practice"` PublishStatus string `json:"publish_status"`
HasPractice bool `json:"has_practice"`
} }
func (q *Queries) ExamPrepGetUnitModuleByID(ctx context.Context, id int64) (ExamPrepGetUnitModuleByIDRow, error) { 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.SortOrder,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.PublishStatus,
&i.HasPractice, &i.HasPractice,
) )
return i, err 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 const ExamPrepListUnitModuleIDsByUnit = `-- name: ExamPrepListUnitModuleIDsByUnit :many
SELECT SELECT
m.id m.id
@ -152,7 +192,9 @@ WITH module_counts AS (
COUNT(DISTINCT p.id)::BIGINT AS practices_count COUNT(DISTINCT p.id)::BIGINT AS practices_count
FROM exam_prep.unit_modules m FROM exam_prep.unit_modules m
LEFT JOIN exam_prep.unit_module_lessons l ON l.unit_module_id = m.id 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 LEFT JOIN exam_prep.lesson_practices p ON p.unit_module_lesson_id = l.id
AND p.publish_status = 'PUBLISHED'
GROUP BY m.id GROUP BY m.id
) )
SELECT SELECT
@ -164,6 +206,7 @@ SELECT
m.thumbnail, m.thumbnail,
m.icon, m.icon,
m.sort_order, m.sort_order,
m.publish_status,
COALESCE(mc.lessons_count, 0)::BIGINT AS lessons_count, COALESCE(mc.lessons_count, 0)::BIGINT AS lessons_count,
COALESCE(mc.practices_count, 0)::BIGINT AS practices_count, COALESCE(mc.practices_count, 0)::BIGINT AS practices_count,
(COALESCE(mc.practices_count, 0)::BIGINT > 0) AS has_practice, (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 LEFT JOIN module_counts mc ON mc.module_id = m.id
WHERE WHERE
m.unit_id = $1 m.unit_id = $1
AND (
$4::boolean = FALSE
OR m.publish_status = 'PUBLISHED'::TEXT
)
ORDER BY ORDER BY
m.sort_order ASC, m.sort_order ASC,
m.id ASC m.id ASC
@ -181,9 +228,10 @@ OFFSET $3
` `
type ExamPrepListUnitModulesByUnitParams struct { type ExamPrepListUnitModulesByUnitParams struct {
UnitID int64 `json:"unit_id"` UnitID int64 `json:"unit_id"`
Limit int32 `json:"limit"` Limit int32 `json:"limit"`
Offset int32 `json:"offset"` Offset int32 `json:"offset"`
PublishedOnly bool `json:"published_only"`
} }
type ExamPrepListUnitModulesByUnitRow struct { type ExamPrepListUnitModulesByUnitRow struct {
@ -195,6 +243,7 @@ type ExamPrepListUnitModulesByUnitRow struct {
Thumbnail pgtype.Text `json:"thumbnail"` Thumbnail pgtype.Text `json:"thumbnail"`
Icon pgtype.Text `json:"icon"` Icon pgtype.Text `json:"icon"`
SortOrder int32 `json:"sort_order"` SortOrder int32 `json:"sort_order"`
PublishStatus string `json:"publish_status"`
LessonsCount int64 `json:"lessons_count"` LessonsCount int64 `json:"lessons_count"`
PracticesCount int64 `json:"practices_count"` PracticesCount int64 `json:"practices_count"`
HasPractice bool `json:"has_practice"` HasPractice bool `json:"has_practice"`
@ -203,7 +252,12 @@ type ExamPrepListUnitModulesByUnitRow struct {
} }
func (q *Queries) ExamPrepListUnitModulesByUnit(ctx context.Context, arg ExamPrepListUnitModulesByUnitParams) ([]ExamPrepListUnitModulesByUnitRow, error) { 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 { if err != nil {
return nil, err return nil, err
} }
@ -220,6 +274,7 @@ func (q *Queries) ExamPrepListUnitModulesByUnit(ctx context.Context, arg ExamPre
&i.Thumbnail, &i.Thumbnail,
&i.Icon, &i.Icon,
&i.SortOrder, &i.SortOrder,
&i.PublishStatus,
&i.LessonsCount, &i.LessonsCount,
&i.PracticesCount, &i.PracticesCount,
&i.HasPractice, &i.HasPractice,
@ -244,19 +299,21 @@ SET
thumbnail = coalesce($3::text, thumbnail), thumbnail = coalesce($3::text, thumbnail),
icon = coalesce($4::text, icon), icon = coalesce($4::text, icon),
sort_order = coalesce($5::int, sort_order), sort_order = coalesce($5::int, sort_order),
publish_status = coalesce($6::varchar, publish_status),
updated_at = CURRENT_TIMESTAMP updated_at = CURRENT_TIMESTAMP
WHERE id = $6 WHERE id = $7
RETURNING 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 { type ExamPrepUpdateUnitModuleParams struct {
Name pgtype.Text `json:"name"` Name pgtype.Text `json:"name"`
Description pgtype.Text `json:"description"` Description pgtype.Text `json:"description"`
Thumbnail pgtype.Text `json:"thumbnail"` Thumbnail pgtype.Text `json:"thumbnail"`
Icon pgtype.Text `json:"icon"` Icon pgtype.Text `json:"icon"`
SortOrder pgtype.Int4 `json:"sort_order"` SortOrder pgtype.Int4 `json:"sort_order"`
ID int64 `json:"id"` PublishStatus pgtype.Text `json:"publish_status"`
ID int64 `json:"id"`
} }
func (q *Queries) ExamPrepUpdateUnitModule(ctx context.Context, arg ExamPrepUpdateUnitModuleParams) (ExamPrepUnitModule, error) { 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.Thumbnail,
arg.Icon, arg.Icon,
arg.SortOrder, arg.SortOrder,
arg.PublishStatus,
arg.ID, arg.ID,
) )
var i ExamPrepUnitModule var i ExamPrepUnitModule
@ -279,6 +337,7 @@ func (q *Queries) ExamPrepUpdateUnitModule(ctx context.Context, arg ExamPrepUpda
&i.SortOrder, &i.SortOrder,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.PublishStatus,
) )
return i, err return i, err
} }

View File

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

View File

@ -12,7 +12,7 @@ import (
) )
const CreateCourse = `-- name: CreateCourse :one 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 SELECT
$1, $1,
$2, $2,
@ -24,17 +24,19 @@ SELECT
max(c.sort_order) max(c.sort_order)
FROM courses c FROM courses c
WHERE WHERE
c.program_id = $1), 0) + 1) c.program_id = $1), 0) + 1),
$6
RETURNING 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 { type CreateCourseParams struct {
ProgramID int64 `json:"program_id"` ProgramID int64 `json:"program_id"`
Name string `json:"name"` Name string `json:"name"`
Description pgtype.Text `json:"description"` Description pgtype.Text `json:"description"`
Thumbnail pgtype.Text `json:"thumbnail"` Thumbnail pgtype.Text `json:"thumbnail"`
SortOrder pgtype.Int4 `json:"sort_order"` SortOrder pgtype.Int4 `json:"sort_order"`
PublishStatus string `json:"publish_status"`
} }
func (q *Queries) CreateCourse(ctx context.Context, arg CreateCourseParams) (Course, error) { 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.Description,
arg.Thumbnail, arg.Thumbnail,
arg.SortOrder, arg.SortOrder,
arg.PublishStatus,
) )
var i Course var i Course
err := row.Scan( err := row.Scan(
@ -55,6 +58,7 @@ func (q *Queries) CreateCourse(ctx context.Context, arg CreateCourseParams) (Cou
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.SortOrder, &i.SortOrder,
&i.PublishStatus,
) )
return i, err return i, err
} }
@ -71,7 +75,7 @@ func (q *Queries) DeleteCourse(ctx context.Context, id int64) error {
const GetCourseByID = `-- name: GetCourseByID :one const GetCourseByID = `-- name: GetCourseByID :one
SELECT 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 ( EXISTS (
SELECT 1 SELECT 1
FROM lms_practices p FROM lms_practices p
@ -86,15 +90,16 @@ WHERE c.id = $1
` `
type GetCourseByIDRow struct { type GetCourseByIDRow struct {
ID int64 `json:"id"` ID int64 `json:"id"`
ProgramID int64 `json:"program_id"` ProgramID int64 `json:"program_id"`
Name string `json:"name"` Name string `json:"name"`
Description pgtype.Text `json:"description"` Description pgtype.Text `json:"description"`
Thumbnail pgtype.Text `json:"thumbnail"` Thumbnail pgtype.Text `json:"thumbnail"`
CreatedAt pgtype.Timestamptz `json:"created_at"` CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"` UpdatedAt pgtype.Timestamptz `json:"updated_at"`
SortOrder int32 `json:"sort_order"` SortOrder int32 `json:"sort_order"`
HasPractice bool `json:"has_practice"` PublishStatus string `json:"publish_status"`
HasPractice bool `json:"has_practice"`
} }
func (q *Queries) GetCourseByID(ctx context.Context, id int64) (GetCourseByIDRow, error) { 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.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.SortOrder, &i.SortOrder,
&i.PublishStatus,
&i.HasPractice, &i.HasPractice,
) )
return i, err return i, err
@ -154,6 +160,7 @@ SELECT
c.description, c.description,
c.thumbnail, c.thumbnail,
c.sort_order, c.sort_order,
c.publish_status,
c.created_at, c.created_at,
c.updated_at, c.updated_at,
( (
@ -162,7 +169,8 @@ SELECT
FROM FROM
modules m modules m
WHERE WHERE
m.course_id = c.id) AS module_count, m.course_id = c.id
AND m.publish_status = 'PUBLISHED') AS module_count,
( (
SELECT SELECT
COUNT(*)::bigint COUNT(*)::bigint
@ -196,6 +204,10 @@ FROM
courses c courses c
WHERE WHERE
c.program_id = $1 c.program_id = $1
AND (
$4::boolean = FALSE
OR c.publish_status = 'PUBLISHED'::TEXT
)
ORDER BY ORDER BY
c.sort_order ASC, c.sort_order ASC,
c.id ASC c.id ASC
@ -203,9 +215,10 @@ LIMIT $2 OFFSET $3
` `
type ListCoursesByProgramIDParams struct { type ListCoursesByProgramIDParams struct {
ProgramID int64 `json:"program_id"` ProgramID int64 `json:"program_id"`
Limit int32 `json:"limit"` Limit int32 `json:"limit"`
Offset int32 `json:"offset"` Offset int32 `json:"offset"`
PublishedOnly bool `json:"published_only"`
} }
type ListCoursesByProgramIDRow struct { type ListCoursesByProgramIDRow struct {
@ -216,6 +229,7 @@ type ListCoursesByProgramIDRow struct {
Description pgtype.Text `json:"description"` Description pgtype.Text `json:"description"`
Thumbnail pgtype.Text `json:"thumbnail"` Thumbnail pgtype.Text `json:"thumbnail"`
SortOrder int32 `json:"sort_order"` SortOrder int32 `json:"sort_order"`
PublishStatus string `json:"publish_status"`
CreatedAt pgtype.Timestamptz `json:"created_at"` CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"` UpdatedAt pgtype.Timestamptz `json:"updated_at"`
ModuleCount int64 `json:"module_count"` ModuleCount int64 `json:"module_count"`
@ -225,7 +239,12 @@ type ListCoursesByProgramIDRow struct {
} }
func (q *Queries) ListCoursesByProgramID(ctx context.Context, arg ListCoursesByProgramIDParams) ([]ListCoursesByProgramIDRow, error) { 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 { if err != nil {
return nil, err return nil, err
} }
@ -241,6 +260,7 @@ func (q *Queries) ListCoursesByProgramID(ctx context.Context, arg ListCoursesByP
&i.Description, &i.Description,
&i.Thumbnail, &i.Thumbnail,
&i.SortOrder, &i.SortOrder,
&i.PublishStatus,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.ModuleCount, &i.ModuleCount,
@ -258,6 +278,39 @@ func (q *Queries) ListCoursesByProgramID(ctx context.Context, arg ListCoursesByP
return items, nil 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 const UpdateCourse = `-- name: UpdateCourse :one
UPDATE courses UPDATE courses
SET SET
@ -265,19 +318,21 @@ SET
description = COALESCE($2::text, description), description = COALESCE($2::text, description),
thumbnail = COALESCE($3::text, thumbnail), thumbnail = COALESCE($3::text, thumbnail),
sort_order = coalesce($4::int, sort_order), sort_order = coalesce($4::int, sort_order),
publish_status = COALESCE($5::varchar, publish_status),
updated_at = CURRENT_TIMESTAMP updated_at = CURRENT_TIMESTAMP
WHERE WHERE
id = $5 id = $6
RETURNING 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 { type UpdateCourseParams struct {
Name pgtype.Text `json:"name"` Name pgtype.Text `json:"name"`
Description pgtype.Text `json:"description"` Description pgtype.Text `json:"description"`
Thumbnail pgtype.Text `json:"thumbnail"` Thumbnail pgtype.Text `json:"thumbnail"`
SortOrder pgtype.Int4 `json:"sort_order"` SortOrder pgtype.Int4 `json:"sort_order"`
ID int64 `json:"id"` PublishStatus pgtype.Text `json:"publish_status"`
ID int64 `json:"id"`
} }
func (q *Queries) UpdateCourse(ctx context.Context, arg UpdateCourseParams) (Course, error) { 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.Description,
arg.Thumbnail, arg.Thumbnail,
arg.SortOrder, arg.SortOrder,
arg.PublishStatus,
arg.ID, arg.ID,
) )
var i Course var i Course
@ -298,6 +354,7 @@ func (q *Queries) UpdateCourse(ctx context.Context, arg UpdateCourseParams) (Cou
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.SortOrder, &i.SortOrder,
&i.PublishStatus,
) )
return i, err return i, err
} }

View File

@ -12,7 +12,7 @@ import (
) )
const CreateModule = `-- name: CreateModule :one 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 SELECT
$1, $1,
$2, $2,
@ -25,18 +25,20 @@ SELECT
max(m.sort_order) max(m.sort_order)
FROM modules m FROM modules m
WHERE WHERE
m.course_id = $2), 0) + 1) m.course_id = $2), 0) + 1),
$7
RETURNING 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 { type CreateModuleParams struct {
ProgramID int64 `json:"program_id"` ProgramID int64 `json:"program_id"`
CourseID int64 `json:"course_id"` CourseID int64 `json:"course_id"`
Name string `json:"name"` Name string `json:"name"`
Description pgtype.Text `json:"description"` Description pgtype.Text `json:"description"`
Icon pgtype.Text `json:"icon"` Icon pgtype.Text `json:"icon"`
SortOrder pgtype.Int4 `json:"sort_order"` SortOrder pgtype.Int4 `json:"sort_order"`
PublishStatus string `json:"publish_status"`
} }
func (q *Queries) CreateModule(ctx context.Context, arg CreateModuleParams) (Module, error) { 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.Description,
arg.Icon, arg.Icon,
arg.SortOrder, arg.SortOrder,
arg.PublishStatus,
) )
var i Module var i Module
err := row.Scan( err := row.Scan(
@ -59,6 +62,7 @@ func (q *Queries) CreateModule(ctx context.Context, arg CreateModuleParams) (Mod
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.SortOrder, &i.SortOrder,
&i.PublishStatus,
) )
return i, err return i, err
} }
@ -75,7 +79,7 @@ func (q *Queries) DeleteModule(ctx context.Context, id int64) error {
const GetModuleByID = `-- name: GetModuleByID :one const GetModuleByID = `-- name: GetModuleByID :one
SELECT 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 ( EXISTS (
SELECT 1 SELECT 1
FROM lms_practices p FROM lms_practices p
@ -89,16 +93,17 @@ WHERE m.id = $1
` `
type GetModuleByIDRow struct { type GetModuleByIDRow struct {
ID int64 `json:"id"` ID int64 `json:"id"`
ProgramID int64 `json:"program_id"` ProgramID int64 `json:"program_id"`
CourseID int64 `json:"course_id"` CourseID int64 `json:"course_id"`
Name string `json:"name"` Name string `json:"name"`
Description pgtype.Text `json:"description"` Description pgtype.Text `json:"description"`
Icon pgtype.Text `json:"icon"` Icon pgtype.Text `json:"icon"`
CreatedAt pgtype.Timestamptz `json:"created_at"` CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"` UpdatedAt pgtype.Timestamptz `json:"updated_at"`
SortOrder int32 `json:"sort_order"` SortOrder int32 `json:"sort_order"`
HasPractice bool `json:"has_practice"` PublishStatus string `json:"publish_status"`
HasPractice bool `json:"has_practice"`
} }
func (q *Queries) GetModuleByID(ctx context.Context, id int64) (GetModuleByIDRow, error) { 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.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.SortOrder, &i.SortOrder,
&i.PublishStatus,
&i.HasPractice, &i.HasPractice,
) )
return i, err return i, err
@ -160,6 +166,7 @@ SELECT
m.description, m.description,
m.icon, m.icon,
m.sort_order, m.sort_order,
m.publish_status,
m.created_at, m.created_at,
m.updated_at, m.updated_at,
EXISTS ( EXISTS (
@ -174,6 +181,10 @@ FROM
WHERE WHERE
m.program_id = $1 m.program_id = $1
AND m.course_id = $2 AND m.course_id = $2
AND (
$5::boolean = FALSE
OR m.publish_status = 'PUBLISHED'::TEXT
)
ORDER BY ORDER BY
m.sort_order ASC, m.sort_order ASC,
m.id ASC m.id ASC
@ -182,24 +193,26 @@ OFFSET $4
` `
type ListModulesByProgramAndCourseParams struct { type ListModulesByProgramAndCourseParams struct {
ProgramID int64 `json:"program_id"` ProgramID int64 `json:"program_id"`
CourseID int64 `json:"course_id"` CourseID int64 `json:"course_id"`
Limit int32 `json:"limit"` Limit int32 `json:"limit"`
Offset int32 `json:"offset"` Offset int32 `json:"offset"`
PublishedOnly bool `json:"published_only"`
} }
type ListModulesByProgramAndCourseRow struct { type ListModulesByProgramAndCourseRow struct {
TotalCount int64 `json:"total_count"` TotalCount int64 `json:"total_count"`
ID int64 `json:"id"` ID int64 `json:"id"`
ProgramID int64 `json:"program_id"` ProgramID int64 `json:"program_id"`
CourseID int64 `json:"course_id"` CourseID int64 `json:"course_id"`
Name string `json:"name"` Name string `json:"name"`
Description pgtype.Text `json:"description"` Description pgtype.Text `json:"description"`
Icon pgtype.Text `json:"icon"` Icon pgtype.Text `json:"icon"`
SortOrder int32 `json:"sort_order"` SortOrder int32 `json:"sort_order"`
CreatedAt pgtype.Timestamptz `json:"created_at"` PublishStatus string `json:"publish_status"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"` CreatedAt pgtype.Timestamptz `json:"created_at"`
HasPractice bool `json:"has_practice"` UpdatedAt pgtype.Timestamptz `json:"updated_at"`
HasPractice bool `json:"has_practice"`
} }
func (q *Queries) ListModulesByProgramAndCourse(ctx context.Context, arg ListModulesByProgramAndCourseParams) ([]ListModulesByProgramAndCourseRow, error) { 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.CourseID,
arg.Limit, arg.Limit,
arg.Offset, arg.Offset,
arg.PublishedOnly,
) )
if err != nil { if err != nil {
return nil, err return nil, err
@ -225,6 +239,7 @@ func (q *Queries) ListModulesByProgramAndCourse(ctx context.Context, arg ListMod
&i.Description, &i.Description,
&i.Icon, &i.Icon,
&i.SortOrder, &i.SortOrder,
&i.PublishStatus,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.HasPractice, &i.HasPractice,
@ -239,6 +254,39 @@ func (q *Queries) ListModulesByProgramAndCourse(ctx context.Context, arg ListMod
return items, nil 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 const UpdateModule = `-- name: UpdateModule :one
UPDATE modules UPDATE modules
SET SET
@ -246,19 +294,21 @@ SET
description = COALESCE($2::text, description), description = COALESCE($2::text, description),
icon = COALESCE($3::text, icon), icon = COALESCE($3::text, icon),
sort_order = coalesce($4::int, sort_order), sort_order = coalesce($4::int, sort_order),
publish_status = COALESCE($5::varchar, publish_status),
updated_at = CURRENT_TIMESTAMP updated_at = CURRENT_TIMESTAMP
WHERE WHERE
id = $5 id = $6
RETURNING 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 { type UpdateModuleParams struct {
Name pgtype.Text `json:"name"` Name pgtype.Text `json:"name"`
Description pgtype.Text `json:"description"` Description pgtype.Text `json:"description"`
Icon pgtype.Text `json:"icon"` Icon pgtype.Text `json:"icon"`
SortOrder pgtype.Int4 `json:"sort_order"` SortOrder pgtype.Int4 `json:"sort_order"`
ID int64 `json:"id"` PublishStatus pgtype.Text `json:"publish_status"`
ID int64 `json:"id"`
} }
func (q *Queries) UpdateModule(ctx context.Context, arg UpdateModuleParams) (Module, error) { 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.Description,
arg.Icon, arg.Icon,
arg.SortOrder, arg.SortOrder,
arg.PublishStatus,
arg.ID, arg.ID,
) )
var i Module var i Module
@ -280,6 +331,7 @@ func (q *Queries) UpdateModule(ctx context.Context, arg UpdateModuleParams) (Mod
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.SortOrder, &i.SortOrder,
&i.PublishStatus,
) )
return i, err return i, err
} }

View File

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

View File

@ -23,14 +23,15 @@ type ActivityLog struct {
} }
type Course struct { type Course struct {
ID int64 `json:"id"` ID int64 `json:"id"`
ProgramID int64 `json:"program_id"` ProgramID int64 `json:"program_id"`
Name string `json:"name"` Name string `json:"name"`
Description pgtype.Text `json:"description"` Description pgtype.Text `json:"description"`
Thumbnail pgtype.Text `json:"thumbnail"` Thumbnail pgtype.Text `json:"thumbnail"`
CreatedAt pgtype.Timestamptz `json:"created_at"` CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"` UpdatedAt pgtype.Timestamptz `json:"updated_at"`
SortOrder int32 `json:"sort_order"` SortOrder int32 `json:"sort_order"`
PublishStatus string `json:"publish_status"`
} }
type Device struct { type Device struct {
@ -58,14 +59,15 @@ type EmailTemplate struct {
} }
type ExamPrepCatalogCourse struct { type ExamPrepCatalogCourse struct {
ID int64 `json:"id"` ID int64 `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Description pgtype.Text `json:"description"` Description pgtype.Text `json:"description"`
Thumbnail pgtype.Text `json:"thumbnail"` Thumbnail pgtype.Text `json:"thumbnail"`
SortOrder int32 `json:"sort_order"` SortOrder int32 `json:"sort_order"`
CreatedAt pgtype.Timestamptz `json:"created_at"` CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"` UpdatedAt pgtype.Timestamptz `json:"updated_at"`
Category string `json:"category"` Category string `json:"category"`
PublishStatus string `json:"publish_status"`
} }
type ExamPrepLessonPractice struct { type ExamPrepLessonPractice struct {
@ -91,30 +93,33 @@ type ExamPrepUnit struct {
SortOrder int32 `json:"sort_order"` SortOrder int32 `json:"sort_order"`
CreatedAt pgtype.Timestamptz `json:"created_at"` CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"` UpdatedAt pgtype.Timestamptz `json:"updated_at"`
PublishStatus string `json:"publish_status"`
} }
type ExamPrepUnitModule struct { type ExamPrepUnitModule struct {
ID int64 `json:"id"` ID int64 `json:"id"`
UnitID int64 `json:"unit_id"` UnitID int64 `json:"unit_id"`
Name string `json:"name"` Name string `json:"name"`
Description pgtype.Text `json:"description"` Description pgtype.Text `json:"description"`
Thumbnail pgtype.Text `json:"thumbnail"` Thumbnail pgtype.Text `json:"thumbnail"`
Icon pgtype.Text `json:"icon"` Icon pgtype.Text `json:"icon"`
SortOrder int32 `json:"sort_order"` SortOrder int32 `json:"sort_order"`
CreatedAt pgtype.Timestamptz `json:"created_at"` CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"` UpdatedAt pgtype.Timestamptz `json:"updated_at"`
PublishStatus string `json:"publish_status"`
} }
type ExamPrepUnitModuleLesson struct { type ExamPrepUnitModuleLesson struct {
ID int64 `json:"id"` ID int64 `json:"id"`
UnitModuleID int64 `json:"unit_module_id"` UnitModuleID int64 `json:"unit_module_id"`
Title string `json:"title"` Title string `json:"title"`
VideoUrl pgtype.Text `json:"video_url"` VideoUrl pgtype.Text `json:"video_url"`
Thumbnail pgtype.Text `json:"thumbnail"` Thumbnail pgtype.Text `json:"thumbnail"`
Description pgtype.Text `json:"description"` Description pgtype.Text `json:"description"`
SortOrder int32 `json:"sort_order"` SortOrder int32 `json:"sort_order"`
CreatedAt pgtype.Timestamptz `json:"created_at"` CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"` UpdatedAt pgtype.Timestamptz `json:"updated_at"`
PublishStatus string `json:"publish_status"`
} }
type Faq struct { type Faq struct {
@ -230,15 +235,16 @@ type MobileAppVersion struct {
} }
type Module struct { type Module struct {
ID int64 `json:"id"` ID int64 `json:"id"`
ProgramID int64 `json:"program_id"` ProgramID int64 `json:"program_id"`
CourseID int64 `json:"course_id"` CourseID int64 `json:"course_id"`
Name string `json:"name"` Name string `json:"name"`
Description pgtype.Text `json:"description"` Description pgtype.Text `json:"description"`
Icon pgtype.Text `json:"icon"` Icon pgtype.Text `json:"icon"`
CreatedAt pgtype.Timestamptz `json:"created_at"` CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"` UpdatedAt pgtype.Timestamptz `json:"updated_at"`
SortOrder int32 `json:"sort_order"` SortOrder int32 `json:"sort_order"`
PublishStatus string `json:"publish_status"`
} }
type ModuleToSubCourse struct { type ModuleToSubCourse struct {
@ -303,14 +309,15 @@ type Permission struct {
} }
type Program struct { type Program struct {
ID int64 `json:"id"` ID int64 `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Description pgtype.Text `json:"description"` Description pgtype.Text `json:"description"`
Thumbnail pgtype.Text `json:"thumbnail"` Thumbnail pgtype.Text `json:"thumbnail"`
CreatedAt pgtype.Timestamptz `json:"created_at"` CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"` UpdatedAt pgtype.Timestamptz `json:"updated_at"`
SortOrder int32 `json:"sort_order"` SortOrder int32 `json:"sort_order"`
Category string `json:"category"` Category string `json:"category"`
PublishStatus string `json:"publish_status"`
} }
type Question struct { type Question struct {

View File

@ -12,7 +12,7 @@ import (
) )
const CreateProgram = `-- name: CreateProgram :one 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 SELECT
$1, $1,
$2, $2,
@ -21,17 +21,19 @@ SELECT
COALESCE($5::int, COALESCE(( COALESCE($5::int, COALESCE((
SELECT SELECT
max(p.sort_order) max(p.sort_order)
FROM programs AS p), 0) + 1) FROM programs AS p), 0) + 1),
$6
RETURNING 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 { type CreateProgramParams struct {
Name string `json:"name"` Name string `json:"name"`
Description pgtype.Text `json:"description"` Description pgtype.Text `json:"description"`
Category string `json:"category"` Category string `json:"category"`
Thumbnail pgtype.Text `json:"thumbnail"` Thumbnail pgtype.Text `json:"thumbnail"`
SortOrder pgtype.Int4 `json:"sort_order"` SortOrder pgtype.Int4 `json:"sort_order"`
PublishStatus string `json:"publish_status"`
} }
func (q *Queries) CreateProgram(ctx context.Context, arg CreateProgramParams) (Program, error) { 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.Category,
arg.Thumbnail, arg.Thumbnail,
arg.SortOrder, arg.SortOrder,
arg.PublishStatus,
) )
var i Program var i Program
err := row.Scan( err := row.Scan(
@ -52,6 +55,7 @@ func (q *Queries) CreateProgram(ctx context.Context, arg CreateProgramParams) (P
&i.UpdatedAt, &i.UpdatedAt,
&i.SortOrder, &i.SortOrder,
&i.Category, &i.Category,
&i.PublishStatus,
) )
return i, err return i, err
} }
@ -67,7 +71,7 @@ func (q *Queries) DeleteProgram(ctx context.Context, id int64) error {
} }
const GetProgramByID = `-- name: GetProgramByID :one 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 FROM programs
WHERE id = $1 WHERE id = $1
` `
@ -84,6 +88,7 @@ func (q *Queries) GetProgramByID(ctx context.Context, id int64) (Program, error)
&i.UpdatedAt, &i.UpdatedAt,
&i.SortOrder, &i.SortOrder,
&i.Category, &i.Category,
&i.PublishStatus,
) )
return i, err return i, err
} }
@ -126,32 +131,39 @@ SELECT
p.category, p.category,
p.thumbnail, p.thumbnail,
p.sort_order, p.sort_order,
p.publish_status,
p.created_at, p.created_at,
p.updated_at p.updated_at
FROM programs p FROM programs p
WHERE (
$3::boolean = FALSE
OR p.publish_status = 'PUBLISHED'::TEXT
)
ORDER BY p.sort_order ASC, p.id ASC ORDER BY p.sort_order ASC, p.id ASC
LIMIT $1 OFFSET $2 LIMIT $1 OFFSET $2
` `
type ListProgramsParams struct { type ListProgramsParams struct {
Limit int32 `json:"limit"` Limit int32 `json:"limit"`
Offset int32 `json:"offset"` Offset int32 `json:"offset"`
PublishedOnly bool `json:"published_only"`
} }
type ListProgramsRow struct { type ListProgramsRow struct {
TotalCount int64 `json:"total_count"` TotalCount int64 `json:"total_count"`
ID int64 `json:"id"` ID int64 `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Description pgtype.Text `json:"description"` Description pgtype.Text `json:"description"`
Category string `json:"category"` Category string `json:"category"`
Thumbnail pgtype.Text `json:"thumbnail"` Thumbnail pgtype.Text `json:"thumbnail"`
SortOrder int32 `json:"sort_order"` SortOrder int32 `json:"sort_order"`
CreatedAt pgtype.Timestamptz `json:"created_at"` PublishStatus string `json:"publish_status"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"` CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
} }
func (q *Queries) ListPrograms(ctx context.Context, arg ListProgramsParams) ([]ListProgramsRow, error) { 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 { if err != nil {
return nil, err return nil, err
} }
@ -167,6 +179,7 @@ func (q *Queries) ListPrograms(ctx context.Context, arg ListProgramsParams) ([]L
&i.Category, &i.Category,
&i.Thumbnail, &i.Thumbnail,
&i.SortOrder, &i.SortOrder,
&i.PublishStatus,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
); err != nil { ); err != nil {
@ -188,20 +201,22 @@ SET
category = COALESCE($3::varchar, category), category = COALESCE($3::varchar, category),
thumbnail = COALESCE($4::text, thumbnail), thumbnail = COALESCE($4::text, thumbnail),
sort_order = coalesce($5::int, sort_order), sort_order = coalesce($5::int, sort_order),
publish_status = COALESCE($6::varchar, publish_status),
updated_at = CURRENT_TIMESTAMP updated_at = CURRENT_TIMESTAMP
WHERE WHERE
id = $6 id = $7
RETURNING 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 { type UpdateProgramParams struct {
Name pgtype.Text `json:"name"` Name pgtype.Text `json:"name"`
Description pgtype.Text `json:"description"` Description pgtype.Text `json:"description"`
Category pgtype.Text `json:"category"` Category pgtype.Text `json:"category"`
Thumbnail pgtype.Text `json:"thumbnail"` Thumbnail pgtype.Text `json:"thumbnail"`
SortOrder pgtype.Int4 `json:"sort_order"` SortOrder pgtype.Int4 `json:"sort_order"`
ID int64 `json:"id"` PublishStatus pgtype.Text `json:"publish_status"`
ID int64 `json:"id"`
} }
func (q *Queries) UpdateProgram(ctx context.Context, arg UpdateProgramParams) (Program, error) { 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.Category,
arg.Thumbnail, arg.Thumbnail,
arg.SortOrder, arg.SortOrder,
arg.PublishStatus,
arg.ID, arg.ID,
) )
var i Program var i Program
@ -223,6 +239,7 @@ func (q *Queries) UpdateProgram(ctx context.Context, arg UpdateProgramParams) (P
&i.UpdatedAt, &i.UpdatedAt,
&i.SortOrder, &i.SortOrder,
&i.Category, &i.Category,
&i.PublishStatus,
) )
return i, err 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. // Course belongs to a Program.
type Course struct { type Course struct {
ID int64 `json:"id"` ID int64 `json:"id"`
ProgramID int64 `json:"program_id"` ProgramID int64 `json:"program_id"`
Name string `json:"name"` Name string `json:"name"`
Description *string `json:"description,omitempty"` Description *string `json:"description,omitempty"`
Thumbnail *string `json:"thumbnail,omitempty"` Thumbnail *string `json:"thumbnail,omitempty"`
SortOrder int `json:"sort_order"` SortOrder int `json:"sort_order"`
CreatedAt time.Time `json:"created_at"` PublishStatus ContentPublishStatus `json:"publish_status"`
UpdatedAt *time.Time `json:"updated_at,omitempty"` 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 // 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). // (not practices attached to a module or lesson under this course).
ModuleCount int `json:"module_count"` ModuleCount int `json:"module_count"`
@ -32,17 +33,25 @@ type Course struct {
Access *LMSEntityAccess `json:"access,omitempty"` 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 { type CreateCourseInput struct {
Name string `json:"name" validate:"required"` Name string `json:"name" validate:"required"`
Description *string `json:"description,omitempty"` Description *string `json:"description,omitempty"`
Thumbnail *string `json:"thumbnail,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 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"` 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 { type UpdateCourseInput struct {
Name *string `json:"name,omitempty"` Name *string `json:"name,omitempty"`
Description *string `json:"description,omitempty"` Description *string `json:"description,omitempty"`
Thumbnail *string `json:"thumbnail,omitempty"` Thumbnail *string `json:"thumbnail,omitempty"`
SortOrder *int `json:"sort_order,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. // 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 { type ExamPrepCatalogCourse struct {
ID int64 `json:"id"` ID int64 `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Description *string `json:"description,omitempty"` Description *string `json:"description,omitempty"`
Category string `json:"category"` Category string `json:"category"`
Thumbnail *string `json:"thumbnail,omitempty"` Thumbnail *string `json:"thumbnail,omitempty"`
SortOrder int `json:"sort_order"` SortOrder int `json:"sort_order"`
UnitsCount *int64 `json:"units_count,omitempty"` PublishStatus ContentPublishStatus `json:"publish_status"`
ModulesCount *int64 `json:"modules_count,omitempty"` UnitsCount *int64 `json:"units_count,omitempty"`
LessonsCount *int64 `json:"lessons_count,omitempty"` ModulesCount *int64 `json:"modules_count,omitempty"`
HasPractice bool `json:"has_practice"` LessonsCount *int64 `json:"lessons_count,omitempty"`
Access *LMSEntityAccess `json:"access,omitempty"` HasPractice bool `json:"has_practice"`
CreatedAt time.Time `json:"created_at"` Access *LMSEntityAccess `json:"access,omitempty"`
UpdatedAt *time.Time `json:"updated_at,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 { type CreateExamPrepCatalogCourseInput struct {
@ -24,12 +30,15 @@ type CreateExamPrepCatalogCourseInput struct {
Description *string `json:"description,omitempty"` Description *string `json:"description,omitempty"`
Category string `json:"category" validate:"required,oneof=IELTS DUOLINGO"` Category string `json:"category" validate:"required,oneof=IELTS DUOLINGO"`
Thumbnail *string `json:"thumbnail,omitempty"` 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 { type UpdateExamPrepCatalogCourseInput struct {
Name *string `json:"name,omitempty"` Name *string `json:"name,omitempty"`
Description *string `json:"description,omitempty"` Description *string `json:"description,omitempty"`
Category *string `json:"category,omitempty" validate:"omitempty,oneof=IELTS DUOLINGO"` Category *string `json:"category,omitempty" validate:"omitempty,oneof=IELTS DUOLINGO"`
Thumbnail *string `json:"thumbnail,omitempty"` Thumbnail *string `json:"thumbnail,omitempty"`
SortOrder *int `json:"sort_order,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). // ExamPrepLesson is a video lesson under an exam-prep unit module (exam_prep.unit_module_lessons).
type ExamPrepLesson struct { type ExamPrepLesson struct {
ID int64 `json:"id"` ID int64 `json:"id"`
UnitModuleID int64 `json:"unit_module_id"` UnitModuleID int64 `json:"unit_module_id"`
Title string `json:"title"` Title string `json:"title"`
VideoURL *string `json:"video_url,omitempty"` VideoURL *string `json:"video_url,omitempty"`
Thumbnail *string `json:"thumbnail,omitempty"` Thumbnail *string `json:"thumbnail,omitempty"`
Description *string `json:"description,omitempty"` Description *string `json:"description,omitempty"`
SortOrder int `json:"sort_order"` SortOrder int `json:"sort_order"`
HasPractice bool `json:"has_practice"` PublishStatus ContentPublishStatus `json:"publish_status"`
Access *LMSEntityAccess `json:"access,omitempty"` HasPractice bool `json:"has_practice"`
CreatedAt time.Time `json:"created_at"` Access *LMSEntityAccess `json:"access,omitempty"`
UpdatedAt *time.Time `json:"updated_at,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 { type CreateExamPrepLessonInput struct {
@ -22,12 +28,15 @@ type CreateExamPrepLessonInput struct {
VideoURL *string `json:"video_url,omitempty"` VideoURL *string `json:"video_url,omitempty"`
Thumbnail *string `json:"thumbnail,omitempty"` Thumbnail *string `json:"thumbnail,omitempty"`
Description *string `json:"description,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 { type UpdateExamPrepLessonInput struct {
Title *string `json:"title,omitempty"` Title *string `json:"title,omitempty"`
VideoURL *string `json:"video_url,omitempty"` VideoURL *string `json:"video_url,omitempty"`
Thumbnail *string `json:"thumbnail,omitempty"` Thumbnail *string `json:"thumbnail,omitempty"`
Description *string `json:"description,omitempty"` Description *string `json:"description,omitempty"`
SortOrder *int `json:"sort_order,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). // ExamPrepModule is a module under an exam-prep unit (stored in exam_prep.unit_modules).
type ExamPrepModule struct { type ExamPrepModule struct {
ID int64 `json:"id"` ID int64 `json:"id"`
UnitID int64 `json:"unit_id"` UnitID int64 `json:"unit_id"`
Name string `json:"name"` Name string `json:"name"`
Description *string `json:"description,omitempty"` Description *string `json:"description,omitempty"`
Thumbnail *string `json:"thumbnail,omitempty"` Thumbnail *string `json:"thumbnail,omitempty"`
Icon *string `json:"icon,omitempty"` Icon *string `json:"icon,omitempty"`
SortOrder int `json:"sort_order"` SortOrder int `json:"sort_order"`
LessonsCount *int64 `json:"lessons_count,omitempty"` PublishStatus ContentPublishStatus `json:"publish_status"`
PracticesCount *int64 `json:"practices_count,omitempty"` LessonsCount *int64 `json:"lessons_count,omitempty"`
HasPractice bool `json:"has_practice"` PracticesCount *int64 `json:"practices_count,omitempty"`
Access *LMSEntityAccess `json:"access,omitempty"` HasPractice bool `json:"has_practice"`
CreatedAt time.Time `json:"created_at"` Access *LMSEntityAccess `json:"access,omitempty"`
UpdatedAt *time.Time `json:"updated_at,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 { type CreateExamPrepModuleInput struct {
@ -24,12 +30,15 @@ type CreateExamPrepModuleInput struct {
Description *string `json:"description,omitempty"` Description *string `json:"description,omitempty"`
Thumbnail *string `json:"thumbnail,omitempty"` Thumbnail *string `json:"thumbnail,omitempty"`
Icon *string `json:"icon,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 { type UpdateExamPrepModuleInput struct {
Name *string `json:"name,omitempty"` Name *string `json:"name,omitempty"`
Description *string `json:"description,omitempty"` Description *string `json:"description,omitempty"`
Thumbnail *string `json:"thumbnail,omitempty"` Thumbnail *string `json:"thumbnail,omitempty"`
Icon *string `json:"icon,omitempty"` Icon *string `json:"icon,omitempty"`
SortOrder *int `json:"sort_order,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). // ExamPrepUnit is a chapter-like grouping under an exam-prep catalog course (schema exam_prep.units).
type ExamPrepUnit struct { type ExamPrepUnit struct {
ID int64 `json:"id"` ID int64 `json:"id"`
CatalogCourseID int64 `json:"catalog_course_id"` CatalogCourseID int64 `json:"catalog_course_id"`
Name string `json:"name"` Name string `json:"name"`
Description *string `json:"description,omitempty"` Description *string `json:"description,omitempty"`
Thumbnail *string `json:"thumbnail,omitempty"` Thumbnail *string `json:"thumbnail,omitempty"`
SortOrder int `json:"sort_order"` SortOrder int `json:"sort_order"`
ModulesCount *int64 `json:"modules_count,omitempty"` PublishStatus ContentPublishStatus `json:"publish_status"`
LessonsCount *int64 `json:"lessons_count,omitempty"` ModulesCount *int64 `json:"modules_count,omitempty"`
PracticesCount *int64 `json:"practices_count,omitempty"` LessonsCount *int64 `json:"lessons_count,omitempty"`
HasPractice bool `json:"has_practice"` PracticesCount *int64 `json:"practices_count,omitempty"`
Access *LMSEntityAccess `json:"access,omitempty"` HasPractice bool `json:"has_practice"`
CreatedAt time.Time `json:"created_at"` Access *LMSEntityAccess `json:"access,omitempty"`
UpdatedAt *time.Time `json:"updated_at,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 { type CreateExamPrepUnitInput struct {
@ -25,11 +31,14 @@ type CreateExamPrepUnitInput struct {
Thumbnail *string `json:"thumbnail,omitempty"` Thumbnail *string `json:"thumbnail,omitempty"`
// SortOrder within the catalog course when set; omit to append after current max sort_order within catalog_course_id. // SortOrder 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"` 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 { type UpdateExamPrepUnitInput struct {
Name *string `json:"name,omitempty"` Name *string `json:"name,omitempty"`
Description *string `json:"description,omitempty"` Description *string `json:"description,omitempty"`
Thumbnail *string `json:"thumbnail,omitempty"` Thumbnail *string `json:"thumbnail,omitempty"`
SortOrder *int `json:"sort_order,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). // Module belongs to a Course. program_id is the courses program (stored for querying; not required from the client on create).
type Module struct { type Module struct {
ID int64 `json:"id"` ID int64 `json:"id"`
ProgramID int64 `json:"program_id"` ProgramID int64 `json:"program_id"`
CourseID int64 `json:"course_id"` CourseID int64 `json:"course_id"`
Name string `json:"name"` Name string `json:"name"`
Description *string `json:"description,omitempty"` Description *string `json:"description,omitempty"`
Icon *string `json:"icon,omitempty"` Icon *string `json:"icon,omitempty"`
SortOrder int `json:"sort_order"` SortOrder int `json:"sort_order"`
HasPractice bool `json:"has_practice"` PublishStatus ContentPublishStatus `json:"publish_status"`
CreatedAt time.Time `json:"created_at"` HasPractice bool `json:"has_practice"`
UpdatedAt *time.Time `json:"updated_at,omitempty"` CreatedAt time.Time `json:"created_at"`
Access *LMSEntityAccess `json:"access,omitempty"` 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 { type CreateModuleInput struct {
@ -23,11 +29,14 @@ type CreateModuleInput struct {
Icon *string `json:"icon,omitempty"` Icon *string `json:"icon,omitempty"`
// SortOrder within the course when set; omit to append after current max within course_id (uniqueness is per-course). // SortOrder 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"` 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 { type UpdateModuleInput struct {
Name *string `json:"name,omitempty"` Name *string `json:"name,omitempty"`
Description *string `json:"description,omitempty"` Description *string `json:"description,omitempty"`
Icon *string `json:"icon,omitempty"` Icon *string `json:"icon,omitempty"`
SortOrder *int `json:"sort_order,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). // Program is the top-level container in the LMS hierarchy (e.g. tracks like Beginner / Intermediate / Advanced).
type Program struct { type Program struct {
ID int64 `json:"id"` ID int64 `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Description *string `json:"description,omitempty"` Description *string `json:"description,omitempty"`
Category string `json:"category"` Category string `json:"category"`
Thumbnail *string `json:"thumbnail,omitempty"` Thumbnail *string `json:"thumbnail,omitempty"`
SortOrder int `json:"sort_order"` SortOrder int `json:"sort_order"`
CreatedAt time.Time `json:"created_at"` PublishStatus ContentPublishStatus `json:"publish_status"`
UpdatedAt *time.Time `json:"updated_at,omitempty"` CreatedAt time.Time `json:"created_at"`
Access *LMSEntityAccess `json:"access,omitempty"` 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 { type CreateProgramInput struct {
@ -22,12 +28,15 @@ type CreateProgramInput struct {
Thumbnail *string `json:"thumbnail,omitempty"` 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 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"` 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 { type UpdateProgramInput struct {
Name *string `json:"name,omitempty"` Name *string `json:"name,omitempty"`
Description *string `json:"description,omitempty"` Description *string `json:"description,omitempty"`
Category *string `json:"category,omitempty" validate:"omitempty,oneof=LEARN_ENGLISH IELTS DUOLINGO"` Category *string `json:"category,omitempty" validate:"omitempty,oneof=LEARN_ENGLISH IELTS DUOLINGO"`
Thumbnail *string `json:"thumbnail,omitempty"` Thumbnail *string `json:"thumbnail,omitempty"`
SortOrder *int `json:"sort_order,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] 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 { func humanizeDefinitionKey(key string) string {
parts := strings.Split(strings.ReplaceAll(key, "-", "_"), "_") parts := strings.Split(strings.ReplaceAll(key, "-", "_"), "_")
for i, p := range parts { 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) { func TestValidateQuestionTextNotAllowedForDynamic(t *testing.T) {
if err := ValidateQuestionTextNotAllowedForDynamic("DYNAMIC", "nope"); err == nil { if err := ValidateQuestionTextNotAllowedForDynamic("DYNAMIC", "nope"); err == nil {
t.Fatal("expected error when question_text sent for DYNAMIC") t.Fatal("expected error when question_text sent for DYNAMIC")

View File

@ -9,7 +9,7 @@ import (
type ExamPrepCatalogCourseStore interface { type ExamPrepCatalogCourseStore interface {
CreateExamPrepCatalogCourse(ctx context.Context, input domain.CreateExamPrepCatalogCourseInput) (domain.ExamPrepCatalogCourse, error) CreateExamPrepCatalogCourse(ctx context.Context, input domain.CreateExamPrepCatalogCourseInput) (domain.ExamPrepCatalogCourse, error)
GetExamPrepCatalogCourseByID(ctx context.Context, id int64) (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) ListAllExamPrepCatalogCourseIDs(ctx context.Context) ([]int64, error)
UpdateExamPrepCatalogCourse(ctx context.Context, id int64, input domain.UpdateExamPrepCatalogCourseInput) (domain.ExamPrepCatalogCourse, error) UpdateExamPrepCatalogCourse(ctx context.Context, id int64, input domain.UpdateExamPrepCatalogCourseInput) (domain.ExamPrepCatalogCourse, error)
DeleteExamPrepCatalogCourse(ctx context.Context, id int64) error DeleteExamPrepCatalogCourse(ctx context.Context, id int64) error

View File

@ -9,8 +9,9 @@ import (
type ExamPrepLessonStore interface { type ExamPrepLessonStore interface {
CreateExamPrepUnitModuleLesson(ctx context.Context, unitModuleID int64, input domain.CreateExamPrepLessonInput) (domain.ExamPrepLesson, error) CreateExamPrepUnitModuleLesson(ctx context.Context, unitModuleID int64, input domain.CreateExamPrepLessonInput) (domain.ExamPrepLesson, error)
GetExamPrepUnitModuleLessonByID(ctx context.Context, id int64) (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) 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) UpdateExamPrepUnitModuleLesson(ctx context.Context, id int64, input domain.UpdateExamPrepLessonInput) (domain.ExamPrepLesson, error)
DeleteExamPrepUnitModuleLesson(ctx context.Context, id int64) error DeleteExamPrepUnitModuleLesson(ctx context.Context, id int64) error
ReorderExamPrepUnitModuleLessonsInUnitModule(ctx context.Context, unitModuleID int64, orderedIDs []int64) error ReorderExamPrepUnitModuleLessonsInUnitModule(ctx context.Context, unitModuleID int64, orderedIDs []int64) error

View File

@ -9,8 +9,9 @@ import (
type ExamPrepModuleStore interface { type ExamPrepModuleStore interface {
CreateExamPrepUnitModule(ctx context.Context, unitID int64, input domain.CreateExamPrepModuleInput) (domain.ExamPrepModule, error) CreateExamPrepUnitModule(ctx context.Context, unitID int64, input domain.CreateExamPrepModuleInput) (domain.ExamPrepModule, error)
GetExamPrepUnitModuleByID(ctx context.Context, id int64) (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) 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) UpdateExamPrepUnitModule(ctx context.Context, id int64, input domain.UpdateExamPrepModuleInput) (domain.ExamPrepModule, error)
DeleteExamPrepUnitModule(ctx context.Context, id int64) error DeleteExamPrepUnitModule(ctx context.Context, id int64) error
ReorderExamPrepUnitModulesInUnit(ctx context.Context, unitID int64, orderedIDs []int64) error ReorderExamPrepUnitModulesInUnit(ctx context.Context, unitID int64, orderedIDs []int64) error

View File

@ -9,8 +9,9 @@ import (
type ExamPrepUnitStore interface { type ExamPrepUnitStore interface {
CreateExamPrepUnit(ctx context.Context, catalogCourseID int64, input domain.CreateExamPrepUnitInput) (domain.ExamPrepUnit, error) CreateExamPrepUnit(ctx context.Context, catalogCourseID int64, input domain.CreateExamPrepUnitInput) (domain.ExamPrepUnit, error)
GetExamPrepUnitByID(ctx context.Context, id int64) (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) 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) UpdateExamPrepUnit(ctx context.Context, id int64, input domain.UpdateExamPrepUnitInput) (domain.ExamPrepUnit, error)
DeleteExamPrepUnit(ctx context.Context, id int64) error DeleteExamPrepUnit(ctx context.Context, id int64) error
ReorderExamPrepUnitsInCatalogCourse(ctx context.Context, catalogCourseID int64, orderedIDs []int64) error ReorderExamPrepUnitsInCatalogCourse(ctx context.Context, catalogCourseID int64, orderedIDs []int64) error

View File

@ -8,8 +8,9 @@ import (
type CourseStore interface { type CourseStore interface {
CreateCourse(ctx context.Context, programID int64, input domain.CreateCourseInput) (domain.Course, error) CreateCourse(ctx context.Context, programID int64, input domain.CreateCourseInput) (domain.Course, error)
GetCourseByID(ctx context.Context, id int64) (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) 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 ReorderCoursesInProgram(ctx context.Context, programID int64, orderedIDs []int64) error
UpdateCourse(ctx context.Context, id int64, input domain.UpdateCourseInput) (domain.Course, error) UpdateCourse(ctx context.Context, id int64, input domain.UpdateCourseInput) (domain.Course, error)
DeleteCourse(ctx context.Context, id int64) error DeleteCourse(ctx context.Context, id int64) error

View File

@ -8,8 +8,9 @@ import (
type ModuleStore interface { type ModuleStore interface {
CreateModule(ctx context.Context, programID, courseID int64, input domain.CreateModuleInput) (domain.Module, error) CreateModule(ctx context.Context, programID, courseID int64, input domain.CreateModuleInput) (domain.Module, error)
GetModuleByID(ctx context.Context, id int64) (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) 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 ReorderModulesInCourse(ctx context.Context, courseID int64, orderedIDs []int64) error
UpdateModule(ctx context.Context, id int64, input domain.UpdateModuleInput) (domain.Module, error) UpdateModule(ctx context.Context, id int64, input domain.UpdateModuleInput) (domain.Module, error)
DeleteModule(ctx context.Context, id int64) error DeleteModule(ctx context.Context, id int64) error

View File

@ -8,7 +8,7 @@ import (
type ProgramStore interface { type ProgramStore interface {
CreateProgram(ctx context.Context, input domain.CreateProgramInput) (domain.Program, error) CreateProgram(ctx context.Context, input domain.CreateProgramInput) (domain.Program, error)
GetProgramByID(ctx context.Context, id int64) (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) ListAllProgramIDs(ctx context.Context) ([]int64, error)
ReorderPrograms(ctx context.Context, orderedIDs []int64) error ReorderPrograms(ctx context.Context, orderedIDs []int64) error
UpdateProgram(ctx context.Context, id int64, input domain.UpdateProgramInput) (domain.Program, 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 { func examPrepCatalogCourseToDomain(c dbgen.ExamPrepCatalogCourse) domain.ExamPrepCatalogCourse {
out := domain.ExamPrepCatalogCourse{ out := domain.ExamPrepCatalogCourse{
ID: c.ID, ID: c.ID,
Name: c.Name, Name: c.Name,
Category: c.Category, Category: c.Category,
SortOrder: int(c.SortOrder), SortOrder: int(c.SortOrder),
PublishStatus: domain.ContentPublishStatusFromDB(c.PublishStatus),
} }
out.Description = fromPgText(c.Description) out.Description = fromPgText(c.Description)
out.Thumbnail = fromPgText(c.Thumbnail) 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) { func (s *Store) CreateExamPrepCatalogCourse(ctx context.Context, input domain.CreateExamPrepCatalogCourseInput) (domain.ExamPrepCatalogCourse, error) {
c, err := s.queries.ExamPrepCreateCatalogCourse(ctx, dbgen.ExamPrepCreateCatalogCourseParams{ c, err := s.queries.ExamPrepCreateCatalogCourse(ctx, dbgen.ExamPrepCreateCatalogCourseParams{
Name: input.Name, Name: input.Name,
Description: toPgText(input.Description), Description: toPgText(input.Description),
Category: input.Category, Category: input.Category,
Thumbnail: toPgText(input.Thumbnail), Thumbnail: toPgText(input.Thumbnail),
PublishStatus: string(domain.ContentPublishStatusFromCreateInput(input.PublishStatus)),
}) })
if err != nil { if err != nil {
return domain.ExamPrepCatalogCourse{}, err return domain.ExamPrepCatalogCourse{}, err
@ -50,23 +52,25 @@ func (s *Store) GetExamPrepCatalogCourseByID(ctx context.Context, id int64) (dom
return domain.ExamPrepCatalogCourse{}, err return domain.ExamPrepCatalogCourse{}, err
} }
out := examPrepCatalogCourseToDomain(dbgen.ExamPrepCatalogCourse{ out := examPrepCatalogCourseToDomain(dbgen.ExamPrepCatalogCourse{
ID: c.ID, ID: c.ID,
Name: c.Name, Name: c.Name,
Description: c.Description, Description: c.Description,
Category: c.Category, Category: c.Category,
Thumbnail: c.Thumbnail, Thumbnail: c.Thumbnail,
SortOrder: c.SortOrder, SortOrder: c.SortOrder,
CreatedAt: c.CreatedAt, CreatedAt: c.CreatedAt,
UpdatedAt: c.UpdatedAt, UpdatedAt: c.UpdatedAt,
PublishStatus: c.PublishStatus,
}) })
out.HasPractice = c.HasPractice out.HasPractice = c.HasPractice
return out, nil 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{ rows, err := s.queries.ExamPrepListCatalogCourses(ctx, dbgen.ExamPrepListCatalogCoursesParams{
Limit: limit, Limit: limit,
Offset: offset, Offset: offset,
PublishedOnly: publishedOnly,
}) })
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
@ -81,14 +85,15 @@ func (s *Store) ListExamPrepCatalogCourses(ctx context.Context, limit, offset in
total = r.TotalCount total = r.TotalCount
} }
item := examPrepCatalogCourseToDomain(dbgen.ExamPrepCatalogCourse{ item := examPrepCatalogCourseToDomain(dbgen.ExamPrepCatalogCourse{
ID: r.ID, ID: r.ID,
Name: r.Name, Name: r.Name,
Description: r.Description, Description: r.Description,
Category: r.Category, Category: r.Category,
Thumbnail: r.Thumbnail, Thumbnail: r.Thumbnail,
SortOrder: r.SortOrder, SortOrder: r.SortOrder,
CreatedAt: r.CreatedAt, CreatedAt: r.CreatedAt,
UpdatedAt: r.UpdatedAt, UpdatedAt: r.UpdatedAt,
PublishStatus: r.PublishStatus,
}) })
item.UnitsCount = &r.UnitsCount item.UnitsCount = &r.UnitsCount
item.ModulesCount = &r.ModulesCount item.ModulesCount = &r.ModulesCount
@ -111,12 +116,13 @@ func (s *Store) UpdateExamPrepCatalogCourse(ctx context.Context, id int64, input
nameText = pgtype.Text{Valid: false} nameText = pgtype.Text{Valid: false}
} }
c, err := s.queries.ExamPrepUpdateCatalogCourse(ctx, dbgen.ExamPrepUpdateCatalogCourseParams{ c, err := s.queries.ExamPrepUpdateCatalogCourse(ctx, dbgen.ExamPrepUpdateCatalogCourseParams{
ID: id, ID: id,
Name: nameText, Name: nameText,
Description: optionalTextUpdate(input.Description), Description: optionalTextUpdate(input.Description),
Category: optionalTextUpdate(input.Category), Category: optionalTextUpdate(input.Category),
Thumbnail: optionalTextUpdate(input.Thumbnail), Thumbnail: optionalTextUpdate(input.Thumbnail),
SortOrder: optionalInt4Update(input.SortOrder), SortOrder: optionalInt4Update(input.SortOrder),
PublishStatus: optionalPublishStatusUpdate(input.PublishStatus),
}) })
if err != nil { if err != nil {
if errors.Is(err, pgx.ErrNoRows) { if errors.Is(err, pgx.ErrNoRows) {

View File

@ -13,10 +13,11 @@ import (
func examPrepLessonToDomain(l dbgen.ExamPrepUnitModuleLesson) domain.ExamPrepLesson { func examPrepLessonToDomain(l dbgen.ExamPrepUnitModuleLesson) domain.ExamPrepLesson {
out := domain.ExamPrepLesson{ out := domain.ExamPrepLesson{
ID: l.ID, ID: l.ID,
UnitModuleID: l.UnitModuleID, UnitModuleID: l.UnitModuleID,
Title: l.Title, Title: l.Title,
SortOrder: int(l.SortOrder), SortOrder: int(l.SortOrder),
PublishStatus: domain.ContentPublishStatusFromDB(l.PublishStatus),
} }
out.VideoURL = fromPgText(l.VideoUrl) out.VideoURL = fromPgText(l.VideoUrl)
out.Thumbnail = fromPgText(l.Thumbnail) 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) { func (s *Store) CreateExamPrepUnitModuleLesson(ctx context.Context, unitModuleID int64, input domain.CreateExamPrepLessonInput) (domain.ExamPrepLesson, error) {
l, err := s.queries.ExamPrepCreateUnitModuleLesson(ctx, dbgen.ExamPrepCreateUnitModuleLessonParams{ l, err := s.queries.ExamPrepCreateUnitModuleLesson(ctx, dbgen.ExamPrepCreateUnitModuleLessonParams{
UnitModuleID: unitModuleID, UnitModuleID: unitModuleID,
Title: input.Title, Title: input.Title,
VideoUrl: toPgText(input.VideoURL), VideoUrl: toPgText(input.VideoURL),
Thumbnail: toPgText(input.Thumbnail), Thumbnail: toPgText(input.Thumbnail),
Description: toPgText(input.Description), Description: toPgText(input.Description),
PublishStatus: string(domain.ContentPublishStatusFromCreateInput(input.PublishStatus)),
}) })
if err != nil { if err != nil {
return domain.ExamPrepLesson{}, err return domain.ExamPrepLesson{}, err
@ -52,25 +54,27 @@ func (s *Store) GetExamPrepUnitModuleLessonByID(ctx context.Context, id int64) (
return domain.ExamPrepLesson{}, err return domain.ExamPrepLesson{}, err
} }
out := examPrepLessonToDomain(dbgen.ExamPrepUnitModuleLesson{ out := examPrepLessonToDomain(dbgen.ExamPrepUnitModuleLesson{
ID: l.ID, ID: l.ID,
UnitModuleID: l.UnitModuleID, UnitModuleID: l.UnitModuleID,
Title: l.Title, Title: l.Title,
VideoUrl: l.VideoUrl, VideoUrl: l.VideoUrl,
Thumbnail: l.Thumbnail, Thumbnail: l.Thumbnail,
Description: l.Description, Description: l.Description,
SortOrder: l.SortOrder, SortOrder: l.SortOrder,
CreatedAt: l.CreatedAt, CreatedAt: l.CreatedAt,
UpdatedAt: l.UpdatedAt, UpdatedAt: l.UpdatedAt,
PublishStatus: l.PublishStatus,
}) })
out.HasPractice = l.HasPractice out.HasPractice = l.HasPractice
return out, nil 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{ rows, err := s.queries.ExamPrepListUnitModuleLessonsByUnitModuleID(ctx, dbgen.ExamPrepListUnitModuleLessonsByUnitModuleIDParams{
UnitModuleID: unitModuleID, UnitModuleID: unitModuleID,
Limit: limit, Limit: limit,
Offset: offset, Offset: offset,
PublishedOnly: publishedOnly,
}) })
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
@ -85,15 +89,16 @@ func (s *Store) ListExamPrepUnitModuleLessonsByUnitModuleID(ctx context.Context,
total = r.TotalCount total = r.TotalCount
} }
item := examPrepLessonToDomain(dbgen.ExamPrepUnitModuleLesson{ item := examPrepLessonToDomain(dbgen.ExamPrepUnitModuleLesson{
ID: r.ID, ID: r.ID,
UnitModuleID: r.UnitModuleID, UnitModuleID: r.UnitModuleID,
Title: r.Title, Title: r.Title,
VideoUrl: r.VideoUrl, VideoUrl: r.VideoUrl,
Thumbnail: r.Thumbnail, Thumbnail: r.Thumbnail,
Description: r.Description, Description: r.Description,
SortOrder: r.SortOrder, SortOrder: r.SortOrder,
CreatedAt: r.CreatedAt, CreatedAt: r.CreatedAt,
UpdatedAt: r.UpdatedAt, UpdatedAt: r.UpdatedAt,
PublishStatus: r.PublishStatus,
}) })
item.HasPractice = r.HasPractice item.HasPractice = r.HasPractice
out = append(out, item) out = append(out, item)
@ -105,6 +110,10 @@ func (s *Store) ListExamPrepUnitModuleLessonIDsByUnitModule(ctx context.Context,
return s.queries.ExamPrepListUnitModuleLessonIDsByUnitModule(ctx, unitModuleID) 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) { func (s *Store) UpdateExamPrepUnitModuleLesson(ctx context.Context, id int64, input domain.UpdateExamPrepLessonInput) (domain.ExamPrepLesson, error) {
var titleText pgtype.Text var titleText pgtype.Text
if input.Title != nil { if input.Title != nil {
@ -113,12 +122,13 @@ func (s *Store) UpdateExamPrepUnitModuleLesson(ctx context.Context, id int64, in
titleText = pgtype.Text{Valid: false} titleText = pgtype.Text{Valid: false}
} }
l, err := s.queries.ExamPrepUpdateUnitModuleLesson(ctx, dbgen.ExamPrepUpdateUnitModuleLessonParams{ l, err := s.queries.ExamPrepUpdateUnitModuleLesson(ctx, dbgen.ExamPrepUpdateUnitModuleLessonParams{
ID: id, ID: id,
Title: titleText, Title: titleText,
VideoUrl: optionalTextUpdate(input.VideoURL), VideoUrl: optionalTextUpdate(input.VideoURL),
Thumbnail: optionalTextUpdate(input.Thumbnail), Thumbnail: optionalTextUpdate(input.Thumbnail),
Description: optionalTextUpdate(input.Description), Description: optionalTextUpdate(input.Description),
SortOrder: optionalInt4Update(input.SortOrder), SortOrder: optionalInt4Update(input.SortOrder),
PublishStatus: optionalPublishStatusUpdate(input.PublishStatus),
}) })
if err != nil { if err != nil {
if errors.Is(err, pgx.ErrNoRows) { if errors.Is(err, pgx.ErrNoRows) {

View File

@ -13,10 +13,11 @@ import (
func examPrepModuleToDomain(m dbgen.ExamPrepUnitModule) domain.ExamPrepModule { func examPrepModuleToDomain(m dbgen.ExamPrepUnitModule) domain.ExamPrepModule {
out := domain.ExamPrepModule{ out := domain.ExamPrepModule{
ID: m.ID, ID: m.ID,
UnitID: m.UnitID, UnitID: m.UnitID,
Name: m.Name, Name: m.Name,
SortOrder: int(m.SortOrder), SortOrder: int(m.SortOrder),
PublishStatus: domain.ContentPublishStatusFromDB(m.PublishStatus),
} }
out.Description = fromPgText(m.Description) out.Description = fromPgText(m.Description)
out.Thumbnail = fromPgText(m.Thumbnail) 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) { func (s *Store) CreateExamPrepUnitModule(ctx context.Context, unitID int64, input domain.CreateExamPrepModuleInput) (domain.ExamPrepModule, error) {
m, err := s.queries.ExamPrepCreateUnitModule(ctx, dbgen.ExamPrepCreateUnitModuleParams{ m, err := s.queries.ExamPrepCreateUnitModule(ctx, dbgen.ExamPrepCreateUnitModuleParams{
UnitID: unitID, UnitID: unitID,
Name: input.Name, Name: input.Name,
Description: toPgText(input.Description), Description: toPgText(input.Description),
Thumbnail: toPgText(input.Thumbnail), Thumbnail: toPgText(input.Thumbnail),
Icon: toPgText(input.Icon), Icon: toPgText(input.Icon),
PublishStatus: string(domain.ContentPublishStatusFromCreateInput(input.PublishStatus)),
}) })
if err != nil { if err != nil {
return domain.ExamPrepModule{}, err return domain.ExamPrepModule{}, err
@ -52,25 +54,27 @@ func (s *Store) GetExamPrepUnitModuleByID(ctx context.Context, id int64) (domain
return domain.ExamPrepModule{}, err return domain.ExamPrepModule{}, err
} }
out := examPrepModuleToDomain(dbgen.ExamPrepUnitModule{ out := examPrepModuleToDomain(dbgen.ExamPrepUnitModule{
ID: m.ID, ID: m.ID,
UnitID: m.UnitID, UnitID: m.UnitID,
Name: m.Name, Name: m.Name,
Description: m.Description, Description: m.Description,
Thumbnail: m.Thumbnail, Thumbnail: m.Thumbnail,
Icon: m.Icon, Icon: m.Icon,
SortOrder: m.SortOrder, SortOrder: m.SortOrder,
CreatedAt: m.CreatedAt, CreatedAt: m.CreatedAt,
UpdatedAt: m.UpdatedAt, UpdatedAt: m.UpdatedAt,
PublishStatus: m.PublishStatus,
}) })
out.HasPractice = m.HasPractice out.HasPractice = m.HasPractice
return out, nil 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{ rows, err := s.queries.ExamPrepListUnitModulesByUnit(ctx, dbgen.ExamPrepListUnitModulesByUnitParams{
UnitID: unitID, UnitID: unitID,
Limit: limit, Limit: limit,
Offset: offset, Offset: offset,
PublishedOnly: publishedOnly,
}) })
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
@ -85,15 +89,16 @@ func (s *Store) ListExamPrepUnitModulesByUnit(ctx context.Context, unitID int64,
total = r.TotalCount total = r.TotalCount
} }
item := examPrepModuleToDomain(dbgen.ExamPrepUnitModule{ item := examPrepModuleToDomain(dbgen.ExamPrepUnitModule{
ID: r.ID, ID: r.ID,
UnitID: r.UnitID, UnitID: r.UnitID,
Name: r.Name, Name: r.Name,
Description: r.Description, Description: r.Description,
Thumbnail: r.Thumbnail, Thumbnail: r.Thumbnail,
Icon: r.Icon, Icon: r.Icon,
SortOrder: r.SortOrder, SortOrder: r.SortOrder,
CreatedAt: r.CreatedAt, CreatedAt: r.CreatedAt,
UpdatedAt: r.UpdatedAt, UpdatedAt: r.UpdatedAt,
PublishStatus: r.PublishStatus,
}) })
item.LessonsCount = &r.LessonsCount item.LessonsCount = &r.LessonsCount
item.PracticesCount = &r.PracticesCount item.PracticesCount = &r.PracticesCount
@ -107,6 +112,10 @@ func (s *Store) ListExamPrepUnitModuleIDsByUnit(ctx context.Context, unitID int6
return s.queries.ExamPrepListUnitModuleIDsByUnit(ctx, unitID) 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) { func (s *Store) UpdateExamPrepUnitModule(ctx context.Context, id int64, input domain.UpdateExamPrepModuleInput) (domain.ExamPrepModule, error) {
var nameText pgtype.Text var nameText pgtype.Text
if input.Name != nil { if input.Name != nil {
@ -115,12 +124,13 @@ func (s *Store) UpdateExamPrepUnitModule(ctx context.Context, id int64, input do
nameText = pgtype.Text{Valid: false} nameText = pgtype.Text{Valid: false}
} }
m, err := s.queries.ExamPrepUpdateUnitModule(ctx, dbgen.ExamPrepUpdateUnitModuleParams{ m, err := s.queries.ExamPrepUpdateUnitModule(ctx, dbgen.ExamPrepUpdateUnitModuleParams{
ID: id, ID: id,
Name: nameText, Name: nameText,
Description: optionalTextUpdate(input.Description), Description: optionalTextUpdate(input.Description),
Thumbnail: optionalTextUpdate(input.Thumbnail), Thumbnail: optionalTextUpdate(input.Thumbnail),
Icon: optionalTextUpdate(input.Icon), Icon: optionalTextUpdate(input.Icon),
SortOrder: optionalInt4Update(input.SortOrder), SortOrder: optionalInt4Update(input.SortOrder),
PublishStatus: optionalPublishStatusUpdate(input.PublishStatus),
}) })
if err != nil { if err != nil {
if errors.Is(err, pgx.ErrNoRows) { if errors.Is(err, pgx.ErrNoRows) {

View File

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

View File

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

View File

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

View File

@ -14,9 +14,10 @@ import (
func programToDomain(p dbgen.Program) domain.Program { func programToDomain(p dbgen.Program) domain.Program {
out := domain.Program{ out := domain.Program{
ID: p.ID, ID: p.ID,
Name: p.Name, Name: p.Name,
Category: p.Category, Category: p.Category,
PublishStatus: domain.ContentPublishStatusFromDB(p.PublishStatus),
} }
out.Description = fromPgText(p.Description) out.Description = fromPgText(p.Description)
out.Thumbnail = fromPgText(p.Thumbnail) 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) { func (s *Store) CreateProgram(ctx context.Context, input domain.CreateProgramInput) (domain.Program, error) {
pub := string(domain.ContentPublishStatusFromCreateInput(input.PublishStatus))
if input.SortOrder != nil { if input.SortOrder != nil {
q, tx, err := s.BeginTx(ctx) q, tx, err := s.BeginTx(ctx)
if err != nil { if err != nil {
@ -41,11 +43,12 @@ func (s *Store) CreateProgram(ctx context.Context, input domain.CreateProgramInp
return domain.Program{}, err return domain.Program{}, err
} }
p, err := q.CreateProgram(ctx, dbgen.CreateProgramParams{ p, err := q.CreateProgram(ctx, dbgen.CreateProgramParams{
Name: input.Name, Name: input.Name,
Description: toPgText(input.Description), Description: toPgText(input.Description),
Category: input.Category, Category: input.Category,
Thumbnail: toPgText(input.Thumbnail), Thumbnail: toPgText(input.Thumbnail),
SortOrder: pgtype.Int4{Int32: target, Valid: true}, SortOrder: pgtype.Int4{Int32: target, Valid: true},
PublishStatus: pub,
}) })
if err != nil { if err != nil {
return domain.Program{}, err 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{ p, err := s.queries.CreateProgram(ctx, dbgen.CreateProgramParams{
Name: input.Name, Name: input.Name,
Description: toPgText(input.Description), Description: toPgText(input.Description),
Category: input.Category, Category: input.Category,
Thumbnail: toPgText(input.Thumbnail), Thumbnail: toPgText(input.Thumbnail),
SortOrder: pgtype.Int4{Valid: false}, SortOrder: pgtype.Int4{Valid: false},
PublishStatus: pub,
}) })
if err != nil { if err != nil {
return domain.Program{}, err return domain.Program{}, err
@ -84,10 +88,11 @@ func (s *Store) GetProgramByID(ctx context.Context, id int64) (domain.Program, e
return programToDomain(p), nil 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{ rows, err := s.queries.ListPrograms(ctx, dbgen.ListProgramsParams{
Limit: limit, Limit: limit,
Offset: offset, Offset: offset,
PublishedOnly: publishedOnly,
}) })
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
@ -102,14 +107,15 @@ func (s *Store) ListPrograms(ctx context.Context, limit, offset int32) ([]domain
total = r.TotalCount total = r.TotalCount
} }
out = append(out, programToDomain(dbgen.Program{ out = append(out, programToDomain(dbgen.Program{
ID: r.ID, ID: r.ID,
Name: r.Name, Name: r.Name,
Description: r.Description, Description: r.Description,
Category: r.Category, Category: r.Category,
Thumbnail: r.Thumbnail, Thumbnail: r.Thumbnail,
CreatedAt: r.CreatedAt, CreatedAt: r.CreatedAt,
UpdatedAt: r.UpdatedAt, UpdatedAt: r.UpdatedAt,
SortOrder: r.SortOrder, SortOrder: r.SortOrder,
PublishStatus: r.PublishStatus,
})) }))
} }
return out, total, nil 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} nameText = pgtype.Text{Valid: false}
} }
p, err := q.UpdateProgram(ctx, dbgen.UpdateProgramParams{ p, err := q.UpdateProgram(ctx, dbgen.UpdateProgramParams{
ID: id, ID: id,
Name: nameText, Name: nameText,
Description: optionalTextUpdate(input.Description), Description: optionalTextUpdate(input.Description),
Category: optionalTextUpdate(input.Category), Category: optionalTextUpdate(input.Category),
Thumbnail: optionalTextUpdate(input.Thumbnail), Thumbnail: optionalTextUpdate(input.Thumbnail),
SortOrder: pgtype.Int4{Valid: false}, SortOrder: pgtype.Int4{Valid: false},
PublishStatus: optionalPublishStatusUpdate(input.PublishStatus),
}) })
if err != nil { if err != nil {
return domain.Program{}, err 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} nameText = pgtype.Text{Valid: false}
} }
p, err := s.queries.UpdateProgram(ctx, dbgen.UpdateProgramParams{ p, err := s.queries.UpdateProgram(ctx, dbgen.UpdateProgramParams{
ID: id, ID: id,
Name: nameText, Name: nameText,
Description: optionalTextUpdate(input.Description), Description: optionalTextUpdate(input.Description),
Category: optionalTextUpdate(input.Category), Category: optionalTextUpdate(input.Category),
Thumbnail: optionalTextUpdate(input.Thumbnail), Thumbnail: optionalTextUpdate(input.Thumbnail),
SortOrder: sortParam, SortOrder: sortParam,
PublishStatus: optionalPublishStatusUpdate(input.PublishStatus),
}) })
if err != nil { if err != nil {
if errors.Is(err, pgx.ErrNoRows) { 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 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 _, err := s.programs.GetProgramByID(ctx, programID); err != nil {
if errors.Is(err, pgx.ErrNoRows) { if errors.Is(err, pgx.ErrNoRows) {
return nil, 0, programs.ErrProgramNotFound return nil, 0, programs.ErrProgramNotFound
@ -58,7 +58,7 @@ func (s *Service) ListByProgram(ctx context.Context, programID int64, limit, off
if offset < 0 { if offset < 0 {
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) { 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 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 { if limit <= 0 {
limit = 20 limit = 20
} }
@ -69,7 +69,7 @@ func (s *Service) ListCatalogCourses(ctx context.Context, limit, offset int32) (
if offset < 0 { if offset < 0 {
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) { 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) 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 { if err := s.ensureCatalogCourse(ctx, catalogCourseID); err != nil {
return nil, 0, err return nil, 0, err
} }
@ -137,7 +137,7 @@ func (s *Service) ListUnitsByCatalogCourse(ctx context.Context, catalogCourseID
if offset < 0 { if offset < 0 {
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) { 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) 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 { if err := s.ensureUnit(ctx, unitID); err != nil {
return nil, 0, err return nil, 0, err
} }
@ -219,7 +219,7 @@ func (s *Service) ListModulesByUnit(ctx context.Context, unitID int64, limit, of
if offset < 0 { if offset < 0 {
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) { 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) 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 { if err := s.ensureModule(ctx, unitModuleID); err != nil {
return nil, 0, err return nil, 0, err
} }
@ -301,7 +301,7 @@ func (s *Service) ListLessonsByUnitModule(ctx context.Context, unitModuleID int6
if offset < 0 { if offset < 0 {
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) { 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 return 1, true, 1, 1, nil
} }
moduleIDs, err := s.store.ListModuleIDsByCourse(ctx, courseID) moduleIDs, err := s.store.ListPublishedModuleIDsByCourse(ctx, courseID)
if err != nil { if err != nil {
return 0, false, 0, 0, err 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) { 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 { if err != nil {
return 0, false, 0, 0, err 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) { 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 { if err != nil {
return 0, false, 0, 0, err 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) { 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 { if err != nil {
return 0, false, 0, 0, err 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) { 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 { if err != nil {
return 0, false, 0, 0, err 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. // 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) c, err := s.getCourseOrErr(ctx, courseID)
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
@ -67,7 +67,7 @@ func (s *Service) ListByCourse(ctx context.Context, courseID int64, limit, offse
if offset < 0 { if offset < 0 {
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) { 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 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 { if limit <= 0 {
limit = 20 limit = 20
} }
@ -44,7 +44,7 @@ func (s *Service) List(ctx context.Context, limit, offset int32) ([]domain.Progr
if offset < 0 { if offset < 0 {
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) { 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) 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 { func (s *Service) UpdateQuestionTypeDefinition(ctx context.Context, id int64, input domain.UpdateQuestionTypeDefinitionInput) error {
return s.questionStore.UpdateQuestionTypeDefinition(ctx, id, input) 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")) limit, _ := strconv.Atoi(c.Query("limit", "20"))
offset, _ := strconv.Atoi(c.Query("offset", "0")) 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 err != nil {
if errors.Is(err, programs.ErrProgramNotFound) { if errors.Is(err, programs.ErrProgramNotFound) {
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{ return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
@ -157,6 +168,12 @@ func (h *Handler) GetCourse(c *fiber.Ctx) error {
Error: err.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) uid := c.Locals("user_id").(int64)
role := c.Locals("role").(domain.Role) role := c.Locals("role").(domain.Role)
if err := h.lmsProgressSvc.ApplyAccessCourse(c.Context(), role, uid, &course); err != nil { 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 { func (h *Handler) ListExamPrepCatalogCourses(c *fiber.Ctx) error {
limit, _ := strconv.Atoi(c.Query("limit", "20")) limit, _ := strconv.Atoi(c.Query("limit", "20"))
offset, _ := strconv.Atoi(c.Query("offset", "0")) offset, _ := strconv.Atoi(c.Query("offset", "0"))
publishedOnly := !h.canManageExamPrepCatalogCourses(c)
role, _ := c.Locals("role").(domain.Role) role, _ := c.Locals("role").(domain.Role)
if role.IsCustomerLearnerRole() && !domain.CategorySubscriptionGateDisabled { 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 { if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to list catalog courses", 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 { if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to list catalog courses", Message: "Failed to list catalog courses",
@ -232,6 +233,12 @@ func (h *Handler) GetExamPrepCatalogCourseByID(c *fiber.Ctx) error {
Error: err.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 { if err := h.applyExamPrepAccessCatalogCourse(c.Context(), c, &out); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to build catalog course", 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")) limit, _ := strconv.Atoi(c.Query("limit", "20"))
offset, _ := strconv.Atoi(c.Query("offset", "0")) 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 err != nil {
if errors.Is(err, examprep.ErrModuleNotFound) { if errors.Is(err, examprep.ErrModuleNotFound) {
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{ return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
@ -181,6 +192,12 @@ func (h *Handler) GetExamPrepLessonByID(c *fiber.Ctx) error {
Error: err.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 { if err := h.applyExamPrepAccessLesson(c.Context(), c, &les); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to build lesson", 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")) limit, _ := strconv.Atoi(c.Query("limit", "20"))
offset, _ := strconv.Atoi(c.Query("offset", "0")) 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 err != nil {
if errors.Is(err, examprep.ErrUnitNotFound) { if errors.Is(err, examprep.ErrUnitNotFound) {
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{ return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
@ -181,6 +192,12 @@ func (h *Handler) GetExamPrepModuleByID(c *fiber.Ctx) error {
Error: err.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 { if err := h.applyExamPrepAccessModule(c.Context(), c, &out); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to build module", 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")) limit, _ := strconv.Atoi(c.Query("limit", "20"))
offset, _ := strconv.Atoi(c.Query("offset", "0")) 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 err != nil {
if errors.Is(err, examprep.ErrCatalogCourseNotFound) { if errors.Is(err, examprep.ErrCatalogCourseNotFound) {
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{ return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
@ -189,6 +200,12 @@ func (h *Handler) GetExamPrepUnitByID(c *fiber.Ctx) error {
Error: err.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 { if err := h.applyExamPrepAccessUnit(c.Context(), c, &out); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to build unit", 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) { 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 { if err != nil {
return domain.LMSProgressSummary{}, err 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 { if err := h.lmsProgressSvc.ApplyAccessProgram(ctx, role, userID, &programs[i]); err != nil {
return domain.LMSProgressSummary{}, err return domain.LMSProgressSummary{}, err
} }
courses, err := h.listAllCoursesByProgram(ctx, programs[i].ID) courses, err := h.listAllCoursesByProgram(ctx, programs[i].ID, publishedOnly)
if err != nil { if err != nil {
return domain.LMSProgressSummary{}, err 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 { if err := h.lmsProgressSvc.ApplyAccessCourse(ctx, role, userID, &courses[j]); err != nil {
return domain.LMSProgressSummary{}, err return domain.LMSProgressSummary{}, err
} }
modules, err := h.listAllModulesByCourse(ctx, courses[j].ID) modules, err := h.listAllModulesByCourse(ctx, courses[j].ID, publishedOnly)
if err != nil { if err != nil {
return domain.LMSProgressSummary{}, err return domain.LMSProgressSummary{}, err
} }
@ -251,13 +251,13 @@ func (h *Handler) buildLMSProgressSummary(ctx context.Context, role domain.Role,
return summary, nil 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 ( var (
all []domain.Program all []domain.Program
offset int32 offset int32
) )
for { for {
items, total, err := h.programSvc.List(ctx, lmsProgressSummaryPageSize, offset) items, total, err := h.programSvc.List(ctx, publishedOnly, lmsProgressSummaryPageSize, offset)
if err != nil { if err != nil {
return nil, err 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 ( var (
all []domain.Course all []domain.Course
offset int32 offset int32
) )
for { 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 { if err != nil {
return nil, err 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 ( var (
all []domain.Module all []domain.Module
offset int32 offset int32
) )
for { 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 { if err != nil {
return nil, err return nil, err
} }

View File

@ -90,7 +90,18 @@ func (h *Handler) ListModulesByCourse(c *fiber.Ctx) error {
} }
limit, _ := strconv.Atoi(c.Query("limit", "20")) limit, _ := strconv.Atoi(c.Query("limit", "20"))
offset, _ := strconv.Atoi(c.Query("offset", "0")) 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 err != nil {
if errors.Is(err, courses.ErrCourseNotFound) { if errors.Is(err, courses.ErrCourseNotFound) {
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{ return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
@ -152,6 +163,12 @@ func (h *Handler) GetModule(c *fiber.Ctx) error {
Error: err.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) uid := c.Locals("user_id").(int64)
role := c.Locals("role").(domain.Role) role := c.Locals("role").(domain.Role)
if err := h.lmsProgressSvc.ApplyAccessModule(c.Context(), role, uid, &mod); err != nil { if err := h.lmsProgressSvc.ApplyAccessModule(c.Context(), role, uid, &mod); err != nil {

View File

@ -9,61 +9,19 @@ import (
"github.com/gofiber/fiber/v2" "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)) out := make([]questionRes, 0, len(questions))
for _, question := range questions { for _, question := range questions {
options := make([]optionRes, 0, len(question.Options)) out = append(out, buildQuestionRes(question, catalog))
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,
})
} }
return out return out
} }
func fullUpdatePracticeResponse(result domain.FullUpdatePracticeResult) fiber.Map { func fullUpdatePracticeResponse(result domain.FullUpdatePracticeResult, catalog []domain.QuestionTypeDefinition) fiber.Map {
return fiber.Map{ return fiber.Map{
"practice": result.Practice, "practice": result.Practice,
"question_set": questionSetResFromDomain(result.QuestionSet), "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{ return c.JSON(domain.Response{
Message: "Practice updated successfully", Message: "Practice updated successfully",
Data: fullUpdatePracticeResponse(result), Data: fullUpdatePracticeResponse(result, catalog),
Success: true, Success: true,
StatusCode: fiber.StatusOK, 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{ return c.JSON(domain.Response{
Message: "Practice updated successfully", Message: "Practice updated successfully",
Data: fullUpdatePracticeResponse(result), Data: fullUpdatePracticeResponse(result, catalog),
Success: true, Success: true,
StatusCode: fiber.StatusOK, 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 { func (h *Handler) ListPrograms(c *fiber.Ctx) error {
limit, _ := strconv.Atoi(c.Query("limit", "20")) limit, _ := strconv.Atoi(c.Query("limit", "20"))
offset, _ := strconv.Atoi(c.Query("offset", "0")) 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 { if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to list programs", Message: "Failed to list programs",
@ -128,6 +129,12 @@ func (h *Handler) GetProgram(c *fiber.Ctx) error {
Error: err.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) uid := c.Locals("user_id").(int64)
role := c.Locals("role").(domain.Role) role := c.Locals("role").(domain.Role)
if err := h.lmsProgressSvc.ApplyAccessProgram(c.Context(), role, uid, &p); err != nil { 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"` 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 { func resolveQuestionTypeFromDefinition(def domain.QuestionTypeDefinition) string {
return domain.ResolveRuntimeQuestionTypeFromDefinition(def.Key, def.ResponseComponentKinds) 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) 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{ return c.Status(fiber.StatusCreated).JSON(domain.Response{
Message: "Question created successfully", Message: "Question created successfully",
Data: questionRes{ Data: buildQuestionResFromQuestion(question, catalog),
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(),
},
}) })
} }
@ -313,51 +365,17 @@ func (h *Handler) GetQuestionByID(c *fiber.Ctx) error {
}) })
} }
var options []optionRes catalog, err := h.activeQuestionTypeDefinitionCatalog(c.Context())
for _, opt := range question.Options { if err != nil {
options = append(options, optionRes{ return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
ID: opt.ID, Message: "Failed to load question type catalog",
OptionText: opt.OptionText, Error: err.Error(),
OptionOrder: opt.OptionOrder,
IsCorrect: opt.IsCorrect,
}) })
} }
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{ return c.JSON(domain.Response{
Message: "Question retrieved successfully", Message: "Question retrieved successfully",
Data: questionRes{ Data: buildQuestionRes(question, catalog),
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,
},
}) })
} }
@ -404,24 +422,19 @@ func (h *Handler) ListQuestions(c *fiber.Ctx) error {
}) })
} }
var questionResponses []questionRes catalog, err := h.activeQuestionTypeDefinitionCatalog(c.Context())
for _, q := range questions { if err != nil {
questionResponses = append(questionResponses, questionRes{ return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
ID: q.ID, Message: "Failed to load question type catalog",
QuestionText: questionTextField(q.QuestionType, q.QuestionText), Error: err.Error(),
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(),
}) })
} }
questionResponses := make([]questionRes, 0, len(questions))
for _, q := range questions {
questionResponses = append(questionResponses, buildQuestionResFromQuestion(q, catalog))
}
return c.JSON(domain.Response{ return c.JSON(domain.Response{
Message: "Questions retrieved successfully", Message: "Questions retrieved successfully",
Data: listQuestionsRes{ Data: listQuestionsRes{
@ -464,21 +477,19 @@ func (h *Handler) SearchQuestions(c *fiber.Ctx) error {
}) })
} }
var questionResponses []questionRes catalog, err := h.activeQuestionTypeDefinitionCatalog(c.Context())
for _, q := range questions { if err != nil {
questionResponses = append(questionResponses, questionRes{ return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
ID: q.ID, Message: "Failed to load question type catalog",
QuestionText: questionTextField(q.QuestionType, q.QuestionText), Error: err.Error(),
QuestionType: q.QuestionType,
QuestionTypeDefinitionID: q.QuestionTypeDefinitionID,
DynamicPayload: q.DynamicPayload,
DifficultyLevel: q.DifficultyLevel,
Points: q.Points,
Status: q.Status,
CreatedAt: q.CreatedAt.String(),
}) })
} }
questionResponses := make([]questionRes, 0, len(questions))
for _, q := range questions {
questionResponses = append(questionResponses, buildQuestionResFromQuestion(q, catalog))
}
return c.JSON(domain.Response{ return c.JSON(domain.Response{
Message: "Questions retrieved successfully", Message: "Questions retrieved successfully",
Data: listQuestionsRes{ 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)) questionResponses := make([]questionRes, 0, len(items))
for _, item := range items { for _, item := range items {
question, err := h.questionsSvc.GetQuestionWithDetails(c.Context(), item.QuestionID) 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)) questionResponses = append(questionResponses, buildQuestionRes(question, catalog))
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,
})
} }
return c.JSON(domain.Response{ return c.JSON(domain.Response{