Add LMS lesson draft and publish visibility.
Migration 000062 adds lessons.publish_status (DRAFT default for new rows; existing rows published). Editors see all lessons; learners see published-only lists and GET by id. Sequential prerequisites and completion counts ignore drafts. Course lesson_count counts published lessons only. Swagger documents publish_status on create/update bodies. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
fffdff1031
commit
bd1767d2a6
3
db/migrations/000062_lesson_publish_status.down.sql
Normal file
3
db/migrations/000062_lesson_publish_status.down.sql
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
ALTER TABLE lessons DROP CONSTRAINT IF EXISTS chk_lessons_publish_status;
|
||||||
|
|
||||||
|
ALTER TABLE lessons DROP COLUMN IF EXISTS publish_status;
|
||||||
9
db/migrations/000062_lesson_publish_status.up.sql
Normal file
9
db/migrations/000062_lesson_publish_status.up.sql
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
-- Draft vs published visibility for LMS lessons (mirrors lms_practices.publish_status).
|
||||||
|
|
||||||
|
ALTER TABLE lessons
|
||||||
|
ADD COLUMN publish_status VARCHAR(16) NOT NULL DEFAULT 'PUBLISHED'
|
||||||
|
CONSTRAINT chk_lessons_publish_status CHECK (publish_status IN ('DRAFT', 'PUBLISHED'));
|
||||||
|
|
||||||
|
-- New inserts default to draft unless the API sends PUBLISHED; existing rows stay published.
|
||||||
|
ALTER TABLE lessons
|
||||||
|
ALTER COLUMN publish_status SET DEFAULT 'DRAFT';
|
||||||
|
|
@ -65,7 +65,8 @@ SELECT
|
||||||
lessons l
|
lessons l
|
||||||
INNER JOIN modules m ON l.module_id = m.id
|
INNER JOIN modules m ON l.module_id = m.id
|
||||||
WHERE
|
WHERE
|
||||||
m.course_id = c.id) AS lesson_count,
|
m.course_id = c.id
|
||||||
|
AND l.publish_status = 'PUBLISHED') AS lesson_count,
|
||||||
-- Practices whose parent is the course only (lms_practices.course_id). Excludes
|
-- Practices whose parent is the course only (lms_practices.course_id). Excludes
|
||||||
-- practices linked via module_id or lesson_id, even for modules/lessons in this course.
|
-- practices linked via module_id or lesson_id, even for modules/lessons in this course.
|
||||||
(
|
(
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
-- name: CreateLesson :one
|
-- name: CreateLesson :one
|
||||||
INSERT INTO lessons (module_id, title, video_url, thumbnail, description, sort_order)
|
INSERT INTO lessons (module_id, title, video_url, thumbnail, description, sort_order, publish_status)
|
||||||
SELECT
|
SELECT
|
||||||
sqlc.arg('module_id'),
|
sqlc.arg('module_id'),
|
||||||
sqlc.arg('title'),
|
sqlc.arg('title'),
|
||||||
|
|
@ -12,7 +12,8 @@ SELECT
|
||||||
max(l.sort_order)
|
max(l.sort_order)
|
||||||
FROM lessons l
|
FROM lessons l
|
||||||
WHERE
|
WHERE
|
||||||
l.module_id = sqlc.arg('module_id')), 0) + 1)
|
l.module_id = sqlc.arg('module_id')), 0) + 1),
|
||||||
|
sqlc.arg('publish_status')
|
||||||
RETURNING
|
RETURNING
|
||||||
*;
|
*;
|
||||||
|
|
||||||
|
|
@ -39,6 +40,7 @@ SELECT
|
||||||
l.thumbnail,
|
l.thumbnail,
|
||||||
l.description,
|
l.description,
|
||||||
l.sort_order,
|
l.sort_order,
|
||||||
|
l.publish_status,
|
||||||
l.created_at,
|
l.created_at,
|
||||||
l.updated_at,
|
l.updated_at,
|
||||||
EXISTS (
|
EXISTS (
|
||||||
|
|
@ -51,6 +53,10 @@ FROM
|
||||||
lessons l
|
lessons l
|
||||||
WHERE
|
WHERE
|
||||||
l.module_id = $1
|
l.module_id = $1
|
||||||
|
AND (
|
||||||
|
sqlc.arg('published_only')::boolean = FALSE
|
||||||
|
OR l.publish_status = 'PUBLISHED'::TEXT
|
||||||
|
)
|
||||||
ORDER BY
|
ORDER BY
|
||||||
l.sort_order ASC,
|
l.sort_order ASC,
|
||||||
l.id ASC
|
l.id ASC
|
||||||
|
|
@ -65,6 +71,7 @@ SET
|
||||||
thumbnail = COALESCE(sqlc.narg('thumbnail')::text, thumbnail),
|
thumbnail = COALESCE(sqlc.narg('thumbnail')::text, thumbnail),
|
||||||
description = COALESCE(sqlc.narg('description')::text, description),
|
description = COALESCE(sqlc.narg('description')::text, description),
|
||||||
sort_order = coalesce(sqlc.narg('sort_order')::int, sort_order),
|
sort_order = coalesce(sqlc.narg('sort_order')::int, sort_order),
|
||||||
|
publish_status = COALESCE(sqlc.narg('publish_status')::varchar, publish_status),
|
||||||
updated_at = CURRENT_TIMESTAMP
|
updated_at = CURRENT_TIMESTAMP
|
||||||
WHERE
|
WHERE
|
||||||
id = sqlc.arg('id')
|
id = sqlc.arg('id')
|
||||||
|
|
|
||||||
|
|
@ -33,9 +33,21 @@ SELECT
|
||||||
FROM
|
FROM
|
||||||
lessons AS l1
|
lessons AS l1
|
||||||
INNER JOIN lessons AS l2 ON l2.module_id = l1.module_id
|
INNER JOIN lessons AS l2 ON l2.module_id = l1.module_id
|
||||||
AND l2.sort_order = l1.sort_order - 1
|
AND l2.publish_status = 'PUBLISHED'
|
||||||
|
AND l1.publish_status = 'PUBLISHED'
|
||||||
|
AND (
|
||||||
|
l2.sort_order < l1.sort_order
|
||||||
|
OR (
|
||||||
|
l2.sort_order = l1.sort_order
|
||||||
|
AND l2.id < l1.id
|
||||||
|
)
|
||||||
|
)
|
||||||
WHERE
|
WHERE
|
||||||
l1.id = $1;
|
l1.id = $1
|
||||||
|
ORDER BY
|
||||||
|
l2.sort_order DESC,
|
||||||
|
l2.id DESC
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
-- name: UserHasProgramProgress :one
|
-- name: UserHasProgramProgress :one
|
||||||
SELECT
|
SELECT
|
||||||
|
|
@ -111,7 +123,8 @@ SELECT
|
||||||
FROM
|
FROM
|
||||||
lessons
|
lessons
|
||||||
WHERE
|
WHERE
|
||||||
module_id = $1;
|
module_id = $1
|
||||||
|
AND publish_status = 'PUBLISHED';
|
||||||
|
|
||||||
-- name: CountUserCompletedLessonsInModule :one
|
-- name: CountUserCompletedLessonsInModule :one
|
||||||
SELECT
|
SELECT
|
||||||
|
|
@ -121,7 +134,8 @@ FROM
|
||||||
INNER JOIN lessons l ON l.id = ulp.lesson_id
|
INNER JOIN lessons l ON l.id = ulp.lesson_id
|
||||||
WHERE
|
WHERE
|
||||||
l.module_id = $1
|
l.module_id = $1
|
||||||
AND ulp.user_id = $2;
|
AND ulp.user_id = $2
|
||||||
|
AND l.publish_status = 'PUBLISHED';
|
||||||
|
|
||||||
-- name: CountModulesInCourse :one
|
-- name: CountModulesInCourse :one
|
||||||
SELECT
|
SELECT
|
||||||
|
|
@ -211,7 +225,8 @@ FROM
|
||||||
lessons l
|
lessons l
|
||||||
INNER JOIN modules m ON m.id = l.module_id
|
INNER JOIN modules m ON m.id = l.module_id
|
||||||
WHERE
|
WHERE
|
||||||
m.course_id = $1;
|
m.course_id = $1
|
||||||
|
AND l.publish_status = 'PUBLISHED';
|
||||||
|
|
||||||
-- name: CountUserCompletedLessonsInCourse :one
|
-- name: CountUserCompletedLessonsInCourse :one
|
||||||
SELECT
|
SELECT
|
||||||
|
|
@ -222,7 +237,8 @@ FROM
|
||||||
INNER JOIN modules m ON m.id = l.module_id
|
INNER JOIN modules m ON m.id = l.module_id
|
||||||
WHERE
|
WHERE
|
||||||
m.course_id = $1
|
m.course_id = $1
|
||||||
AND ulp.user_id = $2;
|
AND ulp.user_id = $2
|
||||||
|
AND l.publish_status = 'PUBLISHED';
|
||||||
|
|
||||||
-- Lesson-based progress within a program (all courses).
|
-- Lesson-based progress within a program (all courses).
|
||||||
-- name: CountLessonsInProgram :one
|
-- name: CountLessonsInProgram :one
|
||||||
|
|
@ -233,7 +249,8 @@ FROM
|
||||||
INNER JOIN modules m ON m.id = l.module_id
|
INNER JOIN modules m ON m.id = l.module_id
|
||||||
INNER JOIN courses c ON c.id = m.course_id
|
INNER JOIN courses c ON c.id = m.course_id
|
||||||
WHERE
|
WHERE
|
||||||
c.program_id = $1;
|
c.program_id = $1
|
||||||
|
AND l.publish_status = 'PUBLISHED';
|
||||||
|
|
||||||
-- name: CountUserCompletedLessonsInProgram :one
|
-- name: CountUserCompletedLessonsInProgram :one
|
||||||
SELECT
|
SELECT
|
||||||
|
|
@ -245,7 +262,8 @@ FROM
|
||||||
INNER JOIN courses c ON c.id = m.course_id
|
INNER JOIN courses c ON c.id = m.course_id
|
||||||
WHERE
|
WHERE
|
||||||
c.program_id = $1
|
c.program_id = $1
|
||||||
AND ulp.user_id = $2;
|
AND ulp.user_id = $2
|
||||||
|
AND l.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).
|
||||||
-- name: CountPublishedPracticesInModule :one
|
-- name: CountPublishedPracticesInModule :one
|
||||||
|
|
|
||||||
|
|
@ -10484,6 +10484,10 @@ const docTemplate = `{
|
||||||
"description": {
|
"description": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"publish_status": {
|
||||||
|
"description": "Omit or DRAFT (default) for drafts; PUBLISHED for learner-visible lessons.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"sort_order": {
|
"sort_order": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
|
@ -11375,6 +11379,10 @@ const docTemplate = `{
|
||||||
"description": {
|
"description": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"publish_status": {
|
||||||
|
"description": "DRAFT or PUBLISHED",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"sort_order": {
|
"sort_order": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -10476,6 +10476,10 @@
|
||||||
"description": {
|
"description": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"publish_status": {
|
||||||
|
"description": "Omit or DRAFT (default) for drafts; PUBLISHED for learner-visible lessons.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"sort_order": {
|
"sort_order": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
|
@ -11367,6 +11371,10 @@
|
||||||
"description": {
|
"description": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"publish_status": {
|
||||||
|
"description": "DRAFT or PUBLISHED",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"sort_order": {
|
"sort_order": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -418,6 +418,9 @@ definitions:
|
||||||
properties:
|
properties:
|
||||||
description:
|
description:
|
||||||
type: string
|
type: string
|
||||||
|
publish_status:
|
||||||
|
description: Omit or DRAFT (default) for drafts; PUBLISHED for learner-visible lessons.
|
||||||
|
type: string
|
||||||
sort_order:
|
sort_order:
|
||||||
type: integer
|
type: integer
|
||||||
thumbnail:
|
thumbnail:
|
||||||
|
|
@ -1027,6 +1030,9 @@ definitions:
|
||||||
properties:
|
properties:
|
||||||
description:
|
description:
|
||||||
type: string
|
type: string
|
||||||
|
publish_status:
|
||||||
|
description: DRAFT or PUBLISHED
|
||||||
|
type: string
|
||||||
sort_order:
|
sort_order:
|
||||||
type: integer
|
type: integer
|
||||||
thumbnail:
|
thumbnail:
|
||||||
|
|
|
||||||
|
|
@ -170,7 +170,8 @@ SELECT
|
||||||
lessons l
|
lessons l
|
||||||
INNER JOIN modules m ON l.module_id = m.id
|
INNER JOIN modules m ON l.module_id = m.id
|
||||||
WHERE
|
WHERE
|
||||||
m.course_id = c.id) AS lesson_count,
|
m.course_id = c.id
|
||||||
|
AND l.publish_status = 'PUBLISHED') AS lesson_count,
|
||||||
-- Practices whose parent is the course only (lms_practices.course_id). Excludes
|
-- Practices whose parent is the course only (lms_practices.course_id). Excludes
|
||||||
-- practices linked via module_id or lesson_id, even for modules/lessons in this course.
|
-- practices linked via module_id or lesson_id, even for modules/lessons in this course.
|
||||||
(
|
(
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const CreateLesson = `-- name: CreateLesson :one
|
const CreateLesson = `-- name: CreateLesson :one
|
||||||
INSERT INTO lessons (module_id, title, video_url, thumbnail, description, sort_order)
|
INSERT INTO lessons (module_id, title, video_url, thumbnail, description, sort_order, publish_status)
|
||||||
SELECT
|
SELECT
|
||||||
$1,
|
$1,
|
||||||
$2,
|
$2,
|
||||||
|
|
@ -25,9 +25,10 @@ SELECT
|
||||||
max(l.sort_order)
|
max(l.sort_order)
|
||||||
FROM lessons l
|
FROM lessons l
|
||||||
WHERE
|
WHERE
|
||||||
l.module_id = $1), 0) + 1)
|
l.module_id = $1), 0) + 1),
|
||||||
|
$7
|
||||||
RETURNING
|
RETURNING
|
||||||
id, module_id, title, video_url, thumbnail, description, created_at, updated_at, sort_order
|
id, module_id, title, video_url, thumbnail, description, created_at, updated_at, sort_order, publish_status
|
||||||
`
|
`
|
||||||
|
|
||||||
type CreateLessonParams struct {
|
type CreateLessonParams struct {
|
||||||
|
|
@ -37,6 +38,7 @@ type CreateLessonParams struct {
|
||||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||||
Description pgtype.Text `json:"description"`
|
Description pgtype.Text `json:"description"`
|
||||||
SortOrder pgtype.Int4 `json:"sort_order"`
|
SortOrder pgtype.Int4 `json:"sort_order"`
|
||||||
|
PublishStatus string `json:"publish_status"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) CreateLesson(ctx context.Context, arg CreateLessonParams) (Lesson, error) {
|
func (q *Queries) CreateLesson(ctx context.Context, arg CreateLessonParams) (Lesson, error) {
|
||||||
|
|
@ -47,6 +49,7 @@ func (q *Queries) CreateLesson(ctx context.Context, arg CreateLessonParams) (Les
|
||||||
arg.Thumbnail,
|
arg.Thumbnail,
|
||||||
arg.Description,
|
arg.Description,
|
||||||
arg.SortOrder,
|
arg.SortOrder,
|
||||||
|
arg.PublishStatus,
|
||||||
)
|
)
|
||||||
var i Lesson
|
var i Lesson
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
|
|
@ -59,6 +62,7 @@ func (q *Queries) CreateLesson(ctx context.Context, arg CreateLessonParams) (Les
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
&i.SortOrder,
|
&i.SortOrder,
|
||||||
|
&i.PublishStatus,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
@ -75,7 +79,7 @@ func (q *Queries) DeleteLesson(ctx context.Context, id int64) error {
|
||||||
|
|
||||||
const GetLessonByID = `-- name: GetLessonByID :one
|
const GetLessonByID = `-- name: GetLessonByID :one
|
||||||
SELECT
|
SELECT
|
||||||
l.id, l.module_id, l.title, l.video_url, l.thumbnail, l.description, l.created_at, l.updated_at, l.sort_order,
|
l.id, l.module_id, l.title, l.video_url, l.thumbnail, l.description, l.created_at, l.updated_at, l.sort_order, l.publish_status,
|
||||||
EXISTS (
|
EXISTS (
|
||||||
SELECT 1
|
SELECT 1
|
||||||
FROM lms_practices p
|
FROM lms_practices p
|
||||||
|
|
@ -97,6 +101,7 @@ type GetLessonByIDRow struct {
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
SortOrder int32 `json:"sort_order"`
|
SortOrder int32 `json:"sort_order"`
|
||||||
|
PublishStatus string `json:"publish_status"`
|
||||||
HasPractice bool `json:"has_practice"`
|
HasPractice bool `json:"has_practice"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -113,6 +118,7 @@ func (q *Queries) GetLessonByID(ctx context.Context, id int64) (GetLessonByIDRow
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
&i.SortOrder,
|
&i.SortOrder,
|
||||||
|
&i.PublishStatus,
|
||||||
&i.HasPractice,
|
&i.HasPractice,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
|
|
@ -128,6 +134,7 @@ SELECT
|
||||||
l.thumbnail,
|
l.thumbnail,
|
||||||
l.description,
|
l.description,
|
||||||
l.sort_order,
|
l.sort_order,
|
||||||
|
l.publish_status,
|
||||||
l.created_at,
|
l.created_at,
|
||||||
l.updated_at,
|
l.updated_at,
|
||||||
EXISTS (
|
EXISTS (
|
||||||
|
|
@ -140,6 +147,10 @@ FROM
|
||||||
lessons l
|
lessons l
|
||||||
WHERE
|
WHERE
|
||||||
l.module_id = $1
|
l.module_id = $1
|
||||||
|
AND (
|
||||||
|
$4::boolean = FALSE
|
||||||
|
OR l.publish_status = 'PUBLISHED'::TEXT
|
||||||
|
)
|
||||||
ORDER BY
|
ORDER BY
|
||||||
l.sort_order ASC,
|
l.sort_order ASC,
|
||||||
l.id ASC
|
l.id ASC
|
||||||
|
|
@ -151,6 +162,7 @@ type ListLessonsByModuleIDParams struct {
|
||||||
ModuleID int64 `json:"module_id"`
|
ModuleID int64 `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 ListLessonsByModuleIDRow struct {
|
type ListLessonsByModuleIDRow struct {
|
||||||
|
|
@ -162,13 +174,19 @@ type ListLessonsByModuleIDRow struct {
|
||||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||||
Description pgtype.Text `json:"description"`
|
Description pgtype.Text `json:"description"`
|
||||||
SortOrder int32 `json:"sort_order"`
|
SortOrder int32 `json:"sort_order"`
|
||||||
|
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"`
|
||||||
HasPractice bool `json:"has_practice"`
|
HasPractice bool `json:"has_practice"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) ListLessonsByModuleID(ctx context.Context, arg ListLessonsByModuleIDParams) ([]ListLessonsByModuleIDRow, error) {
|
func (q *Queries) ListLessonsByModuleID(ctx context.Context, arg ListLessonsByModuleIDParams) ([]ListLessonsByModuleIDRow, error) {
|
||||||
rows, err := q.db.Query(ctx, ListLessonsByModuleID, arg.ModuleID, arg.Limit, arg.Offset)
|
rows, err := q.db.Query(ctx, ListLessonsByModuleID,
|
||||||
|
arg.ModuleID,
|
||||||
|
arg.Limit,
|
||||||
|
arg.Offset,
|
||||||
|
arg.PublishedOnly,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -185,6 +203,7 @@ func (q *Queries) ListLessonsByModuleID(ctx context.Context, arg ListLessonsByMo
|
||||||
&i.Thumbnail,
|
&i.Thumbnail,
|
||||||
&i.Description,
|
&i.Description,
|
||||||
&i.SortOrder,
|
&i.SortOrder,
|
||||||
|
&i.PublishStatus,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
&i.HasPractice,
|
&i.HasPractice,
|
||||||
|
|
@ -207,11 +226,12 @@ SET
|
||||||
thumbnail = COALESCE($3::text, thumbnail),
|
thumbnail = COALESCE($3::text, thumbnail),
|
||||||
description = COALESCE($4::text, description),
|
description = COALESCE($4::text, description),
|
||||||
sort_order = coalesce($5::int, sort_order),
|
sort_order = coalesce($5::int, sort_order),
|
||||||
|
publish_status = COALESCE($6::varchar, publish_status),
|
||||||
updated_at = CURRENT_TIMESTAMP
|
updated_at = CURRENT_TIMESTAMP
|
||||||
WHERE
|
WHERE
|
||||||
id = $6
|
id = $7
|
||||||
RETURNING
|
RETURNING
|
||||||
id, module_id, title, video_url, thumbnail, description, created_at, updated_at, sort_order
|
id, module_id, title, video_url, thumbnail, description, created_at, updated_at, sort_order, publish_status
|
||||||
`
|
`
|
||||||
|
|
||||||
type UpdateLessonParams struct {
|
type UpdateLessonParams struct {
|
||||||
|
|
@ -220,6 +240,7 @@ type UpdateLessonParams struct {
|
||||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||||
Description pgtype.Text `json:"description"`
|
Description pgtype.Text `json:"description"`
|
||||||
SortOrder pgtype.Int4 `json:"sort_order"`
|
SortOrder pgtype.Int4 `json:"sort_order"`
|
||||||
|
PublishStatus pgtype.Text `json:"publish_status"`
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -230,6 +251,7 @@ func (q *Queries) UpdateLesson(ctx context.Context, arg UpdateLessonParams) (Les
|
||||||
arg.Thumbnail,
|
arg.Thumbnail,
|
||||||
arg.Description,
|
arg.Description,
|
||||||
arg.SortOrder,
|
arg.SortOrder,
|
||||||
|
arg.PublishStatus,
|
||||||
arg.ID,
|
arg.ID,
|
||||||
)
|
)
|
||||||
var i Lesson
|
var i Lesson
|
||||||
|
|
@ -243,6 +265,7 @@ func (q *Queries) UpdateLesson(ctx context.Context, arg UpdateLessonParams) (Les
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
&i.SortOrder,
|
&i.SortOrder,
|
||||||
|
&i.PublishStatus,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ FROM
|
||||||
INNER JOIN modules m ON m.id = l.module_id
|
INNER JOIN modules m ON m.id = l.module_id
|
||||||
WHERE
|
WHERE
|
||||||
m.course_id = $1
|
m.course_id = $1
|
||||||
|
AND l.publish_status = 'PUBLISHED'
|
||||||
`
|
`
|
||||||
|
|
||||||
// Lesson-based progress within a course (all modules).
|
// Lesson-based progress within a course (all modules).
|
||||||
|
|
@ -52,6 +53,7 @@ FROM
|
||||||
lessons
|
lessons
|
||||||
WHERE
|
WHERE
|
||||||
module_id = $1
|
module_id = $1
|
||||||
|
AND publish_status = 'PUBLISHED'
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) CountLessonsInModule(ctx context.Context, moduleID int64) (int32, error) {
|
func (q *Queries) CountLessonsInModule(ctx context.Context, moduleID int64) (int32, error) {
|
||||||
|
|
@ -70,6 +72,7 @@ FROM
|
||||||
INNER JOIN courses c ON c.id = m.course_id
|
INNER JOIN courses c ON c.id = m.course_id
|
||||||
WHERE
|
WHERE
|
||||||
c.program_id = $1
|
c.program_id = $1
|
||||||
|
AND l.publish_status = 'PUBLISHED'
|
||||||
`
|
`
|
||||||
|
|
||||||
// Lesson-based progress within a program (all courses).
|
// Lesson-based progress within a program (all courses).
|
||||||
|
|
@ -191,6 +194,7 @@ FROM
|
||||||
WHERE
|
WHERE
|
||||||
m.course_id = $1
|
m.course_id = $1
|
||||||
AND ulp.user_id = $2
|
AND ulp.user_id = $2
|
||||||
|
AND l.publish_status = 'PUBLISHED'
|
||||||
`
|
`
|
||||||
|
|
||||||
type CountUserCompletedLessonsInCourseParams struct {
|
type CountUserCompletedLessonsInCourseParams struct {
|
||||||
|
|
@ -214,6 +218,7 @@ FROM
|
||||||
WHERE
|
WHERE
|
||||||
l.module_id = $1
|
l.module_id = $1
|
||||||
AND ulp.user_id = $2
|
AND ulp.user_id = $2
|
||||||
|
AND l.publish_status = 'PUBLISHED'
|
||||||
`
|
`
|
||||||
|
|
||||||
type CountUserCompletedLessonsInModuleParams struct {
|
type CountUserCompletedLessonsInModuleParams struct {
|
||||||
|
|
@ -239,6 +244,7 @@ FROM
|
||||||
WHERE
|
WHERE
|
||||||
c.program_id = $1
|
c.program_id = $1
|
||||||
AND ulp.user_id = $2
|
AND ulp.user_id = $2
|
||||||
|
AND l.publish_status = 'PUBLISHED'
|
||||||
`
|
`
|
||||||
|
|
||||||
type CountUserCompletedLessonsInProgramParams struct {
|
type CountUserCompletedLessonsInProgramParams struct {
|
||||||
|
|
@ -423,13 +429,25 @@ func (q *Queries) GetPreviousCourseInProgram(ctx context.Context, id int64) (Cou
|
||||||
|
|
||||||
const GetPreviousLessonInModule = `-- name: GetPreviousLessonInModule :one
|
const GetPreviousLessonInModule = `-- name: GetPreviousLessonInModule :one
|
||||||
SELECT
|
SELECT
|
||||||
l2.id, l2.module_id, l2.title, l2.video_url, l2.thumbnail, l2.description, l2.created_at, l2.updated_at, l2.sort_order
|
l2.id, l2.module_id, l2.title, l2.video_url, l2.thumbnail, l2.description, l2.created_at, l2.updated_at, l2.sort_order, l2.publish_status
|
||||||
FROM
|
FROM
|
||||||
lessons AS l1
|
lessons AS l1
|
||||||
INNER JOIN lessons AS l2 ON l2.module_id = l1.module_id
|
INNER JOIN lessons AS l2 ON l2.module_id = l1.module_id
|
||||||
AND l2.sort_order = l1.sort_order - 1
|
AND l2.publish_status = 'PUBLISHED'
|
||||||
|
AND l1.publish_status = 'PUBLISHED'
|
||||||
|
AND (
|
||||||
|
l2.sort_order < l1.sort_order
|
||||||
|
OR (
|
||||||
|
l2.sort_order = l1.sort_order
|
||||||
|
AND l2.id < l1.id
|
||||||
|
)
|
||||||
|
)
|
||||||
WHERE
|
WHERE
|
||||||
l1.id = $1
|
l1.id = $1
|
||||||
|
ORDER BY
|
||||||
|
l2.sort_order DESC,
|
||||||
|
l2.id DESC
|
||||||
|
LIMIT 1
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) GetPreviousLessonInModule(ctx context.Context, id int64) (Lesson, error) {
|
func (q *Queries) GetPreviousLessonInModule(ctx context.Context, id int64) (Lesson, error) {
|
||||||
|
|
@ -445,6 +463,7 @@ func (q *Queries) GetPreviousLessonInModule(ctx context.Context, id int64) (Less
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
&i.SortOrder,
|
&i.SortOrder,
|
||||||
|
&i.PublishStatus,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -130,6 +130,7 @@ type Lesson struct {
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
SortOrder int32 `json:"sort_order"`
|
SortOrder int32 `json:"sort_order"`
|
||||||
|
PublishStatus string `json:"publish_status"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type LevelToSubCourse struct {
|
type LevelToSubCourse struct {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,35 @@
|
||||||
package domain
|
package domain
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LessonPublishStatus controls learner visibility for an LMS lesson row (like PracticePublishStatus).
|
||||||
|
type LessonPublishStatus string
|
||||||
|
|
||||||
|
const (
|
||||||
|
LessonPublishDraft LessonPublishStatus = "DRAFT"
|
||||||
|
LessonPublishPublished LessonPublishStatus = "PUBLISHED"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LessonPublishStatusFromDB normalizes persisted values.
|
||||||
|
func LessonPublishStatusFromDB(raw string) LessonPublishStatus {
|
||||||
|
switch strings.TrimSpace(strings.ToUpper(raw)) {
|
||||||
|
case string(LessonPublishPublished):
|
||||||
|
return LessonPublishPublished
|
||||||
|
default:
|
||||||
|
return LessonPublishDraft
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LessonPublishStatusFromCreateInput resolves create body: omit → draft; explicit value validated separately.
|
||||||
|
func LessonPublishStatusFromCreateInput(raw *string) LessonPublishStatus {
|
||||||
|
if raw == nil || strings.TrimSpace(*raw) == "" {
|
||||||
|
return LessonPublishDraft
|
||||||
|
}
|
||||||
|
return LessonPublishStatusFromDB(*raw)
|
||||||
|
}
|
||||||
|
|
||||||
// Lesson belongs to a Module.
|
// Lesson belongs to a Module.
|
||||||
type Lesson struct {
|
type Lesson struct {
|
||||||
|
|
@ -11,12 +40,18 @@ type Lesson struct {
|
||||||
Thumbnail *string `json:"thumbnail,omitempty"`
|
Thumbnail *string `json:"thumbnail,omitempty"`
|
||||||
Description *string `json:"description,omitempty"`
|
Description *string `json:"description,omitempty"`
|
||||||
SortOrder int `json:"sort_order"`
|
SortOrder int `json:"sort_order"`
|
||||||
|
PublishStatus LessonPublishStatus `json:"publish_status"`
|
||||||
HasPractice bool `json:"has_practice"`
|
HasPractice bool `json:"has_practice"`
|
||||||
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"`
|
||||||
Access *LMSEntityAccess `json:"access,omitempty"`
|
Access *LMSEntityAccess `json:"access,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// VisibleToLearners is true when the lesson appears in subscriber/catalog LMS APIs.
|
||||||
|
func (l Lesson) VisibleToLearners() bool {
|
||||||
|
return l.PublishStatus == LessonPublishPublished
|
||||||
|
}
|
||||||
|
|
||||||
type CreateLessonInput struct {
|
type CreateLessonInput struct {
|
||||||
Title string `json:"title" validate:"required"`
|
Title string `json:"title" validate:"required"`
|
||||||
VideoURL *string `json:"video_url,omitempty"`
|
VideoURL *string `json:"video_url,omitempty"`
|
||||||
|
|
@ -24,6 +59,8 @@ type CreateLessonInput struct {
|
||||||
Description *string `json:"description,omitempty"`
|
Description *string `json:"description,omitempty"`
|
||||||
// SortOrder within the module when set; omit to append after current max within module_id.
|
// SortOrder within the module when set; omit to append after current max within module_id.
|
||||||
SortOrder *int `json:"sort_order,omitempty" validate:"omitempty,min=0"`
|
SortOrder *int `json:"sort_order,omitempty" validate:"omitempty,min=0"`
|
||||||
|
// Omit or empty defaults to DRAFT; set PUBLISHED to make visible to learners immediately.
|
||||||
|
PublishStatus *string `json:"publish_status,omitempty" validate:"omitempty,oneof=DRAFT draft PUBLISHED published"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateLessonInput struct {
|
type UpdateLessonInput struct {
|
||||||
|
|
@ -32,4 +69,5 @@ type UpdateLessonInput struct {
|
||||||
Thumbnail *string `json:"thumbnail,omitempty"`
|
Thumbnail *string `json:"thumbnail,omitempty"`
|
||||||
Description *string `json:"description,omitempty"`
|
Description *string `json:"description,omitempty"`
|
||||||
SortOrder *int `json:"sort_order,omitempty"`
|
SortOrder *int `json:"sort_order,omitempty"`
|
||||||
|
PublishStatus *string `json:"publish_status,omitempty" validate:"omitempty,oneof=DRAFT draft PUBLISHED published"`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import (
|
||||||
type LessonStore interface {
|
type LessonStore interface {
|
||||||
CreateLesson(ctx context.Context, moduleID int64, input domain.CreateLessonInput) (domain.Lesson, error)
|
CreateLesson(ctx context.Context, moduleID int64, input domain.CreateLessonInput) (domain.Lesson, error)
|
||||||
GetLessonByID(ctx context.Context, id int64) (domain.Lesson, error)
|
GetLessonByID(ctx context.Context, id int64) (domain.Lesson, error)
|
||||||
ListLessonsByModuleID(ctx context.Context, moduleID int64, limit, offset int32) ([]domain.Lesson, int64, error)
|
ListLessonsByModuleID(ctx context.Context, moduleID int64, publishedOnly bool, limit, offset int32) ([]domain.Lesson, int64, error)
|
||||||
UpdateLesson(ctx context.Context, id int64, input domain.UpdateLessonInput) (domain.Lesson, error)
|
UpdateLesson(ctx context.Context, id int64, input domain.UpdateLessonInput) (domain.Lesson, error)
|
||||||
DeleteLesson(ctx context.Context, id int64) error
|
DeleteLesson(ctx context.Context, id int64) error
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ func lessonToDomain(l dbgen.Lesson) domain.Lesson {
|
||||||
ID: l.ID,
|
ID: l.ID,
|
||||||
ModuleID: l.ModuleID,
|
ModuleID: l.ModuleID,
|
||||||
Title: l.Title,
|
Title: l.Title,
|
||||||
|
PublishStatus: domain.LessonPublishStatusFromDB(l.PublishStatus),
|
||||||
}
|
}
|
||||||
out.VideoURL = fromPgText(l.VideoUrl)
|
out.VideoURL = fromPgText(l.VideoUrl)
|
||||||
out.Thumbnail = fromPgText(l.Thumbnail)
|
out.Thumbnail = fromPgText(l.Thumbnail)
|
||||||
|
|
@ -30,6 +31,8 @@ func lessonToDomain(l dbgen.Lesson) domain.Lesson {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) CreateLesson(ctx context.Context, moduleID int64, input domain.CreateLessonInput) (domain.Lesson, error) {
|
func (s *Store) CreateLesson(ctx context.Context, moduleID int64, input domain.CreateLessonInput) (domain.Lesson, error) {
|
||||||
|
pub := string(domain.LessonPublishStatusFromCreateInput(input.PublishStatus))
|
||||||
|
|
||||||
if input.SortOrder != nil {
|
if input.SortOrder != nil {
|
||||||
q, tx, err := s.BeginTx(ctx)
|
q, tx, err := s.BeginTx(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -50,6 +53,7 @@ func (s *Store) CreateLesson(ctx context.Context, moduleID int64, input domain.C
|
||||||
Thumbnail: toPgText(input.Thumbnail),
|
Thumbnail: toPgText(input.Thumbnail),
|
||||||
Description: toPgText(input.Description),
|
Description: toPgText(input.Description),
|
||||||
SortOrder: pgtype.Int4{Int32: target, Valid: true},
|
SortOrder: pgtype.Int4{Int32: target, Valid: true},
|
||||||
|
PublishStatus: pub,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return domain.Lesson{}, err
|
return domain.Lesson{}, err
|
||||||
|
|
@ -67,6 +71,7 @@ func (s *Store) CreateLesson(ctx context.Context, moduleID int64, input domain.C
|
||||||
Thumbnail: toPgText(input.Thumbnail),
|
Thumbnail: toPgText(input.Thumbnail),
|
||||||
Description: toPgText(input.Description),
|
Description: toPgText(input.Description),
|
||||||
SortOrder: pgtype.Int4{Valid: false},
|
SortOrder: pgtype.Int4{Valid: false},
|
||||||
|
PublishStatus: pub,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return domain.Lesson{}, err
|
return domain.Lesson{}, err
|
||||||
|
|
@ -90,6 +95,7 @@ func (s *Store) GetLessonByID(ctx context.Context, id int64) (domain.Lesson, err
|
||||||
Thumbnail: l.Thumbnail,
|
Thumbnail: l.Thumbnail,
|
||||||
Description: l.Description,
|
Description: l.Description,
|
||||||
SortOrder: l.SortOrder,
|
SortOrder: l.SortOrder,
|
||||||
|
PublishStatus: l.PublishStatus,
|
||||||
CreatedAt: l.CreatedAt,
|
CreatedAt: l.CreatedAt,
|
||||||
UpdatedAt: l.UpdatedAt,
|
UpdatedAt: l.UpdatedAt,
|
||||||
})
|
})
|
||||||
|
|
@ -97,11 +103,12 @@ func (s *Store) GetLessonByID(ctx context.Context, id int64) (domain.Lesson, err
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) ListLessonsByModuleID(ctx context.Context, moduleID int64, limit, offset int32) ([]domain.Lesson, int64, error) {
|
func (s *Store) ListLessonsByModuleID(ctx context.Context, moduleID int64, publishedOnly bool, limit, offset int32) ([]domain.Lesson, int64, error) {
|
||||||
rows, err := s.queries.ListLessonsByModuleID(ctx, dbgen.ListLessonsByModuleIDParams{
|
rows, err := s.queries.ListLessonsByModuleID(ctx, dbgen.ListLessonsByModuleIDParams{
|
||||||
ModuleID: moduleID,
|
ModuleID: moduleID,
|
||||||
Limit: limit,
|
Limit: limit,
|
||||||
Offset: offset,
|
Offset: offset,
|
||||||
|
PublishedOnly: publishedOnly,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
|
|
@ -122,9 +129,10 @@ func (s *Store) ListLessonsByModuleID(ctx context.Context, moduleID int64, limit
|
||||||
VideoUrl: r.VideoUrl,
|
VideoUrl: r.VideoUrl,
|
||||||
Thumbnail: r.Thumbnail,
|
Thumbnail: r.Thumbnail,
|
||||||
Description: r.Description,
|
Description: r.Description,
|
||||||
|
SortOrder: r.SortOrder,
|
||||||
|
PublishStatus: r.PublishStatus,
|
||||||
CreatedAt: r.CreatedAt,
|
CreatedAt: r.CreatedAt,
|
||||||
UpdatedAt: r.UpdatedAt,
|
UpdatedAt: r.UpdatedAt,
|
||||||
SortOrder: r.SortOrder,
|
|
||||||
})
|
})
|
||||||
lesson.HasPractice = r.HasPractice
|
lesson.HasPractice = r.HasPractice
|
||||||
out = append(out, lesson)
|
out = append(out, lesson)
|
||||||
|
|
@ -134,6 +142,8 @@ func (s *Store) ListLessonsByModuleID(ctx context.Context, moduleID int64, limit
|
||||||
|
|
||||||
func (s *Store) UpdateLesson(ctx context.Context, id int64, input domain.UpdateLessonInput) (domain.Lesson, error) {
|
func (s *Store) UpdateLesson(ctx context.Context, id int64, input domain.UpdateLessonInput) (domain.Lesson, error) {
|
||||||
sortParam := optionalInt4Update(input.SortOrder)
|
sortParam := optionalInt4Update(input.SortOrder)
|
||||||
|
pubParam := optionalPublishStatusUpdate(input.PublishStatus)
|
||||||
|
|
||||||
var titleText pgtype.Text
|
var titleText pgtype.Text
|
||||||
if input.Title != nil {
|
if input.Title != nil {
|
||||||
titleText = pgtype.Text{String: *input.Title, Valid: true}
|
titleText = pgtype.Text{String: *input.Title, Valid: true}
|
||||||
|
|
@ -164,6 +174,7 @@ func (s *Store) UpdateLesson(ctx context.Context, id int64, input domain.UpdateL
|
||||||
Thumbnail: optionalTextUpdate(input.Thumbnail),
|
Thumbnail: optionalTextUpdate(input.Thumbnail),
|
||||||
Description: optionalTextUpdate(input.Description),
|
Description: optionalTextUpdate(input.Description),
|
||||||
SortOrder: pgtype.Int4{Valid: false},
|
SortOrder: pgtype.Int4{Valid: false},
|
||||||
|
PublishStatus: pubParam,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return domain.Lesson{}, err
|
return domain.Lesson{}, err
|
||||||
|
|
@ -185,6 +196,7 @@ func (s *Store) UpdateLesson(ctx context.Context, id int64, input domain.UpdateL
|
||||||
Thumbnail: optionalTextUpdate(input.Thumbnail),
|
Thumbnail: optionalTextUpdate(input.Thumbnail),
|
||||||
Description: optionalTextUpdate(input.Description),
|
Description: optionalTextUpdate(input.Description),
|
||||||
SortOrder: sortParam,
|
SortOrder: sortParam,
|
||||||
|
PublishStatus: pubParam,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ func (s *Service) GetByID(ctx context.Context, id int64) (domain.Lesson, error)
|
||||||
return l, nil
|
return l, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) ListByModule(ctx context.Context, moduleID int64, limit, offset int32) ([]domain.Lesson, int64, error) {
|
func (s *Service) ListByModule(ctx context.Context, moduleID int64, publishedOnly bool, limit, offset int32) ([]domain.Lesson, int64, error) {
|
||||||
if err := s.getModuleOrErr(ctx, moduleID); err != nil {
|
if err := s.getModuleOrErr(ctx, moduleID); err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
|
@ -63,7 +63,7 @@ func (s *Service) ListByModule(ctx context.Context, moduleID int64, limit, offse
|
||||||
if offset < 0 {
|
if offset < 0 {
|
||||||
offset = 0
|
offset = 0
|
||||||
}
|
}
|
||||||
return s.lessons.ListLessonsByModuleID(ctx, moduleID, limit, offset)
|
return s.lessons.ListLessonsByModuleID(ctx, moduleID, publishedOnly, limit, offset)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) Update(ctx context.Context, id int64, input domain.UpdateLessonInput) (domain.Lesson, error) {
|
func (s *Service) Update(ctx context.Context, id int64, input domain.UpdateLessonInput) (domain.Lesson, error) {
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,8 @@ func (h *Handler) ListLessonsByModule(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.lessonSvc.ListByModule(c.Context(), moduleID, int32(limit), int32(offset))
|
publishedOnly := !h.canManageLessons(c)
|
||||||
|
items, total, err := h.lessonSvc.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{
|
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
||||||
|
|
@ -145,6 +146,12 @@ func (h *Handler) GetLesson(c *fiber.Ctx) error {
|
||||||
Error: err.Error(),
|
Error: err.Error(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
if !les.VisibleToLearners() && !h.canManageLessons(c) {
|
||||||
|
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Lesson not found",
|
||||||
|
Error: lessons.ErrLessonNotFound.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
uid := c.Locals("user_id").(int64)
|
uid := c.Locals("user_id").(int64)
|
||||||
role := c.Locals("role").(domain.Role)
|
role := c.Locals("role").(domain.Role)
|
||||||
if err := h.lmsProgressSvc.ApplyAccessLesson(c.Context(), role, uid, &les); err != nil {
|
if err := h.lmsProgressSvc.ApplyAccessLesson(c.Context(), role, uid, &les); err != nil {
|
||||||
|
|
@ -265,7 +272,8 @@ func (h *Handler) CompleteLesson(c *fiber.Ctx) error {
|
||||||
Error: err.Error(),
|
Error: err.Error(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if _, err := h.lessonSvc.GetByID(c.Context(), id); err != nil {
|
les, err := h.lessonSvc.GetByID(c.Context(), id)
|
||||||
|
if err != nil {
|
||||||
if errors.Is(err, lessons.ErrLessonNotFound) {
|
if errors.Is(err, lessons.ErrLessonNotFound) {
|
||||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
||||||
Message: "Lesson not found",
|
Message: "Lesson not found",
|
||||||
|
|
@ -277,8 +285,14 @@ func (h *Handler) CompleteLesson(c *fiber.Ctx) error {
|
||||||
Error: err.Error(),
|
Error: err.Error(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
uid := c.Locals("user_id").(int64)
|
|
||||||
role := c.Locals("role").(domain.Role)
|
role := c.Locals("role").(domain.Role)
|
||||||
|
if role.IsCustomerLearnerRole() && !les.VisibleToLearners() {
|
||||||
|
return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Only published lessons can be completed",
|
||||||
|
Error: "LESSON_NOT_PUBLISHED",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
uid := c.Locals("user_id").(int64)
|
||||||
if role.UsesLMSSequentialGating() {
|
if role.UsesLMSSequentialGating() {
|
||||||
ok, reason, err := h.lmsProgressSvc.CanAccessLesson(c.Context(), uid, id)
|
ok, reason, err := h.lmsProgressSvc.CanAccessLesson(c.Context(), uid, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,11 @@ func (h *Handler) canManageLMSPractices(c *fiber.Ctx) bool {
|
||||||
return h.rbacSvc.HasPermission(rn, "practices.create") || h.rbacSvc.HasPermission(rn, "practices.update")
|
return h.rbacSvc.HasPermission(rn, "practices.create") || h.rbacSvc.HasPermission(rn, "practices.update")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) canManageLessons(c *fiber.Ctx) bool {
|
||||||
|
rn := string(c.Locals("role").(domain.Role))
|
||||||
|
return h.rbacSvc.HasPermission(rn, "lessons.create") || h.rbacSvc.HasPermission(rn, "lessons.update")
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Handler) canManageExamPrepPractices(c *fiber.Ctx) bool {
|
func (h *Handler) canManageExamPrepPractices(c *fiber.Ctx) bool {
|
||||||
rn := string(c.Locals("role").(domain.Role))
|
rn := string(c.Locals("role").(domain.Role))
|
||||||
return h.rbacSvc.HasPermission(rn, "exam_prep.practices.create") || h.rbacSvc.HasPermission(rn, "exam_prep.practices.update")
|
return h.rbacSvc.HasPermission(rn, "exam_prep.practices.create") || h.rbacSvc.HasPermission(rn, "exam_prep.practices.update")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user