Fix LMS sequential gating when sort_order has gaps.

Resolve the immediate predecessor by sort_order (and id) instead of requiring sort_order - 1, so learners cannot skip locked programs, courses, or modules when ordering numbers are non-consecutive.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Yared Yemane 2026-05-30 04:04:38 -07:00
parent fbad083ca4
commit c00ab684c5
2 changed files with 73 additions and 9 deletions

View File

@ -1,11 +1,23 @@
-- name: GetPreviousProgram :one
-- Immediate predecessor by sort_order within the same category (gaps in sort_order are allowed).
SELECT
p2.*
FROM
programs AS p1
INNER JOIN programs AS p2 ON p2.sort_order = p1.sort_order - 1
INNER JOIN programs AS p2 ON p2.category = p1.category
AND (
p2.sort_order < p1.sort_order
OR (
p2.sort_order = p1.sort_order
AND p2.id < p1.id
)
)
WHERE
p1.id = $1;
p1.id = $1
ORDER BY
p2.sort_order DESC,
p2.id DESC
LIMIT 1;
-- name: GetPreviousCourseInProgram :one
SELECT
@ -13,9 +25,19 @@ SELECT
FROM
courses AS c1
INNER JOIN courses AS c2 ON c2.program_id = c1.program_id
AND c2.sort_order = c1.sort_order - 1
AND (
c2.sort_order < c1.sort_order
OR (
c2.sort_order = c1.sort_order
AND c2.id < c1.id
)
)
WHERE
c1.id = $1;
c1.id = $1
ORDER BY
c2.sort_order DESC,
c2.id DESC
LIMIT 1;
-- name: GetPreviousModuleInCourse :one
SELECT
@ -23,9 +45,19 @@ SELECT
FROM
modules AS m1
INNER JOIN modules AS m2 ON m2.course_id = m1.course_id
AND m2.sort_order = m1.sort_order - 1
AND (
m2.sort_order < m1.sort_order
OR (
m2.sort_order = m1.sort_order
AND m2.id < m1.id
)
)
WHERE
m1.id = $1;
m1.id = $1
ORDER BY
m2.sort_order DESC,
m2.id DESC
LIMIT 1;
-- name: GetPreviousLessonInModule :one
SELECT

View File

@ -647,9 +647,19 @@ SELECT
FROM
courses AS c1
INNER JOIN courses AS c2 ON c2.program_id = c1.program_id
AND c2.sort_order = c1.sort_order - 1
AND (
c2.sort_order < c1.sort_order
OR (
c2.sort_order = c1.sort_order
AND c2.id < c1.id
)
)
WHERE
c1.id = $1
ORDER BY
c2.sort_order DESC,
c2.id DESC
LIMIT 1
`
func (q *Queries) GetPreviousCourseInProgram(ctx context.Context, id int64) (Course, error) {
@ -715,9 +725,19 @@ SELECT
FROM
modules AS m1
INNER JOIN modules AS m2 ON m2.course_id = m1.course_id
AND m2.sort_order = m1.sort_order - 1
AND (
m2.sort_order < m1.sort_order
OR (
m2.sort_order = m1.sort_order
AND m2.id < m1.id
)
)
WHERE
m1.id = $1
ORDER BY
m2.sort_order DESC,
m2.id DESC
LIMIT 1
`
func (q *Queries) GetPreviousModuleInCourse(ctx context.Context, id int64) (Module, error) {
@ -742,11 +762,23 @@ SELECT
p2.id, p2.name, p2.description, p2.thumbnail, p2.created_at, p2.updated_at, p2.sort_order, p2.category
FROM
programs AS p1
INNER JOIN programs AS p2 ON p2.sort_order = p1.sort_order - 1
INNER JOIN programs AS p2 ON p2.category = p1.category
AND (
p2.sort_order < p1.sort_order
OR (
p2.sort_order = p1.sort_order
AND p2.id < p1.id
)
)
WHERE
p1.id = $1
ORDER BY
p2.sort_order DESC,
p2.id DESC
LIMIT 1
`
// Immediate predecessor by sort_order within the same category (gaps in sort_order are allowed).
func (q *Queries) GetPreviousProgram(ctx context.Context, id int64) (Program, error) {
row := q.db.QueryRow(ctx, GetPreviousProgram, id)
var i Program