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:
parent
37aef49e28
commit
12ad59c409
5
db/migrations/000060_practice_publish_status.down.sql
Normal file
5
db/migrations/000060_practice_publish_status.down.sql
Normal 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;
|
||||
8
db/migrations/000060_practice_publish_status.up.sql
Normal file
8
db/migrations/000060_practice_publish_status.up.sql
Normal 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'));
|
||||
|
|
@ -6,8 +6,9 @@ INSERT INTO exam_prep.lesson_practices (
|
|||
story_image,
|
||||
persona_id,
|
||||
question_set_id,
|
||||
quick_tips
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
quick_tips,
|
||||
publish_status
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
RETURNING *;
|
||||
|
||||
-- name: ExamPrepGetLessonPracticeByID :one
|
||||
|
|
@ -15,6 +16,13 @@ SELECT *
|
|||
FROM exam_prep.lesson_practices
|
||||
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
|
||||
SELECT
|
||||
COUNT(*) OVER () AS total_count,
|
||||
|
|
@ -26,10 +34,15 @@ SELECT
|
|||
p.persona_id,
|
||||
p.question_set_id,
|
||||
p.quick_tips,
|
||||
p.publish_status,
|
||||
p.created_at,
|
||||
p.updated_at
|
||||
FROM exam_prep.lesson_practices p
|
||||
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
|
||||
LIMIT $2
|
||||
OFFSET $3;
|
||||
|
|
@ -43,6 +56,7 @@ SET
|
|||
persona_id = coalesce(sqlc.narg('persona_id')::bigint, persona_id),
|
||||
question_set_id = coalesce(sqlc.narg('question_set_id')::bigint, question_set_id),
|
||||
quick_tips = coalesce(sqlc.narg('quick_tips')::text, quick_tips),
|
||||
publish_status = coalesce(sqlc.narg('publish_status')::varchar, publish_status),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = sqlc.arg('id')
|
||||
RETURNING *;
|
||||
|
|
|
|||
|
|
@ -86,6 +86,7 @@ UNION ALL
|
|||
user_practice_progress upp
|
||||
INNER JOIN lms_practices lp ON lp.question_set_id = upp.question_set_id
|
||||
AND lp.lesson_id IS NOT NULL
|
||||
AND lp.publish_status = 'PUBLISHED'
|
||||
INNER JOIN question_sets qs ON qs.id = upp.question_set_id
|
||||
AND qs.set_type = 'PRACTICE'
|
||||
AND qs.status = 'PUBLISHED'
|
||||
|
|
@ -132,6 +133,7 @@ UNION ALL
|
|||
INNER JOIN lms_practices lp ON lp.question_set_id = upp.question_set_id
|
||||
AND lp.module_id IS NOT NULL
|
||||
AND lp.lesson_id IS NULL
|
||||
AND lp.publish_status = 'PUBLISHED'
|
||||
INNER JOIN question_sets qs ON qs.id = upp.question_set_id
|
||||
AND qs.set_type = 'PRACTICE'
|
||||
AND qs.status = 'PUBLISHED'
|
||||
|
|
@ -176,6 +178,7 @@ UNION ALL
|
|||
AND lp.course_id IS NOT NULL
|
||||
AND lp.module_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
|
||||
AND qs.set_type = 'PRACTICE'
|
||||
AND qs.status = 'PUBLISHED'
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ SELECT
|
|||
WHERE p.course_id = c.id
|
||||
AND p.module_id IS NULL
|
||||
AND p.lesson_id IS NULL
|
||||
AND p.publish_status = 'PUBLISHED'
|
||||
) AS has_practice
|
||||
FROM courses
|
||||
c
|
||||
|
|
@ -75,13 +76,15 @@ SELECT
|
|||
WHERE
|
||||
p.course_id = c.id
|
||||
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 (
|
||||
SELECT 1
|
||||
FROM lms_practices p
|
||||
WHERE p.course_id = c.id
|
||||
AND p.module_id IS NULL
|
||||
AND p.lesson_id IS NULL
|
||||
AND p.publish_status = 'PUBLISHED'
|
||||
) AS has_practice
|
||||
FROM
|
||||
courses c
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ SELECT
|
|||
SELECT 1
|
||||
FROM lms_practices p
|
||||
WHERE p.lesson_id = l.id
|
||||
AND p.publish_status = 'PUBLISHED'
|
||||
) AS has_practice
|
||||
FROM lessons
|
||||
l
|
||||
|
|
@ -43,6 +44,7 @@ SELECT
|
|||
SELECT 1
|
||||
FROM lms_practices p
|
||||
WHERE p.lesson_id = l.id
|
||||
AND p.publish_status = 'PUBLISHED'
|
||||
) AS has_practice
|
||||
FROM
|
||||
lessons l
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ SELECT
|
|||
FROM lms_practices p
|
||||
WHERE p.module_id = m.id
|
||||
AND p.lesson_id IS NULL
|
||||
AND p.publish_status = 'PUBLISHED'
|
||||
) AS has_practice
|
||||
FROM modules
|
||||
m
|
||||
|
|
@ -55,6 +56,7 @@ SELECT
|
|||
FROM lms_practices p
|
||||
WHERE p.module_id = m.id
|
||||
AND p.lesson_id IS NULL
|
||||
AND p.publish_status = 'PUBLISHED'
|
||||
) AS has_practice
|
||||
FROM
|
||||
modules m
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
-- name: CreateLmsPractice :one
|
||||
INSERT INTO lms_practices (
|
||||
course_id, module_id, lesson_id,
|
||||
title, story_description, story_image, persona_id, question_set_id, quick_tips
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||
title, story_description, story_image, persona_id, question_set_id, quick_tips, publish_status
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
RETURNING *;
|
||||
|
||||
-- name: GetLmsPracticeByID :one
|
||||
|
|
@ -10,6 +10,13 @@ SELECT *
|
|||
FROM lms_practices
|
||||
WHERE id = $1;
|
||||
|
||||
-- name: GetLmsPracticeByQuestionSetID :one
|
||||
SELECT *
|
||||
FROM lms_practices
|
||||
WHERE question_set_id = $1
|
||||
ORDER BY id DESC
|
||||
LIMIT 1;
|
||||
|
||||
-- name: ListLmsPracticesByCourseID :many
|
||||
SELECT
|
||||
COUNT(*) OVER () AS total_count,
|
||||
|
|
@ -23,10 +30,15 @@ SELECT
|
|||
p.persona_id,
|
||||
p.question_set_id,
|
||||
p.quick_tips,
|
||||
p.publish_status,
|
||||
p.created_at,
|
||||
p.updated_at
|
||||
FROM lms_practices p
|
||||
WHERE p.course_id = $1
|
||||
AND (
|
||||
sqlc.arg('published_only')::boolean = FALSE
|
||||
OR p.publish_status = 'PUBLISHED'::TEXT
|
||||
)
|
||||
ORDER BY p.created_at DESC
|
||||
LIMIT $2 OFFSET $3;
|
||||
|
||||
|
|
@ -43,10 +55,15 @@ SELECT
|
|||
p.persona_id,
|
||||
p.question_set_id,
|
||||
p.quick_tips,
|
||||
p.publish_status,
|
||||
p.created_at,
|
||||
p.updated_at
|
||||
FROM lms_practices p
|
||||
WHERE p.module_id = $1
|
||||
AND (
|
||||
sqlc.arg('published_only')::boolean = FALSE
|
||||
OR p.publish_status = 'PUBLISHED'::TEXT
|
||||
)
|
||||
ORDER BY p.created_at DESC
|
||||
LIMIT $2 OFFSET $3;
|
||||
|
||||
|
|
@ -63,10 +80,15 @@ SELECT
|
|||
p.persona_id,
|
||||
p.question_set_id,
|
||||
p.quick_tips,
|
||||
p.publish_status,
|
||||
p.created_at,
|
||||
p.updated_at
|
||||
FROM lms_practices p
|
||||
WHERE p.lesson_id = $1
|
||||
AND (
|
||||
sqlc.arg('published_only')::boolean = FALSE
|
||||
OR p.publish_status = 'PUBLISHED'::TEXT
|
||||
)
|
||||
ORDER BY p.created_at DESC
|
||||
LIMIT $2 OFFSET $3;
|
||||
|
||||
|
|
@ -79,6 +101,7 @@ SET
|
|||
persona_id = COALESCE(sqlc.narg('persona_id')::bigint, persona_id),
|
||||
question_set_id = COALESCE(sqlc.narg('question_set_id')::bigint, question_set_id),
|
||||
quick_tips = COALESCE(sqlc.narg('quick_tips')::text, quick_tips),
|
||||
publish_status = COALESCE(sqlc.narg('publish_status')::varchar, publish_status),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = sqlc.arg('id')
|
||||
RETURNING *;
|
||||
|
|
|
|||
|
|
@ -257,7 +257,8 @@ FROM
|
|||
WHERE
|
||||
lp.module_id = $1
|
||||
AND qs.set_type = 'PRACTICE'
|
||||
AND qs.status = 'PUBLISHED';
|
||||
AND qs.status = 'PUBLISHED'
|
||||
AND lp.publish_status = 'PUBLISHED';
|
||||
|
||||
-- name: CountUserCompletedPublishedPracticesInModule :one
|
||||
SELECT
|
||||
|
|
@ -271,7 +272,8 @@ WHERE
|
|||
AND upp.user_id = $2
|
||||
AND upp.completed_at IS NOT NULL
|
||||
AND qs.set_type = 'PRACTICE'
|
||||
AND qs.status = 'PUBLISHED';
|
||||
AND qs.status = 'PUBLISHED'
|
||||
AND lp.publish_status = 'PUBLISHED';
|
||||
|
||||
-- name: CountPublishedPracticesInCourse :one
|
||||
SELECT
|
||||
|
|
@ -282,7 +284,8 @@ FROM
|
|||
WHERE
|
||||
lp.course_id = $1
|
||||
AND qs.set_type = 'PRACTICE'
|
||||
AND qs.status = 'PUBLISHED';
|
||||
AND qs.status = 'PUBLISHED'
|
||||
AND lp.publish_status = 'PUBLISHED';
|
||||
|
||||
-- name: CountUserCompletedPublishedPracticesInCourse :one
|
||||
SELECT
|
||||
|
|
@ -308,7 +311,8 @@ FROM
|
|||
WHERE
|
||||
c.program_id = $1
|
||||
AND qs.set_type = 'PRACTICE'
|
||||
AND qs.status = 'PUBLISHED';
|
||||
AND qs.status = 'PUBLISHED'
|
||||
AND lp.publish_status = 'PUBLISHED';
|
||||
|
||||
-- name: CountUserCompletedPublishedPracticesInProgram :one
|
||||
SELECT
|
||||
|
|
@ -323,7 +327,8 @@ WHERE
|
|||
AND upp.user_id = $2
|
||||
AND upp.completed_at IS NOT NULL
|
||||
AND qs.set_type = 'PRACTICE'
|
||||
AND qs.status = 'PUBLISHED';
|
||||
AND qs.status = 'PUBLISHED'
|
||||
AND lp.publish_status = 'PUBLISHED';
|
||||
|
||||
-- name: GetPracticeScopeByQuestionSetID :one
|
||||
SELECT
|
||||
|
|
|
|||
|
|
@ -100,6 +100,7 @@ FROM (
|
|||
user_practice_progress upp
|
||||
INNER JOIN lms_practices lp ON lp.question_set_id = upp.question_set_id
|
||||
AND lp.lesson_id IS NOT NULL
|
||||
AND lp.publish_status = 'PUBLISHED'
|
||||
INNER JOIN question_sets qs ON qs.id = upp.question_set_id
|
||||
AND qs.set_type = 'PRACTICE'
|
||||
AND qs.status = 'PUBLISHED'
|
||||
|
|
@ -131,6 +132,7 @@ FROM (
|
|||
INNER JOIN lms_practices lp ON lp.question_set_id = upp.question_set_id
|
||||
AND lp.module_id IS NOT NULL
|
||||
AND lp.lesson_id IS NULL
|
||||
AND lp.publish_status = 'PUBLISHED'
|
||||
INNER JOIN question_sets qs ON qs.id = upp.question_set_id
|
||||
AND qs.set_type = 'PRACTICE'
|
||||
AND qs.status = 'PUBLISHED'
|
||||
|
|
@ -162,6 +164,7 @@ FROM (
|
|||
AND lp.course_id IS NOT NULL
|
||||
AND lp.module_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
|
||||
AND qs.set_type = 'PRACTICE'
|
||||
AND qs.status = 'PUBLISHED'
|
||||
|
|
|
|||
|
|
@ -415,6 +415,8 @@ This creates the practice record scoped to lesson.
|
|||
|
||||
### 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
|
||||
{
|
||||
"parent_kind": "LESSON",
|
||||
|
|
@ -423,7 +425,8 @@ This creates the practice record scoped to lesson.
|
|||
"story_description": "A short two-speaker scenario.",
|
||||
"story_image": "https://cdn.example.com/images/story.webp",
|
||||
"question_set_id": 55,
|
||||
"quick_tips": "Listen carefully before answering."
|
||||
"quick_tips": "Listen carefully before answering.",
|
||||
"publish_status": "DRAFT"
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -19,9 +19,10 @@ INSERT INTO exam_prep.lesson_practices (
|
|||
story_image,
|
||||
persona_id,
|
||||
question_set_id,
|
||||
quick_tips
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING id, unit_module_lesson_id, title, story_description, story_image, persona_id, question_set_id, quick_tips, created_at, updated_at
|
||||
quick_tips,
|
||||
publish_status
|
||||
) 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 {
|
||||
|
|
@ -32,6 +33,7 @@ type ExamPrepCreateLessonPracticeParams struct {
|
|||
PersonaID pgtype.Int8 `json:"persona_id"`
|
||||
QuestionSetID int64 `json:"question_set_id"`
|
||||
QuickTips pgtype.Text `json:"quick_tips"`
|
||||
PublishStatus string `json:"publish_status"`
|
||||
}
|
||||
|
||||
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.QuestionSetID,
|
||||
arg.QuickTips,
|
||||
arg.PublishStatus,
|
||||
)
|
||||
var i ExamPrepLessonPractice
|
||||
err := row.Scan(
|
||||
|
|
@ -56,6 +59,7 @@ func (q *Queries) ExamPrepCreateLessonPractice(ctx context.Context, arg ExamPrep
|
|||
&i.QuickTips,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.PublishStatus,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -71,7 +75,7 @@ func (q *Queries) ExamPrepDeleteLessonPractice(ctx context.Context, id int64) er
|
|||
}
|
||||
|
||||
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
|
||||
WHERE id = $1
|
||||
`
|
||||
|
|
@ -90,6 +94,34 @@ func (q *Queries) ExamPrepGetLessonPracticeByID(ctx context.Context, id int64) (
|
|||
&i.QuickTips,
|
||||
&i.CreatedAt,
|
||||
&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
|
||||
}
|
||||
|
|
@ -105,10 +137,15 @@ SELECT
|
|||
p.persona_id,
|
||||
p.question_set_id,
|
||||
p.quick_tips,
|
||||
p.publish_status,
|
||||
p.created_at,
|
||||
p.updated_at
|
||||
FROM exam_prep.lesson_practices p
|
||||
WHERE p.unit_module_lesson_id = $1
|
||||
AND (
|
||||
$4::boolean = FALSE
|
||||
OR p.publish_status = 'PUBLISHED'::TEXT
|
||||
)
|
||||
ORDER BY p.created_at DESC
|
||||
LIMIT $2
|
||||
OFFSET $3
|
||||
|
|
@ -118,6 +155,7 @@ type ExamPrepListLessonPracticesByLessonIDParams struct {
|
|||
UnitModuleLessonID int64 `json:"unit_module_lesson_id"`
|
||||
Limit int32 `json:"limit"`
|
||||
Offset int32 `json:"offset"`
|
||||
PublishedOnly bool `json:"published_only"`
|
||||
}
|
||||
|
||||
type ExamPrepListLessonPracticesByLessonIDRow struct {
|
||||
|
|
@ -130,12 +168,18 @@ type ExamPrepListLessonPracticesByLessonIDRow struct {
|
|||
PersonaID pgtype.Int8 `json:"persona_id"`
|
||||
QuestionSetID int64 `json:"question_set_id"`
|
||||
QuickTips pgtype.Text `json:"quick_tips"`
|
||||
PublishStatus string `json:"publish_status"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -153,6 +197,7 @@ func (q *Queries) ExamPrepListLessonPracticesByLessonID(ctx context.Context, arg
|
|||
&i.PersonaID,
|
||||
&i.QuestionSetID,
|
||||
&i.QuickTips,
|
||||
&i.PublishStatus,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
|
|
@ -175,9 +220,10 @@ SET
|
|||
persona_id = coalesce($4::bigint, persona_id),
|
||||
question_set_id = coalesce($5::bigint, question_set_id),
|
||||
quick_tips = coalesce($6::text, quick_tips),
|
||||
publish_status = coalesce($7::varchar, publish_status),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $7
|
||||
RETURNING id, unit_module_lesson_id, title, story_description, story_image, persona_id, question_set_id, quick_tips, created_at, updated_at
|
||||
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, publish_status
|
||||
`
|
||||
|
||||
type ExamPrepUpdateLessonPracticeParams struct {
|
||||
|
|
@ -187,6 +233,7 @@ type ExamPrepUpdateLessonPracticeParams struct {
|
|||
PersonaID pgtype.Int8 `json:"persona_id"`
|
||||
QuestionSetID pgtype.Int8 `json:"question_set_id"`
|
||||
QuickTips pgtype.Text `json:"quick_tips"`
|
||||
PublishStatus pgtype.Text `json:"publish_status"`
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
|
||||
|
|
@ -198,6 +245,7 @@ func (q *Queries) ExamPrepUpdateLessonPractice(ctx context.Context, arg ExamPrep
|
|||
arg.PersonaID,
|
||||
arg.QuestionSetID,
|
||||
arg.QuickTips,
|
||||
arg.PublishStatus,
|
||||
arg.ID,
|
||||
)
|
||||
var i ExamPrepLessonPractice
|
||||
|
|
@ -212,6 +260,7 @@ func (q *Queries) ExamPrepUpdateLessonPractice(ctx context.Context, arg ExamPrep
|
|||
&i.QuickTips,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.PublishStatus,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -97,6 +97,7 @@ UNION ALL
|
|||
user_practice_progress upp
|
||||
INNER JOIN lms_practices lp ON lp.question_set_id = upp.question_set_id
|
||||
AND lp.lesson_id IS NOT NULL
|
||||
AND lp.publish_status = 'PUBLISHED'
|
||||
INNER JOIN question_sets qs ON qs.id = upp.question_set_id
|
||||
AND qs.set_type = 'PRACTICE'
|
||||
AND qs.status = 'PUBLISHED'
|
||||
|
|
@ -143,6 +144,7 @@ UNION ALL
|
|||
INNER JOIN lms_practices lp ON lp.question_set_id = upp.question_set_id
|
||||
AND lp.module_id IS NOT NULL
|
||||
AND lp.lesson_id IS NULL
|
||||
AND lp.publish_status = 'PUBLISHED'
|
||||
INNER JOIN question_sets qs ON qs.id = upp.question_set_id
|
||||
AND qs.set_type = 'PRACTICE'
|
||||
AND qs.status = 'PUBLISHED'
|
||||
|
|
@ -187,6 +189,7 @@ UNION ALL
|
|||
AND lp.course_id IS NOT NULL
|
||||
AND lp.module_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
|
||||
AND qs.set_type = 'PRACTICE'
|
||||
AND qs.status = 'PUBLISHED'
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ SELECT
|
|||
WHERE p.course_id = c.id
|
||||
AND p.module_id IS NULL
|
||||
AND p.lesson_id IS NULL
|
||||
AND p.publish_status = 'PUBLISHED'
|
||||
) AS has_practice
|
||||
FROM courses
|
||||
c
|
||||
|
|
@ -180,13 +181,15 @@ SELECT
|
|||
WHERE
|
||||
p.course_id = c.id
|
||||
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 (
|
||||
SELECT 1
|
||||
FROM lms_practices p
|
||||
WHERE p.course_id = c.id
|
||||
AND p.module_id IS NULL
|
||||
AND p.lesson_id IS NULL
|
||||
AND p.publish_status = 'PUBLISHED'
|
||||
) AS has_practice
|
||||
FROM
|
||||
courses c
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@ SELECT
|
|||
SELECT 1
|
||||
FROM lms_practices p
|
||||
WHERE p.lesson_id = l.id
|
||||
AND p.publish_status = 'PUBLISHED'
|
||||
) AS has_practice
|
||||
FROM lessons
|
||||
l
|
||||
|
|
@ -130,6 +131,7 @@ SELECT
|
|||
SELECT 1
|
||||
FROM lms_practices p
|
||||
WHERE p.lesson_id = l.id
|
||||
AND p.publish_status = 'PUBLISHED'
|
||||
) AS has_practice
|
||||
FROM
|
||||
lessons l
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ SELECT
|
|||
FROM lms_practices p
|
||||
WHERE p.module_id = m.id
|
||||
AND p.lesson_id IS NULL
|
||||
AND p.publish_status = 'PUBLISHED'
|
||||
) AS has_practice
|
||||
FROM modules
|
||||
m
|
||||
|
|
@ -163,6 +164,7 @@ SELECT
|
|||
FROM lms_practices p
|
||||
WHERE p.module_id = m.id
|
||||
AND p.lesson_id IS NULL
|
||||
AND p.publish_status = 'PUBLISHED'
|
||||
) AS has_practice
|
||||
FROM
|
||||
modules m
|
||||
|
|
|
|||
|
|
@ -14,9 +14,9 @@ import (
|
|||
const CreateLmsPractice = `-- name: CreateLmsPractice :one
|
||||
INSERT INTO lms_practices (
|
||||
course_id, module_id, lesson_id,
|
||||
title, story_description, story_image, persona_id, question_set_id, quick_tips
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||
RETURNING id, course_id, module_id, lesson_id, title, story_description, story_image, persona_id, question_set_id, quick_tips, created_at, updated_at
|
||||
title, story_description, story_image, persona_id, question_set_id, quick_tips, publish_status
|
||||
) 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, publish_status
|
||||
`
|
||||
|
||||
type CreateLmsPracticeParams struct {
|
||||
|
|
@ -29,6 +29,7 @@ type CreateLmsPracticeParams struct {
|
|||
PersonaID pgtype.Int8 `json:"persona_id"`
|
||||
QuestionSetID int64 `json:"question_set_id"`
|
||||
QuickTips pgtype.Text `json:"quick_tips"`
|
||||
PublishStatus string `json:"publish_status"`
|
||||
}
|
||||
|
||||
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.QuestionSetID,
|
||||
arg.QuickTips,
|
||||
arg.PublishStatus,
|
||||
)
|
||||
var i LmsPractice
|
||||
err := row.Scan(
|
||||
|
|
@ -57,6 +59,7 @@ func (q *Queries) CreateLmsPractice(ctx context.Context, arg CreateLmsPracticePa
|
|||
&i.QuickTips,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.PublishStatus,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -72,7 +75,7 @@ func (q *Queries) DeleteLmsPractice(ctx context.Context, id int64) error {
|
|||
}
|
||||
|
||||
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
|
||||
WHERE id = $1
|
||||
`
|
||||
|
|
@ -93,6 +96,36 @@ func (q *Queries) GetLmsPracticeByID(ctx context.Context, id int64) (LmsPractice
|
|||
&i.QuickTips,
|
||||
&i.CreatedAt,
|
||||
&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
|
||||
}
|
||||
|
|
@ -110,18 +143,24 @@ SELECT
|
|||
p.persona_id,
|
||||
p.question_set_id,
|
||||
p.quick_tips,
|
||||
p.publish_status,
|
||||
p.created_at,
|
||||
p.updated_at
|
||||
FROM lms_practices p
|
||||
WHERE p.course_id = $1
|
||||
AND (
|
||||
$4::boolean = FALSE
|
||||
OR p.publish_status = 'PUBLISHED'::TEXT
|
||||
)
|
||||
ORDER BY p.created_at DESC
|
||||
LIMIT $2 OFFSET $3
|
||||
`
|
||||
|
||||
type ListLmsPracticesByCourseIDParams struct {
|
||||
CourseID pgtype.Int8 `json:"course_id"`
|
||||
Limit int32 `json:"limit"`
|
||||
Offset int32 `json:"offset"`
|
||||
CourseID pgtype.Int8 `json:"course_id"`
|
||||
Limit int32 `json:"limit"`
|
||||
Offset int32 `json:"offset"`
|
||||
PublishedOnly bool `json:"published_only"`
|
||||
}
|
||||
|
||||
type ListLmsPracticesByCourseIDRow struct {
|
||||
|
|
@ -136,12 +175,18 @@ type ListLmsPracticesByCourseIDRow struct {
|
|||
PersonaID pgtype.Int8 `json:"persona_id"`
|
||||
QuestionSetID int64 `json:"question_set_id"`
|
||||
QuickTips pgtype.Text `json:"quick_tips"`
|
||||
PublishStatus string `json:"publish_status"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -161,6 +206,7 @@ func (q *Queries) ListLmsPracticesByCourseID(ctx context.Context, arg ListLmsPra
|
|||
&i.PersonaID,
|
||||
&i.QuestionSetID,
|
||||
&i.QuickTips,
|
||||
&i.PublishStatus,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
|
|
@ -187,18 +233,24 @@ SELECT
|
|||
p.persona_id,
|
||||
p.question_set_id,
|
||||
p.quick_tips,
|
||||
p.publish_status,
|
||||
p.created_at,
|
||||
p.updated_at
|
||||
FROM lms_practices p
|
||||
WHERE p.lesson_id = $1
|
||||
AND (
|
||||
$4::boolean = FALSE
|
||||
OR p.publish_status = 'PUBLISHED'::TEXT
|
||||
)
|
||||
ORDER BY p.created_at DESC
|
||||
LIMIT $2 OFFSET $3
|
||||
`
|
||||
|
||||
type ListLmsPracticesByLessonIDParams struct {
|
||||
LessonID pgtype.Int8 `json:"lesson_id"`
|
||||
Limit int32 `json:"limit"`
|
||||
Offset int32 `json:"offset"`
|
||||
LessonID pgtype.Int8 `json:"lesson_id"`
|
||||
Limit int32 `json:"limit"`
|
||||
Offset int32 `json:"offset"`
|
||||
PublishedOnly bool `json:"published_only"`
|
||||
}
|
||||
|
||||
type ListLmsPracticesByLessonIDRow struct {
|
||||
|
|
@ -213,12 +265,18 @@ type ListLmsPracticesByLessonIDRow struct {
|
|||
PersonaID pgtype.Int8 `json:"persona_id"`
|
||||
QuestionSetID int64 `json:"question_set_id"`
|
||||
QuickTips pgtype.Text `json:"quick_tips"`
|
||||
PublishStatus string `json:"publish_status"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -238,6 +296,7 @@ func (q *Queries) ListLmsPracticesByLessonID(ctx context.Context, arg ListLmsPra
|
|||
&i.PersonaID,
|
||||
&i.QuestionSetID,
|
||||
&i.QuickTips,
|
||||
&i.PublishStatus,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
|
|
@ -264,18 +323,24 @@ SELECT
|
|||
p.persona_id,
|
||||
p.question_set_id,
|
||||
p.quick_tips,
|
||||
p.publish_status,
|
||||
p.created_at,
|
||||
p.updated_at
|
||||
FROM lms_practices p
|
||||
WHERE p.module_id = $1
|
||||
AND (
|
||||
$4::boolean = FALSE
|
||||
OR p.publish_status = 'PUBLISHED'::TEXT
|
||||
)
|
||||
ORDER BY p.created_at DESC
|
||||
LIMIT $2 OFFSET $3
|
||||
`
|
||||
|
||||
type ListLmsPracticesByModuleIDParams struct {
|
||||
ModuleID pgtype.Int8 `json:"module_id"`
|
||||
Limit int32 `json:"limit"`
|
||||
Offset int32 `json:"offset"`
|
||||
ModuleID pgtype.Int8 `json:"module_id"`
|
||||
Limit int32 `json:"limit"`
|
||||
Offset int32 `json:"offset"`
|
||||
PublishedOnly bool `json:"published_only"`
|
||||
}
|
||||
|
||||
type ListLmsPracticesByModuleIDRow struct {
|
||||
|
|
@ -290,12 +355,18 @@ type ListLmsPracticesByModuleIDRow struct {
|
|||
PersonaID pgtype.Int8 `json:"persona_id"`
|
||||
QuestionSetID int64 `json:"question_set_id"`
|
||||
QuickTips pgtype.Text `json:"quick_tips"`
|
||||
PublishStatus string `json:"publish_status"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -315,6 +386,7 @@ func (q *Queries) ListLmsPracticesByModuleID(ctx context.Context, arg ListLmsPra
|
|||
&i.PersonaID,
|
||||
&i.QuestionSetID,
|
||||
&i.QuickTips,
|
||||
&i.PublishStatus,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
|
|
@ -337,9 +409,10 @@ SET
|
|||
persona_id = COALESCE($4::bigint, persona_id),
|
||||
question_set_id = COALESCE($5::bigint, question_set_id),
|
||||
quick_tips = COALESCE($6::text, quick_tips),
|
||||
publish_status = COALESCE($7::varchar, publish_status),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $7
|
||||
RETURNING id, course_id, module_id, lesson_id, title, story_description, story_image, persona_id, question_set_id, quick_tips, created_at, updated_at
|
||||
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, publish_status
|
||||
`
|
||||
|
||||
type UpdateLmsPracticeParams struct {
|
||||
|
|
@ -349,6 +422,7 @@ type UpdateLmsPracticeParams struct {
|
|||
PersonaID pgtype.Int8 `json:"persona_id"`
|
||||
QuestionSetID pgtype.Int8 `json:"question_set_id"`
|
||||
QuickTips pgtype.Text `json:"quick_tips"`
|
||||
PublishStatus pgtype.Text `json:"publish_status"`
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
|
||||
|
|
@ -360,6 +434,7 @@ func (q *Queries) UpdateLmsPractice(ctx context.Context, arg UpdateLmsPracticePa
|
|||
arg.PersonaID,
|
||||
arg.QuestionSetID,
|
||||
arg.QuickTips,
|
||||
arg.PublishStatus,
|
||||
arg.ID,
|
||||
)
|
||||
var i LmsPractice
|
||||
|
|
@ -376,6 +451,7 @@ func (q *Queries) UpdateLmsPractice(ctx context.Context, arg UpdateLmsPracticePa
|
|||
&i.QuickTips,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.PublishStatus,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -106,6 +106,7 @@ WHERE
|
|||
lp.course_id = $1
|
||||
AND qs.set_type = 'PRACTICE'
|
||||
AND qs.status = 'PUBLISHED'
|
||||
AND lp.publish_status = 'PUBLISHED'
|
||||
`
|
||||
|
||||
func (q *Queries) CountPublishedPracticesInCourse(ctx context.Context, courseID pgtype.Int8) (int32, error) {
|
||||
|
|
@ -125,6 +126,7 @@ WHERE
|
|||
lp.module_id = $1
|
||||
AND qs.set_type = 'PRACTICE'
|
||||
AND qs.status = 'PUBLISHED'
|
||||
AND lp.publish_status = 'PUBLISHED'
|
||||
`
|
||||
|
||||
// Published practices in a module (module-level and lesson-level practices should carry module_id).
|
||||
|
|
@ -146,6 +148,7 @@ WHERE
|
|||
c.program_id = $1
|
||||
AND qs.set_type = 'PRACTICE'
|
||||
AND qs.status = 'PUBLISHED'
|
||||
AND lp.publish_status = 'PUBLISHED'
|
||||
`
|
||||
|
||||
func (q *Queries) CountPublishedPracticesInProgram(ctx context.Context, programID int64) (int32, error) {
|
||||
|
|
@ -313,6 +316,7 @@ WHERE
|
|||
AND upp.completed_at IS NOT NULL
|
||||
AND qs.set_type = 'PRACTICE'
|
||||
AND qs.status = 'PUBLISHED'
|
||||
AND lp.publish_status = 'PUBLISHED'
|
||||
`
|
||||
|
||||
type CountUserCompletedPublishedPracticesInModuleParams struct {
|
||||
|
|
@ -341,6 +345,7 @@ WHERE
|
|||
AND upp.completed_at IS NOT NULL
|
||||
AND qs.set_type = 'PRACTICE'
|
||||
AND qs.status = 'PUBLISHED'
|
||||
AND lp.publish_status = 'PUBLISHED'
|
||||
`
|
||||
|
||||
type CountUserCompletedPublishedPracticesInProgramParams struct {
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ type ExamPrepLessonPractice struct {
|
|||
QuickTips pgtype.Text `json:"quick_tips"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
PublishStatus string `json:"publish_status"`
|
||||
}
|
||||
|
||||
type ExamPrepUnit struct {
|
||||
|
|
@ -149,6 +150,7 @@ type LmsPractice struct {
|
|||
QuickTips pgtype.Text `json:"quick_tips"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
PublishStatus string `json:"publish_status"`
|
||||
}
|
||||
|
||||
type LmsUserCourseProgress struct {
|
||||
|
|
|
|||
|
|
@ -223,6 +223,7 @@ FROM (
|
|||
user_practice_progress upp
|
||||
INNER JOIN lms_practices lp ON lp.question_set_id = upp.question_set_id
|
||||
AND lp.lesson_id IS NOT NULL
|
||||
AND lp.publish_status = 'PUBLISHED'
|
||||
INNER JOIN question_sets qs ON qs.id = upp.question_set_id
|
||||
AND qs.set_type = 'PRACTICE'
|
||||
AND qs.status = 'PUBLISHED'
|
||||
|
|
@ -254,6 +255,7 @@ FROM (
|
|||
INNER JOIN lms_practices lp ON lp.question_set_id = upp.question_set_id
|
||||
AND lp.module_id IS NOT NULL
|
||||
AND lp.lesson_id IS NULL
|
||||
AND lp.publish_status = 'PUBLISHED'
|
||||
INNER JOIN question_sets qs ON qs.id = upp.question_set_id
|
||||
AND qs.set_type = 'PRACTICE'
|
||||
AND qs.status = 'PUBLISHED'
|
||||
|
|
@ -285,6 +287,7 @@ FROM (
|
|||
AND lp.course_id IS NOT NULL
|
||||
AND lp.module_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
|
||||
AND qs.set_type = 'PRACTICE'
|
||||
AND qs.status = 'PUBLISHED'
|
||||
|
|
|
|||
|
|
@ -4,16 +4,22 @@ import "time"
|
|||
|
||||
// ExamPrepPractice is question-set content tied to an exam-prep lesson; uses shared question_sets / questions.
|
||||
type ExamPrepPractice struct {
|
||||
ID int64 `json:"id"`
|
||||
LessonID int64 `json:"lesson_id"` // exam_prep.unit_module_lessons.id
|
||||
Title string `json:"title"`
|
||||
StoryDescription *string `json:"story_description,omitempty"`
|
||||
StoryImage *string `json:"story_image,omitempty"`
|
||||
PersonaID *int64 `json:"persona_id,omitempty"`
|
||||
QuestionSetID int64 `json:"question_set_id"`
|
||||
QuickTips *string `json:"quick_tips,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt *time.Time `json:"updated_at,omitempty"`
|
||||
ID int64 `json:"id"`
|
||||
LessonID int64 `json:"lesson_id"` // exam_prep.unit_module_lessons.id
|
||||
Title string `json:"title"`
|
||||
StoryDescription *string `json:"story_description,omitempty"`
|
||||
StoryImage *string `json:"story_image,omitempty"`
|
||||
PersonaID *int64 `json:"persona_id,omitempty"`
|
||||
QuestionSetID int64 `json:"question_set_id"`
|
||||
PublishStatus PracticePublishStatus `json:"publish_status"`
|
||||
QuickTips *string `json:"quick_tips,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
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).
|
||||
|
|
@ -24,6 +30,7 @@ type CreateExamPrepPracticeInput struct {
|
|||
PersonaID *int64 `json:"persona_id,omitempty"`
|
||||
QuestionSetID int64 `json:"question_set_id" validate:"required,gt=0"`
|
||||
QuickTips *string `json:"quick_tips,omitempty"`
|
||||
PublishStatus string `json:"publish_status,omitempty" validate:"omitempty,oneof=DRAFT draft PUBLISHED published"`
|
||||
}
|
||||
|
||||
type UpdateExamPrepPracticeInput struct {
|
||||
|
|
@ -33,4 +40,5 @@ type UpdateExamPrepPracticeInput struct {
|
|||
PersonaID *int64 `json:"persona_id,omitempty"`
|
||||
QuestionSetID *int64 `json:"question_set_id,omitempty"`
|
||||
QuickTips *string `json:"quick_tips,omitempty"`
|
||||
PublishStatus *string `json:"publish_status,omitempty" validate:"omitempty,oneof=DRAFT draft PUBLISHED published"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
package domain
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ParentKind identifies which hierarchy entity owns a practice (exactly one).
|
||||
type ParentKind string
|
||||
|
|
@ -11,19 +14,50 @@ const (
|
|||
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.
|
||||
type Practice struct {
|
||||
ID int64 `json:"id"`
|
||||
ParentKind ParentKind `json:"parent_kind"`
|
||||
ParentID int64 `json:"parent_id"`
|
||||
Title string `json:"title"`
|
||||
StoryDescription *string `json:"story_description,omitempty"`
|
||||
StoryImage *string `json:"story_image,omitempty"`
|
||||
PersonaID *int64 `json:"persona_id,omitempty"`
|
||||
QuestionSetID int64 `json:"question_set_id"`
|
||||
QuickTips *string `json:"quick_tips,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt *time.Time `json:"updated_at,omitempty"`
|
||||
ID int64 `json:"id"`
|
||||
ParentKind ParentKind `json:"parent_kind"`
|
||||
ParentID int64 `json:"parent_id"`
|
||||
Title string `json:"title"`
|
||||
StoryDescription *string `json:"story_description,omitempty"`
|
||||
StoryImage *string `json:"story_image,omitempty"`
|
||||
PersonaID *int64 `json:"persona_id,omitempty"`
|
||||
QuestionSetID int64 `json:"question_set_id"`
|
||||
PublishStatus PracticePublishStatus `json:"publish_status"`
|
||||
QuickTips *string `json:"quick_tips,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
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 {
|
||||
|
|
@ -35,6 +69,8 @@ type CreatePracticeInput struct {
|
|||
PersonaID *int64 `json:"persona_id,omitempty"`
|
||||
QuestionSetID int64 `json:"question_set_id" validate:"required,gt=0"`
|
||||
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 {
|
||||
|
|
@ -44,4 +80,5 @@ type UpdatePracticeInput struct {
|
|||
PersonaID *int64 `json:"persona_id,omitempty"`
|
||||
QuestionSetID *int64 `json:"question_set_id,omitempty"`
|
||||
QuickTips *string `json:"quick_tips,omitempty"`
|
||||
PublishStatus *string `json:"publish_status,omitempty" validate:"omitempty,oneof=DRAFT draft PUBLISHED published"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ import (
|
|||
type ExamPrepPracticeStore interface {
|
||||
CreateExamPrepLessonPractice(ctx context.Context, lessonID int64, in domain.CreateExamPrepPracticeInput) (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)
|
||||
DeleteExamPrepLessonPractice(ctx context.Context, id int64) error
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,9 +23,11 @@ type LmsPracticeStore interface {
|
|||
courseID, moduleID, lessonID *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)
|
||||
ListLmsPracticesByModuleID(ctx context.Context, moduleID int64, limit, offset int32) ([]domain.Practice, int64, error)
|
||||
ListLmsPracticesByLessonID(ctx context.Context, lessonID int64, limit, offset int32) ([]domain.Practice, int64, error)
|
||||
// TryGetLmsPracticeByQuestionSetID returns false when no LMS practice row references the question set.
|
||||
TryGetLmsPracticeByQuestionSetID(ctx context.Context, questionSetID int64) (domain.Practice, bool, 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)
|
||||
DeleteLmsPractice(ctx context.Context, id int64) error
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ func examPrepPracticeFromListRow(r dbgen.ExamPrepListLessonPracticesByLessonIDRo
|
|||
PersonaID: r.PersonaID,
|
||||
QuestionSetID: r.QuestionSetID,
|
||||
QuickTips: r.QuickTips,
|
||||
PublishStatus: r.PublishStatus,
|
||||
CreatedAt: r.CreatedAt,
|
||||
UpdatedAt: r.UpdatedAt,
|
||||
})
|
||||
|
|
@ -32,6 +33,7 @@ func examPrepPracticeToDomain(p dbgen.ExamPrepLessonPractice) domain.ExamPrepPra
|
|||
LessonID: p.UnitModuleLessonID,
|
||||
Title: p.Title,
|
||||
QuestionSetID: p.QuestionSetID,
|
||||
PublishStatus: domain.PracticePublishStatusFromDB(p.PublishStatus),
|
||||
}
|
||||
out.StoryDescription = fromPgText(p.StoryDescription)
|
||||
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) {
|
||||
ps := domain.ParsePracticePublishStatusInput(in.PublishStatus)
|
||||
p, err := s.queries.ExamPrepCreateLessonPractice(ctx, dbgen.ExamPrepCreateLessonPracticeParams{
|
||||
UnitModuleLessonID: lessonID,
|
||||
Title: in.Title,
|
||||
|
|
@ -54,6 +57,7 @@ func (s *Store) CreateExamPrepLessonPractice(ctx context.Context, lessonID int64
|
|||
PersonaID: int64PtrToPg8(in.PersonaID),
|
||||
QuestionSetID: in.QuestionSetID,
|
||||
QuickTips: toPgText(in.QuickTips),
|
||||
PublishStatus: string(ps),
|
||||
})
|
||||
if err != nil {
|
||||
return domain.ExamPrepPractice{}, err
|
||||
|
|
@ -72,9 +76,22 @@ func (s *Store) GetExamPrepLessonPracticeByID(ctx context.Context, id int64) (do
|
|||
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{
|
||||
UnitModuleLessonID: lessonID,
|
||||
PublishedOnly: publishedOnly,
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
})
|
||||
|
|
@ -111,6 +128,7 @@ func (s *Store) UpdateExamPrepLessonPractice(ctx context.Context, id int64, inpu
|
|||
PersonaID: optionalInt8UpdateID(input.PersonaID),
|
||||
QuestionSetID: qs,
|
||||
QuickTips: optionalTextUpdate(input.QuickTips),
|
||||
PublishStatus: optionalPublishStatusUpdate(input.PublishStatus),
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
|
|
|
|||
|
|
@ -26,11 +26,19 @@ func fromPgInt8ID(c pgtype.Int8) *int64 {
|
|||
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 {
|
||||
out := domain.Practice{
|
||||
ID: p.ID,
|
||||
Title: p.Title,
|
||||
QuestionSetID: p.QuestionSetID,
|
||||
PublishStatus: domain.PracticePublishStatusFromDB(p.PublishStatus),
|
||||
}
|
||||
if p.CourseID.Valid {
|
||||
out.ParentKind = domain.ParentKindCourse
|
||||
|
|
@ -55,7 +63,9 @@ func lmsPracticeToDomain(p dbgen.LmsPractice) domain.Practice {
|
|||
}
|
||||
|
||||
func lmsFromListRow(
|
||||
id, qid int64, title string,
|
||||
id, qid int64,
|
||||
publishStatus string,
|
||||
title string,
|
||||
cid, mid, lid pgtype.Int8,
|
||||
sd, si, qt pgtype.Text, pid pgtype.Int8,
|
||||
ca, ua pgtype.Timestamptz,
|
||||
|
|
@ -71,6 +81,7 @@ func lmsFromListRow(
|
|||
PersonaID: pid,
|
||||
QuestionSetID: qid,
|
||||
QuickTips: qt,
|
||||
PublishStatus: publishStatus,
|
||||
CreatedAt: ca,
|
||||
UpdatedAt: ua,
|
||||
})
|
||||
|
|
@ -82,6 +93,7 @@ func (s *Store) CreateLmsPractice(
|
|||
in domain.CreatePracticeInput,
|
||||
courseID, moduleID, lessonID *int64,
|
||||
) (domain.Practice, error) {
|
||||
ps := domain.ParsePracticePublishStatusInput(in.PublishStatus)
|
||||
p, err := s.queries.CreateLmsPractice(ctx, dbgen.CreateLmsPracticeParams{
|
||||
CourseID: int64PtrToPg8(courseID),
|
||||
ModuleID: int64PtrToPg8(moduleID),
|
||||
|
|
@ -92,6 +104,7 @@ func (s *Store) CreateLmsPractice(
|
|||
PersonaID: int64PtrToPg8(in.PersonaID),
|
||||
QuestionSetID: in.QuestionSetID,
|
||||
QuickTips: toPgText(in.QuickTips),
|
||||
PublishStatus: string(ps),
|
||||
})
|
||||
if err != nil {
|
||||
return domain.Practice{}, err
|
||||
|
|
@ -110,11 +123,24 @@ func (s *Store) GetLmsPracticeByID(ctx context.Context, id int64) (domain.Practi
|
|||
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{
|
||||
CourseID: pgtype.Int8{Int64: courseID, Valid: true},
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
CourseID: pgtype.Int8{Int64: courseID, Valid: true},
|
||||
PublishedOnly: publishedOnly,
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
|
|
@ -129,7 +155,7 @@ func (s *Store) ListLmsPracticesByCourseID(ctx context.Context, courseID int64,
|
|||
total = r.TotalCount
|
||||
}
|
||||
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.StoryDescription, r.StoryImage, r.QuickTips,
|
||||
r.PersonaID, r.CreatedAt, r.UpdatedAt,
|
||||
|
|
@ -138,11 +164,12 @@ func (s *Store) ListLmsPracticesByCourseID(ctx context.Context, courseID int64,
|
|||
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{
|
||||
ModuleID: pgtype.Int8{Int64: moduleID, Valid: true},
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
ModuleID: pgtype.Int8{Int64: moduleID, Valid: true},
|
||||
PublishedOnly: publishedOnly,
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
|
|
@ -157,7 +184,7 @@ func (s *Store) ListLmsPracticesByModuleID(ctx context.Context, moduleID int64,
|
|||
total = r.TotalCount
|
||||
}
|
||||
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.StoryDescription, r.StoryImage, r.QuickTips,
|
||||
r.PersonaID, r.CreatedAt, r.UpdatedAt,
|
||||
|
|
@ -166,11 +193,12 @@ func (s *Store) ListLmsPracticesByModuleID(ctx context.Context, moduleID int64,
|
|||
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{
|
||||
LessonID: pgtype.Int8{Int64: lessonID, Valid: true},
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
LessonID: pgtype.Int8{Int64: lessonID, Valid: true},
|
||||
PublishedOnly: publishedOnly,
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
|
|
@ -185,7 +213,7 @@ func (s *Store) ListLmsPracticesByLessonID(ctx context.Context, lessonID int64,
|
|||
total = r.TotalCount
|
||||
}
|
||||
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.StoryDescription, r.StoryImage, r.QuickTips,
|
||||
r.PersonaID, r.CreatedAt, r.UpdatedAt,
|
||||
|
|
@ -194,13 +222,6 @@ func (s *Store) ListLmsPracticesByLessonID(ctx context.Context, lessonID int64,
|
|||
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) {
|
||||
var titleText pgtype.Text
|
||||
if input.Title != nil {
|
||||
|
|
@ -217,6 +238,7 @@ func (s *Store) UpdateLmsPractice(ctx context.Context, id int64, input domain.Up
|
|||
PersonaID: optionalInt8UpdateID(input.PersonaID),
|
||||
QuestionSetID: qs,
|
||||
QuickTips: optionalTextUpdate(input.QuickTips),
|
||||
PublishStatus: optionalPublishStatusUpdate(input.PublishStatus),
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package repository
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
dbgen "Yimaru-Backend/gen/db"
|
||||
"Yimaru-Backend/internal/domain"
|
||||
|
|
@ -117,6 +118,19 @@ func optionalTextUpdate(val *string) pgtype.Text {
|
|||
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 {
|
||||
if v == nil {
|
||||
return pgtype.Int4{Valid: false}
|
||||
|
|
|
|||
|
|
@ -358,7 +358,7 @@ func (s *Service) CreateExamPrepPractice(ctx context.Context, lessonID int64, in
|
|||
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 {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
|
@ -371,7 +371,7 @@ func (s *Service) ListExamPrepPracticesByLesson(ctx context.Context, lessonID in
|
|||
if 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) {
|
||||
|
|
@ -385,6 +385,10 @@ func (s *Service) GetExamPrepPracticeByID(ctx context.Context, id int64) (domain
|
|||
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) {
|
||||
p, err := s.store.UpdateExamPrepLessonPractice(ctx, id, input)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -115,6 +115,10 @@ func (s *Service) Create(ctx context.Context, in domain.CreatePracticeInput) (do
|
|||
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) {
|
||||
p, err := s.practices.GetLmsPracticeByID(ctx, id)
|
||||
if err != nil {
|
||||
|
|
@ -139,7 +143,7 @@ func clampPracticePage(limit, offset int32) (int32, int32) {
|
|||
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 errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, 0, courses.ErrCourseNotFound
|
||||
|
|
@ -147,10 +151,10 @@ func (s *Service) ListByCourse(ctx context.Context, courseID int64, limit, offse
|
|||
return nil, 0, err
|
||||
}
|
||||
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 errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, 0, modules.ErrModuleNotFound
|
||||
|
|
@ -158,10 +162,10 @@ func (s *Service) ListByModule(ctx context.Context, moduleID int64, limit, offse
|
|||
return nil, 0, err
|
||||
}
|
||||
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 errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, 0, lessons.ErrLessonNotFound
|
||||
|
|
@ -169,7 +173,7 @@ func (s *Service) ListByLesson(ctx context.Context, lessonID int64, limit, offse
|
|||
return nil, 0, err
|
||||
}
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -74,7 +74,8 @@ func (h *Handler) ListExamPrepPracticesByLesson(c *fiber.Ctx) error {
|
|||
}
|
||||
limit, _ := strconv.Atoi(c.Query("limit", "20"))
|
||||
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 errors.Is(err, examprep.ErrLessonNotFound) {
|
||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
||||
|
|
@ -126,6 +127,9 @@ func (h *Handler) GetExamPrepPracticeByID(c *fiber.Ctx) 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{
|
||||
Message: "Practice retrieved successfully",
|
||||
Data: p,
|
||||
|
|
|
|||
|
|
@ -76,7 +76,8 @@ func (h *Handler) ListPracticesByCourse(c *fiber.Ctx) error {
|
|||
}
|
||||
limit, _ := strconv.Atoi(c.Query("limit", "20"))
|
||||
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 errors.Is(err, courses.ErrCourseNotFound) {
|
||||
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"))
|
||||
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 errors.Is(err, modules.ErrModuleNotFound) {
|
||||
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"))
|
||||
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 errors.Is(err, lessons.ErrLessonNotFound) {
|
||||
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()})
|
||||
}
|
||||
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})
|
||||
}
|
||||
|
||||
|
|
|
|||
51
internal/web_server/handlers/practice_publish_gate.go
Normal file
51
internal/web_server/handlers/practice_publish_gate.go
Normal 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
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"Yimaru-Backend/internal/domain"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"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 {
|
||||
status := fiber.StatusForbidden
|
||||
if ferr, ok := err.(*fiber.Error); ok {
|
||||
|
|
@ -1552,6 +1567,11 @@ func (h *Handler) CompletePractice(c *fiber.Ctx) error {
|
|||
var set domain.QuestionSet
|
||||
var setErr error
|
||||
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)
|
||||
} else {
|
||||
// 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)) {
|
||||
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.
|
||||
if strings.EqualFold(set.Status, "PUBLISHED") {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user