From ffbb885d0693d3d29a4f039dbf67555345b744a4 Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Thu, 28 May 2026 00:11:48 -0700 Subject: [PATCH] Fix LMS practice visibility and completion publish checks. Require question_sets.status to be PUBLISHED for learner-visible practices and reject completion for non-published practice sets so learner progress reflects only publish-ready content. Co-authored-by: Cursor --- db/query/lms_practices.sql | 21 ++++++++++++++++++--- gen/db/lms_practices.sql.go | 21 ++++++++++++++++++--- internal/web_server/handlers/questions.go | 5 +++++ 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/db/query/lms_practices.sql b/db/query/lms_practices.sql index f3e48de..46c3167 100644 --- a/db/query/lms_practices.sql +++ b/db/query/lms_practices.sql @@ -34,10 +34,15 @@ SELECT p.created_at, p.updated_at FROM lms_practices p +INNER JOIN question_sets qs ON qs.id = p.question_set_id WHERE p.course_id = $1 AND ( sqlc.arg('published_only')::boolean = FALSE - OR p.publish_status = 'PUBLISHED'::TEXT + OR ( + p.publish_status = 'PUBLISHED'::TEXT + AND qs.set_type = 'PRACTICE' + AND qs.status = 'PUBLISHED' + ) ) ORDER BY p.created_at DESC LIMIT $2 OFFSET $3; @@ -59,10 +64,15 @@ SELECT p.created_at, p.updated_at FROM lms_practices p +INNER JOIN question_sets qs ON qs.id = p.question_set_id WHERE p.module_id = $1 AND ( sqlc.arg('published_only')::boolean = FALSE - OR p.publish_status = 'PUBLISHED'::TEXT + OR ( + p.publish_status = 'PUBLISHED'::TEXT + AND qs.set_type = 'PRACTICE' + AND qs.status = 'PUBLISHED' + ) ) ORDER BY p.created_at DESC LIMIT $2 OFFSET $3; @@ -84,10 +94,15 @@ SELECT p.created_at, p.updated_at FROM lms_practices p +INNER JOIN question_sets qs ON qs.id = p.question_set_id WHERE p.lesson_id = $1 AND ( sqlc.arg('published_only')::boolean = FALSE - OR p.publish_status = 'PUBLISHED'::TEXT + OR ( + p.publish_status = 'PUBLISHED'::TEXT + AND qs.set_type = 'PRACTICE' + AND qs.status = 'PUBLISHED' + ) ) ORDER BY p.created_at DESC LIMIT $2 OFFSET $3; diff --git a/gen/db/lms_practices.sql.go b/gen/db/lms_practices.sql.go index 8039241..0e868df 100644 --- a/gen/db/lms_practices.sql.go +++ b/gen/db/lms_practices.sql.go @@ -147,10 +147,15 @@ SELECT p.created_at, p.updated_at FROM lms_practices p +INNER JOIN question_sets qs ON qs.id = p.question_set_id WHERE p.course_id = $1 AND ( $4::boolean = FALSE - OR p.publish_status = 'PUBLISHED'::TEXT + OR ( + p.publish_status = 'PUBLISHED'::TEXT + AND qs.set_type = 'PRACTICE' + AND qs.status = 'PUBLISHED' + ) ) ORDER BY p.created_at DESC LIMIT $2 OFFSET $3 @@ -237,10 +242,15 @@ SELECT p.created_at, p.updated_at FROM lms_practices p +INNER JOIN question_sets qs ON qs.id = p.question_set_id WHERE p.lesson_id = $1 AND ( $4::boolean = FALSE - OR p.publish_status = 'PUBLISHED'::TEXT + OR ( + p.publish_status = 'PUBLISHED'::TEXT + AND qs.set_type = 'PRACTICE' + AND qs.status = 'PUBLISHED' + ) ) ORDER BY p.created_at DESC LIMIT $2 OFFSET $3 @@ -327,10 +337,15 @@ SELECT p.created_at, p.updated_at FROM lms_practices p +INNER JOIN question_sets qs ON qs.id = p.question_set_id WHERE p.module_id = $1 AND ( $4::boolean = FALSE - OR p.publish_status = 'PUBLISHED'::TEXT + OR ( + p.publish_status = 'PUBLISHED'::TEXT + AND qs.set_type = 'PRACTICE' + AND qs.status = 'PUBLISHED' + ) ) ORDER BY p.created_at DESC LIMIT $2 OFFSET $3 diff --git a/internal/web_server/handlers/questions.go b/internal/web_server/handlers/questions.go index cf83bb7..ac25bc5 100644 --- a/internal/web_server/handlers/questions.go +++ b/internal/web_server/handlers/questions.go @@ -1594,6 +1594,11 @@ 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 !strings.EqualFold(set.Status, "PUBLISHED") { + return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{ + Message: "Only published practices can be completed", + }) + } if practiceErr != nil { if err := h.forbidCompletingDraftPractice(c, set.ID); err != nil { code := fiber.StatusInternalServerError