Add draft vs published status for LMS and exam-prep practices.

Expose publish_status on create/update, filter learner-facing lists and gates, and add migration 000060.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Yared Yemane 2026-05-19 03:57:43 -07:00
parent 37aef49e28
commit 12ad59c409
33 changed files with 521 additions and 99 deletions

View File

@ -0,0 +1,5 @@
ALTER TABLE lms_practices DROP CONSTRAINT chk_lms_practices_publish_status;
ALTER TABLE lms_practices DROP COLUMN publish_status;
ALTER TABLE exam_prep.lesson_practices DROP CONSTRAINT chk_exam_prep_lesson_practices_publish_status;
ALTER TABLE exam_prep.lesson_practices DROP COLUMN publish_status;

View File

@ -0,0 +1,8 @@
-- Draft vs published visibility for LMS and exam-prep practices.
ALTER TABLE lms_practices
ADD COLUMN publish_status VARCHAR(16) NOT NULL DEFAULT 'PUBLISHED'
CONSTRAINT chk_lms_practices_publish_status CHECK (publish_status IN ('DRAFT', 'PUBLISHED'));
ALTER TABLE exam_prep.lesson_practices
ADD COLUMN publish_status VARCHAR(16) NOT NULL DEFAULT 'PUBLISHED'
CONSTRAINT chk_exam_prep_lesson_practices_publish_status CHECK (publish_status IN ('DRAFT', 'PUBLISHED'));

View File

@ -6,8 +6,9 @@ INSERT INTO exam_prep.lesson_practices (
story_image, story_image,
persona_id, persona_id,
question_set_id, question_set_id,
quick_tips quick_tips,
) VALUES ($1, $2, $3, $4, $5, $6, $7) publish_status
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING *; RETURNING *;
-- name: ExamPrepGetLessonPracticeByID :one -- name: ExamPrepGetLessonPracticeByID :one
@ -15,6 +16,13 @@ SELECT *
FROM exam_prep.lesson_practices FROM exam_prep.lesson_practices
WHERE id = $1; WHERE id = $1;
-- name: ExamPrepGetLessonPracticeByQuestionSetID :one
SELECT *
FROM exam_prep.lesson_practices
WHERE question_set_id = $1
ORDER BY id DESC
LIMIT 1;
-- name: ExamPrepListLessonPracticesByLessonID :many -- name: ExamPrepListLessonPracticesByLessonID :many
SELECT SELECT
COUNT(*) OVER () AS total_count, COUNT(*) OVER () AS total_count,
@ -26,10 +34,15 @@ SELECT
p.persona_id, p.persona_id,
p.question_set_id, p.question_set_id,
p.quick_tips, p.quick_tips,
p.publish_status,
p.created_at, p.created_at,
p.updated_at p.updated_at
FROM exam_prep.lesson_practices p FROM exam_prep.lesson_practices p
WHERE p.unit_module_lesson_id = $1 WHERE p.unit_module_lesson_id = $1
AND (
sqlc.arg('published_only')::boolean = FALSE
OR p.publish_status = 'PUBLISHED'::TEXT
)
ORDER BY p.created_at DESC ORDER BY p.created_at DESC
LIMIT $2 LIMIT $2
OFFSET $3; OFFSET $3;
@ -43,6 +56,7 @@ SET
persona_id = coalesce(sqlc.narg('persona_id')::bigint, persona_id), persona_id = coalesce(sqlc.narg('persona_id')::bigint, persona_id),
question_set_id = coalesce(sqlc.narg('question_set_id')::bigint, question_set_id), question_set_id = coalesce(sqlc.narg('question_set_id')::bigint, question_set_id),
quick_tips = coalesce(sqlc.narg('quick_tips')::text, quick_tips), quick_tips = coalesce(sqlc.narg('quick_tips')::text, quick_tips),
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

@ -86,6 +86,7 @@ UNION ALL
user_practice_progress upp user_practice_progress upp
INNER JOIN lms_practices lp ON lp.question_set_id = upp.question_set_id INNER JOIN lms_practices lp ON lp.question_set_id = upp.question_set_id
AND lp.lesson_id IS NOT NULL AND lp.lesson_id IS NOT NULL
AND lp.publish_status = 'PUBLISHED'
INNER JOIN question_sets qs ON qs.id = upp.question_set_id INNER JOIN question_sets qs ON qs.id = upp.question_set_id
AND qs.set_type = 'PRACTICE' AND qs.set_type = 'PRACTICE'
AND qs.status = 'PUBLISHED' AND qs.status = 'PUBLISHED'
@ -132,6 +133,7 @@ UNION ALL
INNER JOIN lms_practices lp ON lp.question_set_id = upp.question_set_id INNER JOIN lms_practices lp ON lp.question_set_id = upp.question_set_id
AND lp.module_id IS NOT NULL AND lp.module_id IS NOT NULL
AND lp.lesson_id IS NULL AND lp.lesson_id IS NULL
AND lp.publish_status = 'PUBLISHED'
INNER JOIN question_sets qs ON qs.id = upp.question_set_id INNER JOIN question_sets qs ON qs.id = upp.question_set_id
AND qs.set_type = 'PRACTICE' AND qs.set_type = 'PRACTICE'
AND qs.status = 'PUBLISHED' AND qs.status = 'PUBLISHED'
@ -176,6 +178,7 @@ UNION ALL
AND lp.course_id IS NOT NULL AND lp.course_id IS NOT NULL
AND lp.module_id IS NULL AND lp.module_id IS NULL
AND lp.lesson_id IS NULL AND lp.lesson_id IS NULL
AND lp.publish_status = 'PUBLISHED'
INNER JOIN question_sets qs ON qs.id = upp.question_set_id INNER JOIN question_sets qs ON qs.id = upp.question_set_id
AND qs.set_type = 'PRACTICE' AND qs.set_type = 'PRACTICE'
AND qs.status = 'PUBLISHED' AND qs.status = 'PUBLISHED'

View File

@ -24,6 +24,7 @@ SELECT
WHERE p.course_id = c.id WHERE p.course_id = c.id
AND p.module_id IS NULL AND p.module_id IS NULL
AND p.lesson_id IS NULL AND p.lesson_id IS NULL
AND p.publish_status = 'PUBLISHED'
) AS has_practice ) AS has_practice
FROM courses FROM courses
c c
@ -75,13 +76,15 @@ SELECT
WHERE WHERE
p.course_id = c.id p.course_id = c.id
AND p.module_id IS NULL AND p.module_id IS NULL
AND p.lesson_id IS NULL) AS practice_count, AND p.lesson_id IS NULL
AND p.publish_status = 'PUBLISHED') AS practice_count,
EXISTS ( EXISTS (
SELECT 1 SELECT 1
FROM lms_practices p FROM lms_practices p
WHERE p.course_id = c.id WHERE p.course_id = c.id
AND p.module_id IS NULL AND p.module_id IS NULL
AND p.lesson_id IS NULL AND p.lesson_id IS NULL
AND p.publish_status = 'PUBLISHED'
) AS has_practice ) AS has_practice
FROM FROM
courses c courses c

View File

@ -22,6 +22,7 @@ SELECT
SELECT 1 SELECT 1
FROM lms_practices p FROM lms_practices p
WHERE p.lesson_id = l.id WHERE p.lesson_id = l.id
AND p.publish_status = 'PUBLISHED'
) AS has_practice ) AS has_practice
FROM lessons FROM lessons
l l
@ -43,6 +44,7 @@ SELECT
SELECT 1 SELECT 1
FROM lms_practices p FROM lms_practices p
WHERE p.lesson_id = l.id WHERE p.lesson_id = l.id
AND p.publish_status = 'PUBLISHED'
) AS has_practice ) AS has_practice
FROM FROM
lessons l lessons l

View File

@ -23,6 +23,7 @@ SELECT
FROM lms_practices p FROM lms_practices p
WHERE p.module_id = m.id WHERE p.module_id = m.id
AND p.lesson_id IS NULL AND p.lesson_id IS NULL
AND p.publish_status = 'PUBLISHED'
) AS has_practice ) AS has_practice
FROM modules FROM modules
m m
@ -55,6 +56,7 @@ SELECT
FROM lms_practices p FROM lms_practices p
WHERE p.module_id = m.id WHERE p.module_id = m.id
AND p.lesson_id IS NULL AND p.lesson_id IS NULL
AND p.publish_status = 'PUBLISHED'
) AS has_practice ) AS has_practice
FROM FROM
modules m modules m

View File

@ -1,8 +1,8 @@
-- name: CreateLmsPractice :one -- name: CreateLmsPractice :one
INSERT INTO lms_practices ( INSERT INTO lms_practices (
course_id, module_id, lesson_id, course_id, module_id, lesson_id,
title, story_description, story_image, persona_id, question_set_id, quick_tips title, story_description, story_image, persona_id, question_set_id, quick_tips, publish_status
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
RETURNING *; RETURNING *;
-- name: GetLmsPracticeByID :one -- name: GetLmsPracticeByID :one
@ -10,6 +10,13 @@ SELECT *
FROM lms_practices FROM lms_practices
WHERE id = $1; WHERE id = $1;
-- name: GetLmsPracticeByQuestionSetID :one
SELECT *
FROM lms_practices
WHERE question_set_id = $1
ORDER BY id DESC
LIMIT 1;
-- name: ListLmsPracticesByCourseID :many -- name: ListLmsPracticesByCourseID :many
SELECT SELECT
COUNT(*) OVER () AS total_count, COUNT(*) OVER () AS total_count,
@ -23,10 +30,15 @@ SELECT
p.persona_id, p.persona_id,
p.question_set_id, p.question_set_id,
p.quick_tips, p.quick_tips,
p.publish_status,
p.created_at, p.created_at,
p.updated_at p.updated_at
FROM lms_practices p FROM lms_practices p
WHERE p.course_id = $1 WHERE p.course_id = $1
AND (
sqlc.arg('published_only')::boolean = FALSE
OR p.publish_status = 'PUBLISHED'::TEXT
)
ORDER BY p.created_at DESC ORDER BY p.created_at DESC
LIMIT $2 OFFSET $3; LIMIT $2 OFFSET $3;
@ -43,10 +55,15 @@ SELECT
p.persona_id, p.persona_id,
p.question_set_id, p.question_set_id,
p.quick_tips, p.quick_tips,
p.publish_status,
p.created_at, p.created_at,
p.updated_at p.updated_at
FROM lms_practices p FROM lms_practices p
WHERE p.module_id = $1 WHERE p.module_id = $1
AND (
sqlc.arg('published_only')::boolean = FALSE
OR p.publish_status = 'PUBLISHED'::TEXT
)
ORDER BY p.created_at DESC ORDER BY p.created_at DESC
LIMIT $2 OFFSET $3; LIMIT $2 OFFSET $3;
@ -63,10 +80,15 @@ SELECT
p.persona_id, p.persona_id,
p.question_set_id, p.question_set_id,
p.quick_tips, p.quick_tips,
p.publish_status,
p.created_at, p.created_at,
p.updated_at p.updated_at
FROM lms_practices p FROM lms_practices p
WHERE p.lesson_id = $1 WHERE p.lesson_id = $1
AND (
sqlc.arg('published_only')::boolean = FALSE
OR p.publish_status = 'PUBLISHED'::TEXT
)
ORDER BY p.created_at DESC ORDER BY p.created_at DESC
LIMIT $2 OFFSET $3; LIMIT $2 OFFSET $3;
@ -79,6 +101,7 @@ SET
persona_id = COALESCE(sqlc.narg('persona_id')::bigint, persona_id), persona_id = COALESCE(sqlc.narg('persona_id')::bigint, persona_id),
question_set_id = COALESCE(sqlc.narg('question_set_id')::bigint, question_set_id), question_set_id = COALESCE(sqlc.narg('question_set_id')::bigint, question_set_id),
quick_tips = COALESCE(sqlc.narg('quick_tips')::text, quick_tips), quick_tips = COALESCE(sqlc.narg('quick_tips')::text, quick_tips),
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

@ -257,7 +257,8 @@ FROM
WHERE WHERE
lp.module_id = $1 lp.module_id = $1
AND qs.set_type = 'PRACTICE' AND qs.set_type = 'PRACTICE'
AND qs.status = 'PUBLISHED'; AND qs.status = 'PUBLISHED'
AND lp.publish_status = 'PUBLISHED';
-- name: CountUserCompletedPublishedPracticesInModule :one -- name: CountUserCompletedPublishedPracticesInModule :one
SELECT SELECT
@ -271,7 +272,8 @@ WHERE
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'
AND qs.status = 'PUBLISHED'; AND qs.status = 'PUBLISHED'
AND lp.publish_status = 'PUBLISHED';
-- name: CountPublishedPracticesInCourse :one -- name: CountPublishedPracticesInCourse :one
SELECT SELECT
@ -282,7 +284,8 @@ FROM
WHERE WHERE
lp.course_id = $1 lp.course_id = $1
AND qs.set_type = 'PRACTICE' AND qs.set_type = 'PRACTICE'
AND qs.status = 'PUBLISHED'; AND qs.status = 'PUBLISHED'
AND lp.publish_status = 'PUBLISHED';
-- name: CountUserCompletedPublishedPracticesInCourse :one -- name: CountUserCompletedPublishedPracticesInCourse :one
SELECT SELECT
@ -308,7 +311,8 @@ FROM
WHERE WHERE
c.program_id = $1 c.program_id = $1
AND qs.set_type = 'PRACTICE' AND qs.set_type = 'PRACTICE'
AND qs.status = 'PUBLISHED'; AND qs.status = 'PUBLISHED'
AND lp.publish_status = 'PUBLISHED';
-- name: CountUserCompletedPublishedPracticesInProgram :one -- name: CountUserCompletedPublishedPracticesInProgram :one
SELECT SELECT
@ -323,7 +327,8 @@ WHERE
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'
AND qs.status = 'PUBLISHED'; AND qs.status = 'PUBLISHED'
AND lp.publish_status = 'PUBLISHED';
-- name: GetPracticeScopeByQuestionSetID :one -- name: GetPracticeScopeByQuestionSetID :one
SELECT SELECT

View File

@ -100,6 +100,7 @@ FROM (
user_practice_progress upp user_practice_progress upp
INNER JOIN lms_practices lp ON lp.question_set_id = upp.question_set_id INNER JOIN lms_practices lp ON lp.question_set_id = upp.question_set_id
AND lp.lesson_id IS NOT NULL AND lp.lesson_id IS NOT NULL
AND lp.publish_status = 'PUBLISHED'
INNER JOIN question_sets qs ON qs.id = upp.question_set_id INNER JOIN question_sets qs ON qs.id = upp.question_set_id
AND qs.set_type = 'PRACTICE' AND qs.set_type = 'PRACTICE'
AND qs.status = 'PUBLISHED' AND qs.status = 'PUBLISHED'
@ -131,6 +132,7 @@ FROM (
INNER JOIN lms_practices lp ON lp.question_set_id = upp.question_set_id INNER JOIN lms_practices lp ON lp.question_set_id = upp.question_set_id
AND lp.module_id IS NOT NULL AND lp.module_id IS NOT NULL
AND lp.lesson_id IS NULL AND lp.lesson_id IS NULL
AND lp.publish_status = 'PUBLISHED'
INNER JOIN question_sets qs ON qs.id = upp.question_set_id INNER JOIN question_sets qs ON qs.id = upp.question_set_id
AND qs.set_type = 'PRACTICE' AND qs.set_type = 'PRACTICE'
AND qs.status = 'PUBLISHED' AND qs.status = 'PUBLISHED'
@ -162,6 +164,7 @@ FROM (
AND lp.course_id IS NOT NULL AND lp.course_id IS NOT NULL
AND lp.module_id IS NULL AND lp.module_id IS NULL
AND lp.lesson_id IS NULL AND lp.lesson_id IS NULL
AND lp.publish_status = 'PUBLISHED'
INNER JOIN question_sets qs ON qs.id = upp.question_set_id INNER JOIN question_sets qs ON qs.id = upp.question_set_id
AND qs.set_type = 'PRACTICE' AND qs.set_type = 'PRACTICE'
AND qs.status = 'PUBLISHED' AND qs.status = 'PUBLISHED'

View File

@ -415,6 +415,8 @@ This creates the practice record scoped to lesson.
### Request ### Request
Include `publish_status`: `DRAFT` to hide the practice from subscribed learners until you set it to `PUBLISHED` (via create or `PUT /practices/:id`). Omit the field or send `PUBLISHED` to go live immediately (backward compatible).
```json ```json
{ {
"parent_kind": "LESSON", "parent_kind": "LESSON",
@ -423,7 +425,8 @@ This creates the practice record scoped to lesson.
"story_description": "A short two-speaker scenario.", "story_description": "A short two-speaker scenario.",
"story_image": "https://cdn.example.com/images/story.webp", "story_image": "https://cdn.example.com/images/story.webp",
"question_set_id": 55, "question_set_id": 55,
"quick_tips": "Listen carefully before answering." "quick_tips": "Listen carefully before answering.",
"publish_status": "DRAFT"
} }
``` ```

View File

@ -19,9 +19,10 @@ INSERT INTO exam_prep.lesson_practices (
story_image, story_image,
persona_id, persona_id,
question_set_id, question_set_id,
quick_tips quick_tips,
) VALUES ($1, $2, $3, $4, $5, $6, $7) publish_status
RETURNING id, unit_module_lesson_id, title, story_description, story_image, persona_id, question_set_id, quick_tips, created_at, updated_at ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING id, unit_module_lesson_id, title, story_description, story_image, persona_id, question_set_id, quick_tips, created_at, updated_at, publish_status
` `
type ExamPrepCreateLessonPracticeParams struct { type ExamPrepCreateLessonPracticeParams struct {
@ -32,6 +33,7 @@ type ExamPrepCreateLessonPracticeParams struct {
PersonaID pgtype.Int8 `json:"persona_id"` PersonaID pgtype.Int8 `json:"persona_id"`
QuestionSetID int64 `json:"question_set_id"` QuestionSetID int64 `json:"question_set_id"`
QuickTips pgtype.Text `json:"quick_tips"` QuickTips pgtype.Text `json:"quick_tips"`
PublishStatus string `json:"publish_status"`
} }
func (q *Queries) ExamPrepCreateLessonPractice(ctx context.Context, arg ExamPrepCreateLessonPracticeParams) (ExamPrepLessonPractice, error) { func (q *Queries) ExamPrepCreateLessonPractice(ctx context.Context, arg ExamPrepCreateLessonPracticeParams) (ExamPrepLessonPractice, error) {
@ -43,6 +45,7 @@ func (q *Queries) ExamPrepCreateLessonPractice(ctx context.Context, arg ExamPrep
arg.PersonaID, arg.PersonaID,
arg.QuestionSetID, arg.QuestionSetID,
arg.QuickTips, arg.QuickTips,
arg.PublishStatus,
) )
var i ExamPrepLessonPractice var i ExamPrepLessonPractice
err := row.Scan( err := row.Scan(
@ -56,6 +59,7 @@ func (q *Queries) ExamPrepCreateLessonPractice(ctx context.Context, arg ExamPrep
&i.QuickTips, &i.QuickTips,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.PublishStatus,
) )
return i, err return i, err
} }
@ -71,7 +75,7 @@ func (q *Queries) ExamPrepDeleteLessonPractice(ctx context.Context, id int64) er
} }
const ExamPrepGetLessonPracticeByID = `-- name: ExamPrepGetLessonPracticeByID :one const ExamPrepGetLessonPracticeByID = `-- name: ExamPrepGetLessonPracticeByID :one
SELECT id, unit_module_lesson_id, title, story_description, story_image, persona_id, question_set_id, quick_tips, created_at, updated_at SELECT id, unit_module_lesson_id, title, story_description, story_image, persona_id, question_set_id, quick_tips, created_at, updated_at, publish_status
FROM exam_prep.lesson_practices FROM exam_prep.lesson_practices
WHERE id = $1 WHERE id = $1
` `
@ -90,6 +94,34 @@ func (q *Queries) ExamPrepGetLessonPracticeByID(ctx context.Context, id int64) (
&i.QuickTips, &i.QuickTips,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.PublishStatus,
)
return i, err
}
const ExamPrepGetLessonPracticeByQuestionSetID = `-- name: ExamPrepGetLessonPracticeByQuestionSetID :one
SELECT id, unit_module_lesson_id, title, story_description, story_image, persona_id, question_set_id, quick_tips, created_at, updated_at, publish_status
FROM exam_prep.lesson_practices
WHERE question_set_id = $1
ORDER BY id DESC
LIMIT 1
`
func (q *Queries) ExamPrepGetLessonPracticeByQuestionSetID(ctx context.Context, questionSetID int64) (ExamPrepLessonPractice, error) {
row := q.db.QueryRow(ctx, ExamPrepGetLessonPracticeByQuestionSetID, questionSetID)
var i ExamPrepLessonPractice
err := row.Scan(
&i.ID,
&i.UnitModuleLessonID,
&i.Title,
&i.StoryDescription,
&i.StoryImage,
&i.PersonaID,
&i.QuestionSetID,
&i.QuickTips,
&i.CreatedAt,
&i.UpdatedAt,
&i.PublishStatus,
) )
return i, err return i, err
} }
@ -105,10 +137,15 @@ SELECT
p.persona_id, p.persona_id,
p.question_set_id, p.question_set_id,
p.quick_tips, p.quick_tips,
p.publish_status,
p.created_at, p.created_at,
p.updated_at p.updated_at
FROM exam_prep.lesson_practices p FROM exam_prep.lesson_practices p
WHERE p.unit_module_lesson_id = $1 WHERE p.unit_module_lesson_id = $1
AND (
$4::boolean = FALSE
OR p.publish_status = 'PUBLISHED'::TEXT
)
ORDER BY p.created_at DESC ORDER BY p.created_at DESC
LIMIT $2 LIMIT $2
OFFSET $3 OFFSET $3
@ -118,6 +155,7 @@ type ExamPrepListLessonPracticesByLessonIDParams struct {
UnitModuleLessonID int64 `json:"unit_module_lesson_id"` UnitModuleLessonID int64 `json:"unit_module_lesson_id"`
Limit int32 `json:"limit"` Limit int32 `json:"limit"`
Offset int32 `json:"offset"` Offset int32 `json:"offset"`
PublishedOnly bool `json:"published_only"`
} }
type ExamPrepListLessonPracticesByLessonIDRow struct { type ExamPrepListLessonPracticesByLessonIDRow struct {
@ -130,12 +168,18 @@ type ExamPrepListLessonPracticesByLessonIDRow struct {
PersonaID pgtype.Int8 `json:"persona_id"` PersonaID pgtype.Int8 `json:"persona_id"`
QuestionSetID int64 `json:"question_set_id"` QuestionSetID int64 `json:"question_set_id"`
QuickTips pgtype.Text `json:"quick_tips"` QuickTips pgtype.Text `json:"quick_tips"`
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"`
} }
func (q *Queries) ExamPrepListLessonPracticesByLessonID(ctx context.Context, arg ExamPrepListLessonPracticesByLessonIDParams) ([]ExamPrepListLessonPracticesByLessonIDRow, error) { func (q *Queries) ExamPrepListLessonPracticesByLessonID(ctx context.Context, arg ExamPrepListLessonPracticesByLessonIDParams) ([]ExamPrepListLessonPracticesByLessonIDRow, error) {
rows, err := q.db.Query(ctx, ExamPrepListLessonPracticesByLessonID, arg.UnitModuleLessonID, arg.Limit, arg.Offset) rows, err := q.db.Query(ctx, ExamPrepListLessonPracticesByLessonID,
arg.UnitModuleLessonID,
arg.Limit,
arg.Offset,
arg.PublishedOnly,
)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -153,6 +197,7 @@ func (q *Queries) ExamPrepListLessonPracticesByLessonID(ctx context.Context, arg
&i.PersonaID, &i.PersonaID,
&i.QuestionSetID, &i.QuestionSetID,
&i.QuickTips, &i.QuickTips,
&i.PublishStatus,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
); err != nil { ); err != nil {
@ -175,9 +220,10 @@ SET
persona_id = coalesce($4::bigint, persona_id), persona_id = coalesce($4::bigint, persona_id),
question_set_id = coalesce($5::bigint, question_set_id), question_set_id = coalesce($5::bigint, question_set_id),
quick_tips = coalesce($6::text, quick_tips), quick_tips = coalesce($6::text, quick_tips),
publish_status = coalesce($7::varchar, publish_status),
updated_at = CURRENT_TIMESTAMP updated_at = CURRENT_TIMESTAMP
WHERE id = $7 WHERE id = $8
RETURNING id, unit_module_lesson_id, title, story_description, story_image, persona_id, question_set_id, quick_tips, created_at, updated_at RETURNING id, unit_module_lesson_id, title, story_description, story_image, persona_id, question_set_id, quick_tips, created_at, updated_at, publish_status
` `
type ExamPrepUpdateLessonPracticeParams struct { type ExamPrepUpdateLessonPracticeParams struct {
@ -187,6 +233,7 @@ type ExamPrepUpdateLessonPracticeParams struct {
PersonaID pgtype.Int8 `json:"persona_id"` PersonaID pgtype.Int8 `json:"persona_id"`
QuestionSetID pgtype.Int8 `json:"question_set_id"` QuestionSetID pgtype.Int8 `json:"question_set_id"`
QuickTips pgtype.Text `json:"quick_tips"` QuickTips pgtype.Text `json:"quick_tips"`
PublishStatus pgtype.Text `json:"publish_status"`
ID int64 `json:"id"` ID int64 `json:"id"`
} }
@ -198,6 +245,7 @@ func (q *Queries) ExamPrepUpdateLessonPractice(ctx context.Context, arg ExamPrep
arg.PersonaID, arg.PersonaID,
arg.QuestionSetID, arg.QuestionSetID,
arg.QuickTips, arg.QuickTips,
arg.PublishStatus,
arg.ID, arg.ID,
) )
var i ExamPrepLessonPractice var i ExamPrepLessonPractice
@ -212,6 +260,7 @@ func (q *Queries) ExamPrepUpdateLessonPractice(ctx context.Context, arg ExamPrep
&i.QuickTips, &i.QuickTips,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.PublishStatus,
) )
return i, err return i, err
} }

View File

@ -97,6 +97,7 @@ UNION ALL
user_practice_progress upp user_practice_progress upp
INNER JOIN lms_practices lp ON lp.question_set_id = upp.question_set_id INNER JOIN lms_practices lp ON lp.question_set_id = upp.question_set_id
AND lp.lesson_id IS NOT NULL AND lp.lesson_id IS NOT NULL
AND lp.publish_status = 'PUBLISHED'
INNER JOIN question_sets qs ON qs.id = upp.question_set_id INNER JOIN question_sets qs ON qs.id = upp.question_set_id
AND qs.set_type = 'PRACTICE' AND qs.set_type = 'PRACTICE'
AND qs.status = 'PUBLISHED' AND qs.status = 'PUBLISHED'
@ -143,6 +144,7 @@ UNION ALL
INNER JOIN lms_practices lp ON lp.question_set_id = upp.question_set_id INNER JOIN lms_practices lp ON lp.question_set_id = upp.question_set_id
AND lp.module_id IS NOT NULL AND lp.module_id IS NOT NULL
AND lp.lesson_id IS NULL AND lp.lesson_id IS NULL
AND lp.publish_status = 'PUBLISHED'
INNER JOIN question_sets qs ON qs.id = upp.question_set_id INNER JOIN question_sets qs ON qs.id = upp.question_set_id
AND qs.set_type = 'PRACTICE' AND qs.set_type = 'PRACTICE'
AND qs.status = 'PUBLISHED' AND qs.status = 'PUBLISHED'
@ -187,6 +189,7 @@ UNION ALL
AND lp.course_id IS NOT NULL AND lp.course_id IS NOT NULL
AND lp.module_id IS NULL AND lp.module_id IS NULL
AND lp.lesson_id IS NULL AND lp.lesson_id IS NULL
AND lp.publish_status = 'PUBLISHED'
INNER JOIN question_sets qs ON qs.id = upp.question_set_id INNER JOIN question_sets qs ON qs.id = upp.question_set_id
AND qs.set_type = 'PRACTICE' AND qs.set_type = 'PRACTICE'
AND qs.status = 'PUBLISHED' AND qs.status = 'PUBLISHED'

View File

@ -78,6 +78,7 @@ SELECT
WHERE p.course_id = c.id WHERE p.course_id = c.id
AND p.module_id IS NULL AND p.module_id IS NULL
AND p.lesson_id IS NULL AND p.lesson_id IS NULL
AND p.publish_status = 'PUBLISHED'
) AS has_practice ) AS has_practice
FROM courses FROM courses
c c
@ -180,13 +181,15 @@ SELECT
WHERE WHERE
p.course_id = c.id p.course_id = c.id
AND p.module_id IS NULL AND p.module_id IS NULL
AND p.lesson_id IS NULL) AS practice_count, AND p.lesson_id IS NULL
AND p.publish_status = 'PUBLISHED') AS practice_count,
EXISTS ( EXISTS (
SELECT 1 SELECT 1
FROM lms_practices p FROM lms_practices p
WHERE p.course_id = c.id WHERE p.course_id = c.id
AND p.module_id IS NULL AND p.module_id IS NULL
AND p.lesson_id IS NULL AND p.lesson_id IS NULL
AND p.publish_status = 'PUBLISHED'
) AS has_practice ) AS has_practice
FROM FROM
courses c courses c

View File

@ -77,6 +77,7 @@ SELECT
SELECT 1 SELECT 1
FROM lms_practices p FROM lms_practices p
WHERE p.lesson_id = l.id WHERE p.lesson_id = l.id
AND p.publish_status = 'PUBLISHED'
) AS has_practice ) AS has_practice
FROM lessons FROM lessons
l l
@ -130,6 +131,7 @@ SELECT
SELECT 1 SELECT 1
FROM lms_practices p FROM lms_practices p
WHERE p.lesson_id = l.id WHERE p.lesson_id = l.id
AND p.publish_status = 'PUBLISHED'
) AS has_practice ) AS has_practice
FROM FROM
lessons l lessons l

View File

@ -78,6 +78,7 @@ SELECT
FROM lms_practices p FROM lms_practices p
WHERE p.module_id = m.id WHERE p.module_id = m.id
AND p.lesson_id IS NULL AND p.lesson_id IS NULL
AND p.publish_status = 'PUBLISHED'
) AS has_practice ) AS has_practice
FROM modules FROM modules
m m
@ -163,6 +164,7 @@ SELECT
FROM lms_practices p FROM lms_practices p
WHERE p.module_id = m.id WHERE p.module_id = m.id
AND p.lesson_id IS NULL AND p.lesson_id IS NULL
AND p.publish_status = 'PUBLISHED'
) AS has_practice ) AS has_practice
FROM FROM
modules m modules m

View File

@ -14,9 +14,9 @@ import (
const CreateLmsPractice = `-- name: CreateLmsPractice :one const CreateLmsPractice = `-- name: CreateLmsPractice :one
INSERT INTO lms_practices ( INSERT INTO lms_practices (
course_id, module_id, lesson_id, course_id, module_id, lesson_id,
title, story_description, story_image, persona_id, question_set_id, quick_tips title, story_description, story_image, persona_id, question_set_id, quick_tips, publish_status
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
RETURNING id, course_id, module_id, lesson_id, title, story_description, story_image, persona_id, question_set_id, quick_tips, created_at, updated_at RETURNING id, course_id, module_id, lesson_id, title, story_description, story_image, persona_id, question_set_id, quick_tips, created_at, updated_at, publish_status
` `
type CreateLmsPracticeParams struct { type CreateLmsPracticeParams struct {
@ -29,6 +29,7 @@ type CreateLmsPracticeParams struct {
PersonaID pgtype.Int8 `json:"persona_id"` PersonaID pgtype.Int8 `json:"persona_id"`
QuestionSetID int64 `json:"question_set_id"` QuestionSetID int64 `json:"question_set_id"`
QuickTips pgtype.Text `json:"quick_tips"` QuickTips pgtype.Text `json:"quick_tips"`
PublishStatus string `json:"publish_status"`
} }
func (q *Queries) CreateLmsPractice(ctx context.Context, arg CreateLmsPracticeParams) (LmsPractice, error) { func (q *Queries) CreateLmsPractice(ctx context.Context, arg CreateLmsPracticeParams) (LmsPractice, error) {
@ -42,6 +43,7 @@ func (q *Queries) CreateLmsPractice(ctx context.Context, arg CreateLmsPracticePa
arg.PersonaID, arg.PersonaID,
arg.QuestionSetID, arg.QuestionSetID,
arg.QuickTips, arg.QuickTips,
arg.PublishStatus,
) )
var i LmsPractice var i LmsPractice
err := row.Scan( err := row.Scan(
@ -57,6 +59,7 @@ func (q *Queries) CreateLmsPractice(ctx context.Context, arg CreateLmsPracticePa
&i.QuickTips, &i.QuickTips,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.PublishStatus,
) )
return i, err return i, err
} }
@ -72,7 +75,7 @@ func (q *Queries) DeleteLmsPractice(ctx context.Context, id int64) error {
} }
const GetLmsPracticeByID = `-- name: GetLmsPracticeByID :one const GetLmsPracticeByID = `-- name: GetLmsPracticeByID :one
SELECT id, course_id, module_id, lesson_id, title, story_description, story_image, persona_id, question_set_id, quick_tips, created_at, updated_at SELECT id, course_id, module_id, lesson_id, title, story_description, story_image, persona_id, question_set_id, quick_tips, created_at, updated_at, publish_status
FROM lms_practices FROM lms_practices
WHERE id = $1 WHERE id = $1
` `
@ -93,6 +96,36 @@ func (q *Queries) GetLmsPracticeByID(ctx context.Context, id int64) (LmsPractice
&i.QuickTips, &i.QuickTips,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.PublishStatus,
)
return i, err
}
const GetLmsPracticeByQuestionSetID = `-- name: GetLmsPracticeByQuestionSetID :one
SELECT id, course_id, module_id, lesson_id, title, story_description, story_image, persona_id, question_set_id, quick_tips, created_at, updated_at, publish_status
FROM lms_practices
WHERE question_set_id = $1
ORDER BY id DESC
LIMIT 1
`
func (q *Queries) GetLmsPracticeByQuestionSetID(ctx context.Context, questionSetID int64) (LmsPractice, error) {
row := q.db.QueryRow(ctx, GetLmsPracticeByQuestionSetID, questionSetID)
var i LmsPractice
err := row.Scan(
&i.ID,
&i.CourseID,
&i.ModuleID,
&i.LessonID,
&i.Title,
&i.StoryDescription,
&i.StoryImage,
&i.PersonaID,
&i.QuestionSetID,
&i.QuickTips,
&i.CreatedAt,
&i.UpdatedAt,
&i.PublishStatus,
) )
return i, err return i, err
} }
@ -110,10 +143,15 @@ SELECT
p.persona_id, p.persona_id,
p.question_set_id, p.question_set_id,
p.quick_tips, p.quick_tips,
p.publish_status,
p.created_at, p.created_at,
p.updated_at p.updated_at
FROM lms_practices p FROM lms_practices p
WHERE p.course_id = $1 WHERE p.course_id = $1
AND (
$4::boolean = FALSE
OR p.publish_status = 'PUBLISHED'::TEXT
)
ORDER BY p.created_at DESC ORDER BY p.created_at DESC
LIMIT $2 OFFSET $3 LIMIT $2 OFFSET $3
` `
@ -122,6 +160,7 @@ type ListLmsPracticesByCourseIDParams struct {
CourseID pgtype.Int8 `json:"course_id"` CourseID pgtype.Int8 `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 ListLmsPracticesByCourseIDRow struct { type ListLmsPracticesByCourseIDRow struct {
@ -136,12 +175,18 @@ type ListLmsPracticesByCourseIDRow struct {
PersonaID pgtype.Int8 `json:"persona_id"` PersonaID pgtype.Int8 `json:"persona_id"`
QuestionSetID int64 `json:"question_set_id"` QuestionSetID int64 `json:"question_set_id"`
QuickTips pgtype.Text `json:"quick_tips"` QuickTips pgtype.Text `json:"quick_tips"`
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"`
} }
func (q *Queries) ListLmsPracticesByCourseID(ctx context.Context, arg ListLmsPracticesByCourseIDParams) ([]ListLmsPracticesByCourseIDRow, error) { func (q *Queries) ListLmsPracticesByCourseID(ctx context.Context, arg ListLmsPracticesByCourseIDParams) ([]ListLmsPracticesByCourseIDRow, error) {
rows, err := q.db.Query(ctx, ListLmsPracticesByCourseID, arg.CourseID, arg.Limit, arg.Offset) rows, err := q.db.Query(ctx, ListLmsPracticesByCourseID,
arg.CourseID,
arg.Limit,
arg.Offset,
arg.PublishedOnly,
)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -161,6 +206,7 @@ func (q *Queries) ListLmsPracticesByCourseID(ctx context.Context, arg ListLmsPra
&i.PersonaID, &i.PersonaID,
&i.QuestionSetID, &i.QuestionSetID,
&i.QuickTips, &i.QuickTips,
&i.PublishStatus,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
); err != nil { ); err != nil {
@ -187,10 +233,15 @@ SELECT
p.persona_id, p.persona_id,
p.question_set_id, p.question_set_id,
p.quick_tips, p.quick_tips,
p.publish_status,
p.created_at, p.created_at,
p.updated_at p.updated_at
FROM lms_practices p FROM lms_practices p
WHERE p.lesson_id = $1 WHERE p.lesson_id = $1
AND (
$4::boolean = FALSE
OR p.publish_status = 'PUBLISHED'::TEXT
)
ORDER BY p.created_at DESC ORDER BY p.created_at DESC
LIMIT $2 OFFSET $3 LIMIT $2 OFFSET $3
` `
@ -199,6 +250,7 @@ type ListLmsPracticesByLessonIDParams struct {
LessonID pgtype.Int8 `json:"lesson_id"` LessonID pgtype.Int8 `json:"lesson_id"`
Limit int32 `json:"limit"` Limit int32 `json:"limit"`
Offset int32 `json:"offset"` Offset int32 `json:"offset"`
PublishedOnly bool `json:"published_only"`
} }
type ListLmsPracticesByLessonIDRow struct { type ListLmsPracticesByLessonIDRow struct {
@ -213,12 +265,18 @@ type ListLmsPracticesByLessonIDRow struct {
PersonaID pgtype.Int8 `json:"persona_id"` PersonaID pgtype.Int8 `json:"persona_id"`
QuestionSetID int64 `json:"question_set_id"` QuestionSetID int64 `json:"question_set_id"`
QuickTips pgtype.Text `json:"quick_tips"` QuickTips pgtype.Text `json:"quick_tips"`
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"`
} }
func (q *Queries) ListLmsPracticesByLessonID(ctx context.Context, arg ListLmsPracticesByLessonIDParams) ([]ListLmsPracticesByLessonIDRow, error) { func (q *Queries) ListLmsPracticesByLessonID(ctx context.Context, arg ListLmsPracticesByLessonIDParams) ([]ListLmsPracticesByLessonIDRow, error) {
rows, err := q.db.Query(ctx, ListLmsPracticesByLessonID, arg.LessonID, arg.Limit, arg.Offset) rows, err := q.db.Query(ctx, ListLmsPracticesByLessonID,
arg.LessonID,
arg.Limit,
arg.Offset,
arg.PublishedOnly,
)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -238,6 +296,7 @@ func (q *Queries) ListLmsPracticesByLessonID(ctx context.Context, arg ListLmsPra
&i.PersonaID, &i.PersonaID,
&i.QuestionSetID, &i.QuestionSetID,
&i.QuickTips, &i.QuickTips,
&i.PublishStatus,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
); err != nil { ); err != nil {
@ -264,10 +323,15 @@ SELECT
p.persona_id, p.persona_id,
p.question_set_id, p.question_set_id,
p.quick_tips, p.quick_tips,
p.publish_status,
p.created_at, p.created_at,
p.updated_at p.updated_at
FROM lms_practices p FROM lms_practices p
WHERE p.module_id = $1 WHERE p.module_id = $1
AND (
$4::boolean = FALSE
OR p.publish_status = 'PUBLISHED'::TEXT
)
ORDER BY p.created_at DESC ORDER BY p.created_at DESC
LIMIT $2 OFFSET $3 LIMIT $2 OFFSET $3
` `
@ -276,6 +340,7 @@ type ListLmsPracticesByModuleIDParams struct {
ModuleID pgtype.Int8 `json:"module_id"` ModuleID pgtype.Int8 `json:"module_id"`
Limit int32 `json:"limit"` Limit int32 `json:"limit"`
Offset int32 `json:"offset"` Offset int32 `json:"offset"`
PublishedOnly bool `json:"published_only"`
} }
type ListLmsPracticesByModuleIDRow struct { type ListLmsPracticesByModuleIDRow struct {
@ -290,12 +355,18 @@ type ListLmsPracticesByModuleIDRow struct {
PersonaID pgtype.Int8 `json:"persona_id"` PersonaID pgtype.Int8 `json:"persona_id"`
QuestionSetID int64 `json:"question_set_id"` QuestionSetID int64 `json:"question_set_id"`
QuickTips pgtype.Text `json:"quick_tips"` QuickTips pgtype.Text `json:"quick_tips"`
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"`
} }
func (q *Queries) ListLmsPracticesByModuleID(ctx context.Context, arg ListLmsPracticesByModuleIDParams) ([]ListLmsPracticesByModuleIDRow, error) { func (q *Queries) ListLmsPracticesByModuleID(ctx context.Context, arg ListLmsPracticesByModuleIDParams) ([]ListLmsPracticesByModuleIDRow, error) {
rows, err := q.db.Query(ctx, ListLmsPracticesByModuleID, arg.ModuleID, arg.Limit, arg.Offset) rows, err := q.db.Query(ctx, ListLmsPracticesByModuleID,
arg.ModuleID,
arg.Limit,
arg.Offset,
arg.PublishedOnly,
)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -315,6 +386,7 @@ func (q *Queries) ListLmsPracticesByModuleID(ctx context.Context, arg ListLmsPra
&i.PersonaID, &i.PersonaID,
&i.QuestionSetID, &i.QuestionSetID,
&i.QuickTips, &i.QuickTips,
&i.PublishStatus,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
); err != nil { ); err != nil {
@ -337,9 +409,10 @@ SET
persona_id = COALESCE($4::bigint, persona_id), persona_id = COALESCE($4::bigint, persona_id),
question_set_id = COALESCE($5::bigint, question_set_id), question_set_id = COALESCE($5::bigint, question_set_id),
quick_tips = COALESCE($6::text, quick_tips), quick_tips = COALESCE($6::text, quick_tips),
publish_status = COALESCE($7::varchar, publish_status),
updated_at = CURRENT_TIMESTAMP updated_at = CURRENT_TIMESTAMP
WHERE id = $7 WHERE id = $8
RETURNING id, course_id, module_id, lesson_id, title, story_description, story_image, persona_id, question_set_id, quick_tips, created_at, updated_at RETURNING id, course_id, module_id, lesson_id, title, story_description, story_image, persona_id, question_set_id, quick_tips, created_at, updated_at, publish_status
` `
type UpdateLmsPracticeParams struct { type UpdateLmsPracticeParams struct {
@ -349,6 +422,7 @@ type UpdateLmsPracticeParams struct {
PersonaID pgtype.Int8 `json:"persona_id"` PersonaID pgtype.Int8 `json:"persona_id"`
QuestionSetID pgtype.Int8 `json:"question_set_id"` QuestionSetID pgtype.Int8 `json:"question_set_id"`
QuickTips pgtype.Text `json:"quick_tips"` QuickTips pgtype.Text `json:"quick_tips"`
PublishStatus pgtype.Text `json:"publish_status"`
ID int64 `json:"id"` ID int64 `json:"id"`
} }
@ -360,6 +434,7 @@ func (q *Queries) UpdateLmsPractice(ctx context.Context, arg UpdateLmsPracticePa
arg.PersonaID, arg.PersonaID,
arg.QuestionSetID, arg.QuestionSetID,
arg.QuickTips, arg.QuickTips,
arg.PublishStatus,
arg.ID, arg.ID,
) )
var i LmsPractice var i LmsPractice
@ -376,6 +451,7 @@ func (q *Queries) UpdateLmsPractice(ctx context.Context, arg UpdateLmsPracticePa
&i.QuickTips, &i.QuickTips,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.PublishStatus,
) )
return i, err return i, err
} }

View File

@ -106,6 +106,7 @@ WHERE
lp.course_id = $1 lp.course_id = $1
AND qs.set_type = 'PRACTICE' AND qs.set_type = 'PRACTICE'
AND qs.status = 'PUBLISHED' AND qs.status = 'PUBLISHED'
AND lp.publish_status = 'PUBLISHED'
` `
func (q *Queries) CountPublishedPracticesInCourse(ctx context.Context, courseID pgtype.Int8) (int32, error) { func (q *Queries) CountPublishedPracticesInCourse(ctx context.Context, courseID pgtype.Int8) (int32, error) {
@ -125,6 +126,7 @@ WHERE
lp.module_id = $1 lp.module_id = $1
AND qs.set_type = 'PRACTICE' AND qs.set_type = 'PRACTICE'
AND qs.status = 'PUBLISHED' AND qs.status = 'PUBLISHED'
AND lp.publish_status = 'PUBLISHED'
` `
// Published practices in a module (module-level and lesson-level practices should carry module_id). // Published practices in a module (module-level and lesson-level practices should carry module_id).
@ -146,6 +148,7 @@ WHERE
c.program_id = $1 c.program_id = $1
AND qs.set_type = 'PRACTICE' AND qs.set_type = 'PRACTICE'
AND qs.status = 'PUBLISHED' AND qs.status = 'PUBLISHED'
AND lp.publish_status = 'PUBLISHED'
` `
func (q *Queries) CountPublishedPracticesInProgram(ctx context.Context, programID int64) (int32, error) { func (q *Queries) CountPublishedPracticesInProgram(ctx context.Context, programID int64) (int32, error) {
@ -313,6 +316,7 @@ WHERE
AND upp.completed_at IS NOT NULL AND upp.completed_at IS NOT NULL
AND qs.set_type = 'PRACTICE' AND qs.set_type = 'PRACTICE'
AND qs.status = 'PUBLISHED' AND qs.status = 'PUBLISHED'
AND lp.publish_status = 'PUBLISHED'
` `
type CountUserCompletedPublishedPracticesInModuleParams struct { type CountUserCompletedPublishedPracticesInModuleParams struct {
@ -341,6 +345,7 @@ WHERE
AND upp.completed_at IS NOT NULL AND upp.completed_at IS NOT NULL
AND qs.set_type = 'PRACTICE' AND qs.set_type = 'PRACTICE'
AND qs.status = 'PUBLISHED' AND qs.status = 'PUBLISHED'
AND lp.publish_status = 'PUBLISHED'
` `
type CountUserCompletedPublishedPracticesInProgramParams struct { type CountUserCompletedPublishedPracticesInProgramParams struct {

View File

@ -64,6 +64,7 @@ type ExamPrepLessonPractice struct {
QuickTips pgtype.Text `json:"quick_tips"` QuickTips pgtype.Text `json:"quick_tips"`
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 ExamPrepUnit struct { type ExamPrepUnit struct {
@ -149,6 +150,7 @@ type LmsPractice struct {
QuickTips pgtype.Text `json:"quick_tips"` QuickTips pgtype.Text `json:"quick_tips"`
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 LmsUserCourseProgress struct { type LmsUserCourseProgress struct {

View File

@ -223,6 +223,7 @@ FROM (
user_practice_progress upp user_practice_progress upp
INNER JOIN lms_practices lp ON lp.question_set_id = upp.question_set_id INNER JOIN lms_practices lp ON lp.question_set_id = upp.question_set_id
AND lp.lesson_id IS NOT NULL AND lp.lesson_id IS NOT NULL
AND lp.publish_status = 'PUBLISHED'
INNER JOIN question_sets qs ON qs.id = upp.question_set_id INNER JOIN question_sets qs ON qs.id = upp.question_set_id
AND qs.set_type = 'PRACTICE' AND qs.set_type = 'PRACTICE'
AND qs.status = 'PUBLISHED' AND qs.status = 'PUBLISHED'
@ -254,6 +255,7 @@ FROM (
INNER JOIN lms_practices lp ON lp.question_set_id = upp.question_set_id INNER JOIN lms_practices lp ON lp.question_set_id = upp.question_set_id
AND lp.module_id IS NOT NULL AND lp.module_id IS NOT NULL
AND lp.lesson_id IS NULL AND lp.lesson_id IS NULL
AND lp.publish_status = 'PUBLISHED'
INNER JOIN question_sets qs ON qs.id = upp.question_set_id INNER JOIN question_sets qs ON qs.id = upp.question_set_id
AND qs.set_type = 'PRACTICE' AND qs.set_type = 'PRACTICE'
AND qs.status = 'PUBLISHED' AND qs.status = 'PUBLISHED'
@ -285,6 +287,7 @@ FROM (
AND lp.course_id IS NOT NULL AND lp.course_id IS NOT NULL
AND lp.module_id IS NULL AND lp.module_id IS NULL
AND lp.lesson_id IS NULL AND lp.lesson_id IS NULL
AND lp.publish_status = 'PUBLISHED'
INNER JOIN question_sets qs ON qs.id = upp.question_set_id INNER JOIN question_sets qs ON qs.id = upp.question_set_id
AND qs.set_type = 'PRACTICE' AND qs.set_type = 'PRACTICE'
AND qs.status = 'PUBLISHED' AND qs.status = 'PUBLISHED'

View File

@ -11,11 +11,17 @@ type ExamPrepPractice struct {
StoryImage *string `json:"story_image,omitempty"` StoryImage *string `json:"story_image,omitempty"`
PersonaID *int64 `json:"persona_id,omitempty"` PersonaID *int64 `json:"persona_id,omitempty"`
QuestionSetID int64 `json:"question_set_id"` QuestionSetID int64 `json:"question_set_id"`
PublishStatus PracticePublishStatus `json:"publish_status"`
QuickTips *string `json:"quick_tips,omitempty"` QuickTips *string `json:"quick_tips,omitempty"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at,omitempty"` UpdatedAt *time.Time `json:"updated_at,omitempty"`
} }
// VisibleToLearners mirrors LMS practice visibility rules for subscribers.
func (p ExamPrepPractice) VisibleToLearners() bool {
return p.PublishStatus == PracticePublishPublished
}
// CreateExamPrepPracticeInput is the body for POST .../exam-prep/lessons/{lessonId}/practices (lesson from path). // CreateExamPrepPracticeInput is the body for POST .../exam-prep/lessons/{lessonId}/practices (lesson from path).
type CreateExamPrepPracticeInput struct { type CreateExamPrepPracticeInput struct {
Title string `json:"title" validate:"required"` Title string `json:"title" validate:"required"`
@ -24,6 +30,7 @@ type CreateExamPrepPracticeInput struct {
PersonaID *int64 `json:"persona_id,omitempty"` PersonaID *int64 `json:"persona_id,omitempty"`
QuestionSetID int64 `json:"question_set_id" validate:"required,gt=0"` QuestionSetID int64 `json:"question_set_id" validate:"required,gt=0"`
QuickTips *string `json:"quick_tips,omitempty"` QuickTips *string `json:"quick_tips,omitempty"`
PublishStatus string `json:"publish_status,omitempty" validate:"omitempty,oneof=DRAFT draft PUBLISHED published"`
} }
type UpdateExamPrepPracticeInput struct { type UpdateExamPrepPracticeInput struct {
@ -33,4 +40,5 @@ type UpdateExamPrepPracticeInput struct {
PersonaID *int64 `json:"persona_id,omitempty"` PersonaID *int64 `json:"persona_id,omitempty"`
QuestionSetID *int64 `json:"question_set_id,omitempty"` QuestionSetID *int64 `json:"question_set_id,omitempty"`
QuickTips *string `json:"quick_tips,omitempty"` QuickTips *string `json:"quick_tips,omitempty"`
PublishStatus *string `json:"publish_status,omitempty" validate:"omitempty,oneof=DRAFT draft PUBLISHED published"`
} }

View File

@ -1,6 +1,9 @@
package domain package domain
import "time" import (
"strings"
"time"
)
// ParentKind identifies which hierarchy entity owns a practice (exactly one). // ParentKind identifies which hierarchy entity owns a practice (exactly one).
type ParentKind string type ParentKind string
@ -11,6 +14,31 @@ const (
ParentKindLesson ParentKind = "LESSON" ParentKindLesson ParentKind = "LESSON"
) )
// PracticePublishStatus controls learner visibility for a practice shell (independent of question_set.status).
type PracticePublishStatus string
const (
PracticePublishDraft PracticePublishStatus = "DRAFT"
PracticePublishPublished PracticePublishStatus = "PUBLISHED"
)
// ParsePracticePublishStatusInput maps API input. Empty or unknown values default to PUBLISHED for backward compatibility.
func ParsePracticePublishStatusInput(raw string) PracticePublishStatus {
switch strings.TrimSpace(strings.ToUpper(raw)) {
case string(PracticePublishDraft):
return PracticePublishDraft
case string(PracticePublishPublished):
return PracticePublishPublished
default:
return PracticePublishPublished
}
}
// PracticePublishStatusFromDB maps persisted values into the domain type.
func PracticePublishStatusFromDB(raw string) PracticePublishStatus {
return ParsePracticePublishStatusInput(raw)
}
// Practice is question-set content (story, persona, tips) scoped to a course, module, or lesson. // Practice is question-set content (story, persona, tips) scoped to a course, module, or lesson.
type Practice struct { type Practice struct {
ID int64 `json:"id"` ID int64 `json:"id"`
@ -21,11 +49,17 @@ type Practice struct {
StoryImage *string `json:"story_image,omitempty"` StoryImage *string `json:"story_image,omitempty"`
PersonaID *int64 `json:"persona_id,omitempty"` PersonaID *int64 `json:"persona_id,omitempty"`
QuestionSetID int64 `json:"question_set_id"` QuestionSetID int64 `json:"question_set_id"`
PublishStatus PracticePublishStatus `json:"publish_status"`
QuickTips *string `json:"quick_tips,omitempty"` QuickTips *string `json:"quick_tips,omitempty"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at,omitempty"` UpdatedAt *time.Time `json:"updated_at,omitempty"`
} }
// VisibleToLearners is true when the practice shell should appear in subscribed learner catalogs and progression.
func (p Practice) VisibleToLearners() bool {
return p.PublishStatus == PracticePublishPublished
}
type CreatePracticeInput struct { type CreatePracticeInput struct {
ParentKind ParentKind `json:"parent_kind" validate:"required,oneof=COURSE MODULE LESSON"` ParentKind ParentKind `json:"parent_kind" validate:"required,oneof=COURSE MODULE LESSON"`
ParentID int64 `json:"parent_id" validate:"required,gt=0"` ParentID int64 `json:"parent_id" validate:"required,gt=0"`
@ -35,6 +69,8 @@ type CreatePracticeInput struct {
PersonaID *int64 `json:"persona_id,omitempty"` PersonaID *int64 `json:"persona_id,omitempty"`
QuestionSetID int64 `json:"question_set_id" validate:"required,gt=0"` QuestionSetID int64 `json:"question_set_id" validate:"required,gt=0"`
QuickTips *string `json:"quick_tips,omitempty"` QuickTips *string `json:"quick_tips,omitempty"`
// Omit or empty for backward compatibility defaults to PUBLISHED; set DRAFT to save hidden from learners until published.
PublishStatus string `json:"publish_status,omitempty" validate:"omitempty,oneof=DRAFT draft PUBLISHED published"`
} }
type UpdatePracticeInput struct { type UpdatePracticeInput struct {
@ -44,4 +80,5 @@ type UpdatePracticeInput struct {
PersonaID *int64 `json:"persona_id,omitempty"` PersonaID *int64 `json:"persona_id,omitempty"`
QuestionSetID *int64 `json:"question_set_id,omitempty"` QuestionSetID *int64 `json:"question_set_id,omitempty"`
QuickTips *string `json:"quick_tips,omitempty"` QuickTips *string `json:"quick_tips,omitempty"`
PublishStatus *string `json:"publish_status,omitempty" validate:"omitempty,oneof=DRAFT draft PUBLISHED published"`
} }

View File

@ -9,7 +9,8 @@ import (
type ExamPrepPracticeStore interface { type ExamPrepPracticeStore interface {
CreateExamPrepLessonPractice(ctx context.Context, lessonID int64, in domain.CreateExamPrepPracticeInput) (domain.ExamPrepPractice, error) CreateExamPrepLessonPractice(ctx context.Context, lessonID int64, in domain.CreateExamPrepPracticeInput) (domain.ExamPrepPractice, error)
GetExamPrepLessonPracticeByID(ctx context.Context, id int64) (domain.ExamPrepPractice, error) GetExamPrepLessonPracticeByID(ctx context.Context, id int64) (domain.ExamPrepPractice, error)
ListExamPrepLessonPracticesByLessonID(ctx context.Context, lessonID int64, limit, offset int32) ([]domain.ExamPrepPractice, int64, error) TryGetExamPrepLessonPracticeByQuestionSetID(ctx context.Context, questionSetID int64) (domain.ExamPrepPractice, bool, error)
ListExamPrepLessonPracticesByLessonID(ctx context.Context, lessonID int64, publishedOnly bool, limit, offset int32) ([]domain.ExamPrepPractice, int64, error)
UpdateExamPrepLessonPractice(ctx context.Context, id int64, input domain.UpdateExamPrepPracticeInput) (domain.ExamPrepPractice, error) UpdateExamPrepLessonPractice(ctx context.Context, id int64, input domain.UpdateExamPrepPracticeInput) (domain.ExamPrepPractice, error)
DeleteExamPrepLessonPractice(ctx context.Context, id int64) error DeleteExamPrepLessonPractice(ctx context.Context, id int64) error
} }

View File

@ -23,9 +23,11 @@ type LmsPracticeStore interface {
courseID, moduleID, lessonID *int64, courseID, moduleID, lessonID *int64,
) (domain.Practice, error) ) (domain.Practice, error)
GetLmsPracticeByID(ctx context.Context, id int64) (domain.Practice, error) GetLmsPracticeByID(ctx context.Context, id int64) (domain.Practice, error)
ListLmsPracticesByCourseID(ctx context.Context, courseID int64, limit, offset int32) ([]domain.Practice, int64, error) // TryGetLmsPracticeByQuestionSetID returns false when no LMS practice row references the question set.
ListLmsPracticesByModuleID(ctx context.Context, moduleID int64, limit, offset int32) ([]domain.Practice, int64, error) TryGetLmsPracticeByQuestionSetID(ctx context.Context, questionSetID int64) (domain.Practice, bool, error)
ListLmsPracticesByLessonID(ctx context.Context, lessonID int64, limit, offset int32) ([]domain.Practice, int64, error) ListLmsPracticesByCourseID(ctx context.Context, courseID int64, publishedOnly bool, limit, offset int32) ([]domain.Practice, int64, error)
ListLmsPracticesByModuleID(ctx context.Context, moduleID int64, publishedOnly bool, limit, offset int32) ([]domain.Practice, int64, error)
ListLmsPracticesByLessonID(ctx context.Context, lessonID int64, publishedOnly bool, limit, offset int32) ([]domain.Practice, int64, error)
UpdateLmsPractice(ctx context.Context, id int64, input domain.UpdatePracticeInput) (domain.Practice, error) UpdateLmsPractice(ctx context.Context, id int64, input domain.UpdatePracticeInput) (domain.Practice, error)
DeleteLmsPractice(ctx context.Context, id int64) error DeleteLmsPractice(ctx context.Context, id int64) error
} }

View File

@ -21,6 +21,7 @@ func examPrepPracticeFromListRow(r dbgen.ExamPrepListLessonPracticesByLessonIDRo
PersonaID: r.PersonaID, PersonaID: r.PersonaID,
QuestionSetID: r.QuestionSetID, QuestionSetID: r.QuestionSetID,
QuickTips: r.QuickTips, QuickTips: r.QuickTips,
PublishStatus: r.PublishStatus,
CreatedAt: r.CreatedAt, CreatedAt: r.CreatedAt,
UpdatedAt: r.UpdatedAt, UpdatedAt: r.UpdatedAt,
}) })
@ -32,6 +33,7 @@ func examPrepPracticeToDomain(p dbgen.ExamPrepLessonPractice) domain.ExamPrepPra
LessonID: p.UnitModuleLessonID, LessonID: p.UnitModuleLessonID,
Title: p.Title, Title: p.Title,
QuestionSetID: p.QuestionSetID, QuestionSetID: p.QuestionSetID,
PublishStatus: domain.PracticePublishStatusFromDB(p.PublishStatus),
} }
out.StoryDescription = fromPgText(p.StoryDescription) out.StoryDescription = fromPgText(p.StoryDescription)
out.StoryImage = fromPgText(p.StoryImage) out.StoryImage = fromPgText(p.StoryImage)
@ -46,6 +48,7 @@ func examPrepPracticeToDomain(p dbgen.ExamPrepLessonPractice) domain.ExamPrepPra
} }
func (s *Store) CreateExamPrepLessonPractice(ctx context.Context, lessonID int64, in domain.CreateExamPrepPracticeInput) (domain.ExamPrepPractice, error) { func (s *Store) CreateExamPrepLessonPractice(ctx context.Context, lessonID int64, in domain.CreateExamPrepPracticeInput) (domain.ExamPrepPractice, error) {
ps := domain.ParsePracticePublishStatusInput(in.PublishStatus)
p, err := s.queries.ExamPrepCreateLessonPractice(ctx, dbgen.ExamPrepCreateLessonPracticeParams{ p, err := s.queries.ExamPrepCreateLessonPractice(ctx, dbgen.ExamPrepCreateLessonPracticeParams{
UnitModuleLessonID: lessonID, UnitModuleLessonID: lessonID,
Title: in.Title, Title: in.Title,
@ -54,6 +57,7 @@ func (s *Store) CreateExamPrepLessonPractice(ctx context.Context, lessonID int64
PersonaID: int64PtrToPg8(in.PersonaID), PersonaID: int64PtrToPg8(in.PersonaID),
QuestionSetID: in.QuestionSetID, QuestionSetID: in.QuestionSetID,
QuickTips: toPgText(in.QuickTips), QuickTips: toPgText(in.QuickTips),
PublishStatus: string(ps),
}) })
if err != nil { if err != nil {
return domain.ExamPrepPractice{}, err return domain.ExamPrepPractice{}, err
@ -72,9 +76,22 @@ func (s *Store) GetExamPrepLessonPracticeByID(ctx context.Context, id int64) (do
return examPrepPracticeToDomain(p), nil return examPrepPracticeToDomain(p), nil
} }
func (s *Store) ListExamPrepLessonPracticesByLessonID(ctx context.Context, lessonID int64, limit, offset int32) ([]domain.ExamPrepPractice, int64, error) { // TryGetExamPrepLessonPracticeByQuestionSetID returns false when no row exists.
func (s *Store) TryGetExamPrepLessonPracticeByQuestionSetID(ctx context.Context, questionSetID int64) (domain.ExamPrepPractice, bool, error) {
p, err := s.queries.ExamPrepGetLessonPracticeByQuestionSetID(ctx, questionSetID)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return domain.ExamPrepPractice{}, false, nil
}
return domain.ExamPrepPractice{}, false, err
}
return examPrepPracticeToDomain(p), true, nil
}
func (s *Store) ListExamPrepLessonPracticesByLessonID(ctx context.Context, lessonID int64, publishedOnly bool, limit, offset int32) ([]domain.ExamPrepPractice, int64, error) {
rows, err := s.queries.ExamPrepListLessonPracticesByLessonID(ctx, dbgen.ExamPrepListLessonPracticesByLessonIDParams{ rows, err := s.queries.ExamPrepListLessonPracticesByLessonID(ctx, dbgen.ExamPrepListLessonPracticesByLessonIDParams{
UnitModuleLessonID: lessonID, UnitModuleLessonID: lessonID,
PublishedOnly: publishedOnly,
Limit: limit, Limit: limit,
Offset: offset, Offset: offset,
}) })
@ -111,6 +128,7 @@ func (s *Store) UpdateExamPrepLessonPractice(ctx context.Context, id int64, inpu
PersonaID: optionalInt8UpdateID(input.PersonaID), PersonaID: optionalInt8UpdateID(input.PersonaID),
QuestionSetID: qs, QuestionSetID: qs,
QuickTips: optionalTextUpdate(input.QuickTips), QuickTips: optionalTextUpdate(input.QuickTips),
PublishStatus: optionalPublishStatusUpdate(input.PublishStatus),
}) })
if err != nil { if err != nil {
if errors.Is(err, pgx.ErrNoRows) { if errors.Is(err, pgx.ErrNoRows) {

View File

@ -26,11 +26,19 @@ func fromPgInt8ID(c pgtype.Int8) *int64 {
return &v return &v
} }
func optionalInt8UpdateID(val *int64) pgtype.Int8 {
if val == nil {
return pgtype.Int8{Valid: false}
}
return pgtype.Int8{Int64: *val, Valid: true}
}
func lmsPracticeToDomain(p dbgen.LmsPractice) domain.Practice { func lmsPracticeToDomain(p dbgen.LmsPractice) domain.Practice {
out := domain.Practice{ out := domain.Practice{
ID: p.ID, ID: p.ID,
Title: p.Title, Title: p.Title,
QuestionSetID: p.QuestionSetID, QuestionSetID: p.QuestionSetID,
PublishStatus: domain.PracticePublishStatusFromDB(p.PublishStatus),
} }
if p.CourseID.Valid { if p.CourseID.Valid {
out.ParentKind = domain.ParentKindCourse out.ParentKind = domain.ParentKindCourse
@ -55,7 +63,9 @@ func lmsPracticeToDomain(p dbgen.LmsPractice) domain.Practice {
} }
func lmsFromListRow( func lmsFromListRow(
id, qid int64, title string, id, qid int64,
publishStatus string,
title string,
cid, mid, lid pgtype.Int8, cid, mid, lid pgtype.Int8,
sd, si, qt pgtype.Text, pid pgtype.Int8, sd, si, qt pgtype.Text, pid pgtype.Int8,
ca, ua pgtype.Timestamptz, ca, ua pgtype.Timestamptz,
@ -71,6 +81,7 @@ func lmsFromListRow(
PersonaID: pid, PersonaID: pid,
QuestionSetID: qid, QuestionSetID: qid,
QuickTips: qt, QuickTips: qt,
PublishStatus: publishStatus,
CreatedAt: ca, CreatedAt: ca,
UpdatedAt: ua, UpdatedAt: ua,
}) })
@ -82,6 +93,7 @@ func (s *Store) CreateLmsPractice(
in domain.CreatePracticeInput, in domain.CreatePracticeInput,
courseID, moduleID, lessonID *int64, courseID, moduleID, lessonID *int64,
) (domain.Practice, error) { ) (domain.Practice, error) {
ps := domain.ParsePracticePublishStatusInput(in.PublishStatus)
p, err := s.queries.CreateLmsPractice(ctx, dbgen.CreateLmsPracticeParams{ p, err := s.queries.CreateLmsPractice(ctx, dbgen.CreateLmsPracticeParams{
CourseID: int64PtrToPg8(courseID), CourseID: int64PtrToPg8(courseID),
ModuleID: int64PtrToPg8(moduleID), ModuleID: int64PtrToPg8(moduleID),
@ -92,6 +104,7 @@ func (s *Store) CreateLmsPractice(
PersonaID: int64PtrToPg8(in.PersonaID), PersonaID: int64PtrToPg8(in.PersonaID),
QuestionSetID: in.QuestionSetID, QuestionSetID: in.QuestionSetID,
QuickTips: toPgText(in.QuickTips), QuickTips: toPgText(in.QuickTips),
PublishStatus: string(ps),
}) })
if err != nil { if err != nil {
return domain.Practice{}, err return domain.Practice{}, err
@ -110,9 +123,22 @@ func (s *Store) GetLmsPracticeByID(ctx context.Context, id int64) (domain.Practi
return lmsPracticeToDomain(p), nil return lmsPracticeToDomain(p), nil
} }
func (s *Store) ListLmsPracticesByCourseID(ctx context.Context, courseID int64, limit, offset int32) ([]domain.Practice, int64, error) { // TryGetLmsPracticeByQuestionSetID returns false when no row exists.
func (s *Store) TryGetLmsPracticeByQuestionSetID(ctx context.Context, questionSetID int64) (domain.Practice, bool, error) {
p, err := s.queries.GetLmsPracticeByQuestionSetID(ctx, questionSetID)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return domain.Practice{}, false, nil
}
return domain.Practice{}, false, err
}
return lmsPracticeToDomain(p), true, nil
}
func (s *Store) ListLmsPracticesByCourseID(ctx context.Context, courseID int64, publishedOnly bool, limit, offset int32) ([]domain.Practice, int64, error) {
rows, err := s.queries.ListLmsPracticesByCourseID(ctx, dbgen.ListLmsPracticesByCourseIDParams{ rows, err := s.queries.ListLmsPracticesByCourseID(ctx, dbgen.ListLmsPracticesByCourseIDParams{
CourseID: pgtype.Int8{Int64: courseID, Valid: true}, CourseID: pgtype.Int8{Int64: courseID, Valid: true},
PublishedOnly: publishedOnly,
Limit: limit, Limit: limit,
Offset: offset, Offset: offset,
}) })
@ -129,7 +155,7 @@ func (s *Store) ListLmsPracticesByCourseID(ctx context.Context, courseID int64,
total = r.TotalCount total = r.TotalCount
} }
out = append(out, lmsFromListRow( out = append(out, lmsFromListRow(
r.ID, r.QuestionSetID, r.Title, r.ID, r.QuestionSetID, r.PublishStatus, r.Title,
r.CourseID, r.ModuleID, r.LessonID, r.CourseID, r.ModuleID, r.LessonID,
r.StoryDescription, r.StoryImage, r.QuickTips, r.StoryDescription, r.StoryImage, r.QuickTips,
r.PersonaID, r.CreatedAt, r.UpdatedAt, r.PersonaID, r.CreatedAt, r.UpdatedAt,
@ -138,9 +164,10 @@ func (s *Store) ListLmsPracticesByCourseID(ctx context.Context, courseID int64,
return out, total, nil return out, total, nil
} }
func (s *Store) ListLmsPracticesByModuleID(ctx context.Context, moduleID int64, limit, offset int32) ([]domain.Practice, int64, error) { func (s *Store) ListLmsPracticesByModuleID(ctx context.Context, moduleID int64, publishedOnly bool, limit, offset int32) ([]domain.Practice, int64, error) {
rows, err := s.queries.ListLmsPracticesByModuleID(ctx, dbgen.ListLmsPracticesByModuleIDParams{ rows, err := s.queries.ListLmsPracticesByModuleID(ctx, dbgen.ListLmsPracticesByModuleIDParams{
ModuleID: pgtype.Int8{Int64: moduleID, Valid: true}, ModuleID: pgtype.Int8{Int64: moduleID, Valid: true},
PublishedOnly: publishedOnly,
Limit: limit, Limit: limit,
Offset: offset, Offset: offset,
}) })
@ -157,7 +184,7 @@ func (s *Store) ListLmsPracticesByModuleID(ctx context.Context, moduleID int64,
total = r.TotalCount total = r.TotalCount
} }
out = append(out, lmsFromListRow( out = append(out, lmsFromListRow(
r.ID, r.QuestionSetID, r.Title, r.ID, r.QuestionSetID, r.PublishStatus, r.Title,
r.CourseID, r.ModuleID, r.LessonID, r.CourseID, r.ModuleID, r.LessonID,
r.StoryDescription, r.StoryImage, r.QuickTips, r.StoryDescription, r.StoryImage, r.QuickTips,
r.PersonaID, r.CreatedAt, r.UpdatedAt, r.PersonaID, r.CreatedAt, r.UpdatedAt,
@ -166,9 +193,10 @@ func (s *Store) ListLmsPracticesByModuleID(ctx context.Context, moduleID int64,
return out, total, nil return out, total, nil
} }
func (s *Store) ListLmsPracticesByLessonID(ctx context.Context, lessonID int64, limit, offset int32) ([]domain.Practice, int64, error) { func (s *Store) ListLmsPracticesByLessonID(ctx context.Context, lessonID int64, publishedOnly bool, limit, offset int32) ([]domain.Practice, int64, error) {
rows, err := s.queries.ListLmsPracticesByLessonID(ctx, dbgen.ListLmsPracticesByLessonIDParams{ rows, err := s.queries.ListLmsPracticesByLessonID(ctx, dbgen.ListLmsPracticesByLessonIDParams{
LessonID: pgtype.Int8{Int64: lessonID, Valid: true}, LessonID: pgtype.Int8{Int64: lessonID, Valid: true},
PublishedOnly: publishedOnly,
Limit: limit, Limit: limit,
Offset: offset, Offset: offset,
}) })
@ -185,7 +213,7 @@ func (s *Store) ListLmsPracticesByLessonID(ctx context.Context, lessonID int64,
total = r.TotalCount total = r.TotalCount
} }
out = append(out, lmsFromListRow( out = append(out, lmsFromListRow(
r.ID, r.QuestionSetID, r.Title, r.ID, r.QuestionSetID, r.PublishStatus, r.Title,
r.CourseID, r.ModuleID, r.LessonID, r.CourseID, r.ModuleID, r.LessonID,
r.StoryDescription, r.StoryImage, r.QuickTips, r.StoryDescription, r.StoryImage, r.QuickTips,
r.PersonaID, r.CreatedAt, r.UpdatedAt, r.PersonaID, r.CreatedAt, r.UpdatedAt,
@ -194,13 +222,6 @@ func (s *Store) ListLmsPracticesByLessonID(ctx context.Context, lessonID int64,
return out, total, nil return out, total, nil
} }
func optionalInt8UpdateID(val *int64) pgtype.Int8 {
if val == nil {
return pgtype.Int8{Valid: false}
}
return pgtype.Int8{Int64: *val, Valid: true}
}
func (s *Store) UpdateLmsPractice(ctx context.Context, id int64, input domain.UpdatePracticeInput) (domain.Practice, error) { func (s *Store) UpdateLmsPractice(ctx context.Context, id int64, input domain.UpdatePracticeInput) (domain.Practice, error) {
var titleText pgtype.Text var titleText pgtype.Text
if input.Title != nil { if input.Title != nil {
@ -217,6 +238,7 @@ func (s *Store) UpdateLmsPractice(ctx context.Context, id int64, input domain.Up
PersonaID: optionalInt8UpdateID(input.PersonaID), PersonaID: optionalInt8UpdateID(input.PersonaID),
QuestionSetID: qs, QuestionSetID: qs,
QuickTips: optionalTextUpdate(input.QuickTips), QuickTips: optionalTextUpdate(input.QuickTips),
PublishStatus: optionalPublishStatusUpdate(input.PublishStatus),
}) })
if err != nil { if err != nil {
if errors.Is(err, pgx.ErrNoRows) { if errors.Is(err, pgx.ErrNoRows) {

View File

@ -3,6 +3,7 @@ package repository
import ( import (
"context" "context"
"errors" "errors"
"strings"
dbgen "Yimaru-Backend/gen/db" dbgen "Yimaru-Backend/gen/db"
"Yimaru-Backend/internal/domain" "Yimaru-Backend/internal/domain"
@ -117,6 +118,19 @@ func optionalTextUpdate(val *string) pgtype.Text {
return pgtype.Text{String: *val, Valid: true} return pgtype.Text{String: *val, Valid: true}
} }
func optionalPublishStatusUpdate(val *string) pgtype.Text {
if val == nil {
return pgtype.Text{Valid: false}
}
s := strings.TrimSpace(strings.ToUpper(*val))
switch s {
case string(domain.PracticePublishDraft), string(domain.PracticePublishPublished):
return pgtype.Text{String: s, Valid: true}
default:
return pgtype.Text{Valid: false}
}
}
func optionalInt4Update(v *int) pgtype.Int4 { func optionalInt4Update(v *int) pgtype.Int4 {
if v == nil { if v == nil {
return pgtype.Int4{Valid: false} return pgtype.Int4{Valid: false}

View File

@ -358,7 +358,7 @@ func (s *Service) CreateExamPrepPractice(ctx context.Context, lessonID int64, in
return s.store.CreateExamPrepLessonPractice(ctx, lessonID, input) return s.store.CreateExamPrepLessonPractice(ctx, lessonID, input)
} }
func (s *Service) ListExamPrepPracticesByLesson(ctx context.Context, lessonID int64, limit, offset int32) ([]domain.ExamPrepPractice, int64, error) { func (s *Service) ListExamPrepPracticesByLesson(ctx context.Context, lessonID int64, publishedOnly bool, limit, offset int32) ([]domain.ExamPrepPractice, int64, error) {
if err := s.ensureLesson(ctx, lessonID); err != nil { if err := s.ensureLesson(ctx, lessonID); err != nil {
return nil, 0, err return nil, 0, err
} }
@ -371,7 +371,7 @@ func (s *Service) ListExamPrepPracticesByLesson(ctx context.Context, lessonID in
if offset < 0 { if offset < 0 {
offset = 0 offset = 0
} }
return s.store.ListExamPrepLessonPracticesByLessonID(ctx, lessonID, limit, offset) return s.store.ListExamPrepLessonPracticesByLessonID(ctx, lessonID, publishedOnly, limit, offset)
} }
func (s *Service) GetExamPrepPracticeByID(ctx context.Context, id int64) (domain.ExamPrepPractice, error) { func (s *Service) GetExamPrepPracticeByID(ctx context.Context, id int64) (domain.ExamPrepPractice, error) {
@ -385,6 +385,10 @@ func (s *Service) GetExamPrepPracticeByID(ctx context.Context, id int64) (domain
return p, nil return p, nil
} }
func (s *Service) TryGetExamPrepPracticeByQuestionSetID(ctx context.Context, questionSetID int64) (domain.ExamPrepPractice, bool, error) {
return s.store.TryGetExamPrepLessonPracticeByQuestionSetID(ctx, questionSetID)
}
func (s *Service) UpdateExamPrepPractice(ctx context.Context, id int64, input domain.UpdateExamPrepPracticeInput) (domain.ExamPrepPractice, error) { func (s *Service) UpdateExamPrepPractice(ctx context.Context, id int64, input domain.UpdateExamPrepPracticeInput) (domain.ExamPrepPractice, error) {
p, err := s.store.UpdateExamPrepLessonPractice(ctx, id, input) p, err := s.store.UpdateExamPrepLessonPractice(ctx, id, input)
if err != nil { if err != nil {

View File

@ -115,6 +115,10 @@ func (s *Service) Create(ctx context.Context, in domain.CreatePracticeInput) (do
return s.practices.CreateLmsPractice(ctx, in, courseID, moduleID, lessonID) return s.practices.CreateLmsPractice(ctx, in, courseID, moduleID, lessonID)
} }
func (s *Service) TryGetByQuestionSetID(ctx context.Context, questionSetID int64) (domain.Practice, bool, error) {
return s.practices.TryGetLmsPracticeByQuestionSetID(ctx, questionSetID)
}
func (s *Service) GetByID(ctx context.Context, id int64) (domain.Practice, error) { func (s *Service) GetByID(ctx context.Context, id int64) (domain.Practice, error) {
p, err := s.practices.GetLmsPracticeByID(ctx, id) p, err := s.practices.GetLmsPracticeByID(ctx, id)
if err != nil { if err != nil {
@ -139,7 +143,7 @@ func clampPracticePage(limit, offset int32) (int32, int32) {
return limit, offset return limit, offset
} }
func (s *Service) ListByCourse(ctx context.Context, courseID int64, limit, offset int32) ([]domain.Practice, int64, error) { func (s *Service) ListByCourse(ctx context.Context, courseID int64, publishedOnly bool, limit, offset int32) ([]domain.Practice, int64, error) {
if _, err := s.courses.GetCourseByID(ctx, courseID); err != nil { if _, err := s.courses.GetCourseByID(ctx, courseID); err != nil {
if errors.Is(err, pgx.ErrNoRows) { if errors.Is(err, pgx.ErrNoRows) {
return nil, 0, courses.ErrCourseNotFound return nil, 0, courses.ErrCourseNotFound
@ -147,10 +151,10 @@ func (s *Service) ListByCourse(ctx context.Context, courseID int64, limit, offse
return nil, 0, err return nil, 0, err
} }
limit, offset = clampPracticePage(limit, offset) limit, offset = clampPracticePage(limit, offset)
return s.practices.ListLmsPracticesByCourseID(ctx, courseID, limit, offset) return s.practices.ListLmsPracticesByCourseID(ctx, courseID, publishedOnly, limit, offset)
} }
func (s *Service) ListByModule(ctx context.Context, moduleID int64, limit, offset int32) ([]domain.Practice, int64, error) { func (s *Service) ListByModule(ctx context.Context, moduleID int64, publishedOnly bool, limit, offset int32) ([]domain.Practice, int64, error) {
if _, err := s.modules.GetModuleByID(ctx, moduleID); err != nil { if _, err := s.modules.GetModuleByID(ctx, moduleID); err != nil {
if errors.Is(err, pgx.ErrNoRows) { if errors.Is(err, pgx.ErrNoRows) {
return nil, 0, modules.ErrModuleNotFound return nil, 0, modules.ErrModuleNotFound
@ -158,10 +162,10 @@ func (s *Service) ListByModule(ctx context.Context, moduleID int64, limit, offse
return nil, 0, err return nil, 0, err
} }
limit, offset = clampPracticePage(limit, offset) limit, offset = clampPracticePage(limit, offset)
return s.practices.ListLmsPracticesByModuleID(ctx, moduleID, limit, offset) return s.practices.ListLmsPracticesByModuleID(ctx, moduleID, publishedOnly, limit, offset)
} }
func (s *Service) ListByLesson(ctx context.Context, lessonID int64, limit, offset int32) ([]domain.Practice, int64, error) { func (s *Service) ListByLesson(ctx context.Context, lessonID int64, publishedOnly bool, limit, offset int32) ([]domain.Practice, int64, error) {
if _, err := s.lessons.GetLessonByID(ctx, lessonID); err != nil { if _, err := s.lessons.GetLessonByID(ctx, lessonID); err != nil {
if errors.Is(err, pgx.ErrNoRows) { if errors.Is(err, pgx.ErrNoRows) {
return nil, 0, lessons.ErrLessonNotFound return nil, 0, lessons.ErrLessonNotFound
@ -169,7 +173,7 @@ func (s *Service) ListByLesson(ctx context.Context, lessonID int64, limit, offse
return nil, 0, err return nil, 0, err
} }
limit, offset = clampPracticePage(limit, offset) limit, offset = clampPracticePage(limit, offset)
return s.practices.ListLmsPracticesByLessonID(ctx, lessonID, limit, offset) return s.practices.ListLmsPracticesByLessonID(ctx, lessonID, publishedOnly, limit, offset)
} }
func (s *Service) Update(ctx context.Context, id int64, input domain.UpdatePracticeInput) (domain.Practice, error) { func (s *Service) Update(ctx context.Context, id int64, input domain.UpdatePracticeInput) (domain.Practice, error) {

View File

@ -74,7 +74,8 @@ func (h *Handler) ListExamPrepPracticesByLesson(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.ListExamPrepPracticesByLesson(c.Context(), lessonID, int32(limit), int32(offset)) publishedOnly := !h.canManageExamPrepPractices(c)
items, total, err := h.examPrepSvc.ListExamPrepPracticesByLesson(c.Context(), lessonID, publishedOnly, int32(limit), int32(offset))
if err != nil { if err != nil {
if errors.Is(err, examprep.ErrLessonNotFound) { if errors.Is(err, examprep.ErrLessonNotFound) {
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{ return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
@ -126,6 +127,9 @@ func (h *Handler) GetExamPrepPracticeByID(c *fiber.Ctx) error {
Error: err.Error(), Error: err.Error(),
}) })
} }
if !p.VisibleToLearners() && !h.canManageExamPrepPractices(c) {
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{Message: "Practice not found"})
}
return c.JSON(domain.Response{ return c.JSON(domain.Response{
Message: "Practice retrieved successfully", Message: "Practice retrieved successfully",
Data: p, Data: p,

View File

@ -76,7 +76,8 @@ func (h *Handler) ListPracticesByCourse(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.practiceSvc.ListByCourse(c.Context(), courseID, int32(limit), int32(offset)) publishedOnly := !h.canManageLMSPractices(c)
items, total, err := h.practiceSvc.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{Message: "Course not found", Error: err.Error()}) return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{Message: "Course not found", Error: err.Error()})
@ -107,7 +108,8 @@ func (h *Handler) ListPracticesByModule(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.practiceSvc.ListByModule(c.Context(), moduleID, int32(limit), int32(offset)) publishedOnly := !h.canManageLMSPractices(c)
items, total, err := h.practiceSvc.ListByModule(c.Context(), moduleID, publishedOnly, int32(limit), int32(offset))
if err != nil { if err != nil {
if errors.Is(err, modules.ErrModuleNotFound) { if errors.Is(err, modules.ErrModuleNotFound) {
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{Message: "Module not found", Error: err.Error()}) return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{Message: "Module not found", Error: err.Error()})
@ -138,7 +140,8 @@ func (h *Handler) ListPracticesByLesson(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.practiceSvc.ListByLesson(c.Context(), lessonID, int32(limit), int32(offset)) publishedOnly := !h.canManageLMSPractices(c)
items, total, err := h.practiceSvc.ListByLesson(c.Context(), lessonID, publishedOnly, int32(limit), int32(offset))
if err != nil { if err != nil {
if errors.Is(err, lessons.ErrLessonNotFound) { if errors.Is(err, lessons.ErrLessonNotFound) {
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{Message: "Lesson not found", Error: err.Error()}) return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{Message: "Lesson not found", Error: err.Error()})
@ -174,6 +177,9 @@ func (h *Handler) GetPractice(c *fiber.Ctx) error {
} }
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load practice", Error: err.Error()}) return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load practice", Error: err.Error()})
} }
if !p.VisibleToLearners() && !h.canManageLMSPractices(c) {
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{Message: "Practice not found"})
}
return c.JSON(domain.Response{Message: "Practice retrieved successfully", Data: p, Success: true, StatusCode: fiber.StatusOK}) return c.JSON(domain.Response{Message: "Practice retrieved successfully", Data: p, Success: true, StatusCode: fiber.StatusOK})
} }

View File

@ -0,0 +1,51 @@
package handlers
import (
"Yimaru-Backend/internal/domain"
"github.com/gofiber/fiber/v2"
)
func (h *Handler) canManageLMSPractices(c *fiber.Ctx) bool {
rn := string(c.Locals("role").(domain.Role))
return h.rbacSvc.HasPermission(rn, "practices.create") || h.rbacSvc.HasPermission(rn, "practices.update")
}
func (h *Handler) canManageExamPrepPractices(c *fiber.Ctx) bool {
rn := string(c.Locals("role").(domain.Role))
return h.rbacSvc.HasPermission(rn, "exam_prep.practices.create") || h.rbacSvc.HasPermission(rn, "exam_prep.practices.update")
}
// forbidIfLinkedPracticeDraftForSubscriber returns 404 for draft LMS/exam-prep shells when the caller cannot manage content.
func (h *Handler) forbidIfLinkedPracticeDraftForSubscriber(c *fiber.Ctx, questionSetID int64) error {
if lp, ok, err := h.practiceSvc.TryGetByQuestionSetID(c.Context(), questionSetID); err != nil {
return err
} else if ok && !lp.VisibleToLearners() && !h.canManageLMSPractices(c) {
return fiber.NewError(fiber.StatusNotFound, "Practice not found")
}
if ep, ok, err := h.examPrepSvc.TryGetExamPrepPracticeByQuestionSetID(c.Context(), questionSetID); err != nil {
return err
} else if ok && !ep.VisibleToLearners() && !h.canManageExamPrepPractices(c) {
return fiber.NewError(fiber.StatusNotFound, "Practice not found")
}
return nil
}
// forbidCompletingDraftPractice blocks completion when an LMS/exam-prep shell is still in draft (learners/students path).
func (h *Handler) forbidCompletingDraftPractice(c *fiber.Ctx, questionSetID int64) error {
if lp, ok, err := h.practiceSvc.TryGetByQuestionSetID(c.Context(), questionSetID); err != nil {
return err
} else if ok && !lp.VisibleToLearners() {
return fiber.NewError(fiber.StatusForbidden, "Only published practices can be completed")
}
if ep, ok, err := h.examPrepSvc.TryGetExamPrepPracticeByQuestionSetID(c.Context(), questionSetID); err != nil {
return err
} else if ok && !ep.VisibleToLearners() {
return fiber.NewError(fiber.StatusForbidden, "Only published practices can be completed")
}
return nil
}

View File

@ -4,6 +4,7 @@ import (
"Yimaru-Backend/internal/domain" "Yimaru-Backend/internal/domain"
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
@ -1469,6 +1470,20 @@ func (h *Handler) GetQuestionsByPractice(c *fiber.Ctx) error {
}) })
} }
if err := h.forbidIfLinkedPracticeDraftForSubscriber(c, set.ID); err != nil {
code := fiber.StatusInternalServerError
msg := err.Error()
var ferr *fiber.Error
if errors.As(err, &ferr) {
code = ferr.Code
msg = ferr.Message
}
return c.Status(code).JSON(domain.ErrorResponse{
Message: msg,
Error: err.Error(),
})
}
if err := h.enforcePracticeSequenceForStudent(c, set); err != nil { if err := h.enforcePracticeSequenceForStudent(c, set); err != nil {
status := fiber.StatusForbidden status := fiber.StatusForbidden
if ferr, ok := err.(*fiber.Error); ok { if ferr, ok := err.(*fiber.Error); ok {
@ -1552,6 +1567,11 @@ func (h *Handler) CompletePractice(c *fiber.Ctx) error {
var set domain.QuestionSet var set domain.QuestionSet
var setErr error var setErr error
if practiceErr == nil { if practiceErr == nil {
if !practice.VisibleToLearners() {
return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{
Message: "Only published practices can be completed",
})
}
set, setErr = h.questionsSvc.GetQuestionSetByID(c.Context(), practice.QuestionSetID) set, setErr = h.questionsSvc.GetQuestionSetByID(c.Context(), practice.QuestionSetID)
} else { } else {
// Backward compatibility: also accept question_set.id directly. // Backward compatibility: also accept question_set.id directly.
@ -1566,6 +1586,21 @@ func (h *Handler) CompletePractice(c *fiber.Ctx) error {
if !strings.EqualFold(set.SetType, string(domain.QuestionSetTypePractice)) { if !strings.EqualFold(set.SetType, string(domain.QuestionSetTypePractice)) {
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{Message: "Practice not found"}) return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{Message: "Practice not found"})
} }
if practiceErr != nil {
if err := h.forbidCompletingDraftPractice(c, set.ID); err != nil {
code := fiber.StatusInternalServerError
msg := err.Error()
var ferr *fiber.Error
if errors.As(err, &ferr) {
code = ferr.Code
msg = ferr.Message
}
return c.Status(code).JSON(domain.ErrorResponse{
Message: msg,
Error: err.Error(),
})
}
}
// Enforce sequential gating only for published practices. // Enforce sequential gating only for published practices.
if strings.EqualFold(set.Status, "PUBLISHED") { if strings.EqualFold(set.Status, "PUBLISHED") {