diff --git a/db/query/exam_prep_progress.sql b/db/query/exam_prep_progress.sql new file mode 100644 index 0000000..e01eda8 --- /dev/null +++ b/db/query/exam_prep_progress.sql @@ -0,0 +1,119 @@ +-- name: CountPublishedExamPrepPracticesInLesson :one +SELECT + count(*)::int AS n +FROM + exam_prep.lesson_practices p + INNER JOIN question_sets qs ON qs.id = p.question_set_id +WHERE + p.unit_module_lesson_id = $1 + AND qs.set_type = 'PRACTICE' + AND qs.status = 'PUBLISHED' + AND p.publish_status = 'PUBLISHED'; + +-- name: CountUserCompletedPublishedExamPrepPracticesInLesson :one +SELECT + count(*)::int AS n +FROM + exam_prep.lesson_practices p + INNER JOIN question_sets qs ON qs.id = p.question_set_id + INNER JOIN user_practice_progress upp ON upp.question_set_id = p.question_set_id +WHERE + p.unit_module_lesson_id = $1 + AND upp.user_id = $2 + AND upp.completed_at IS NOT NULL + AND qs.set_type = 'PRACTICE' + AND qs.status = 'PUBLISHED' + AND p.publish_status = 'PUBLISHED'; + +-- name: CountPublishedExamPrepPracticesInModule :one +SELECT + count(*)::int AS n +FROM + exam_prep.lesson_practices p + INNER JOIN exam_prep.unit_module_lessons l ON l.id = p.unit_module_lesson_id + INNER JOIN question_sets qs ON qs.id = p.question_set_id +WHERE + l.unit_module_id = $1 + AND qs.set_type = 'PRACTICE' + AND qs.status = 'PUBLISHED' + AND p.publish_status = 'PUBLISHED'; + +-- name: CountUserCompletedPublishedExamPrepPracticesInModule :one +SELECT + count(*)::int AS n +FROM + exam_prep.lesson_practices p + INNER JOIN exam_prep.unit_module_lessons l ON l.id = p.unit_module_lesson_id + INNER JOIN question_sets qs ON qs.id = p.question_set_id + INNER JOIN user_practice_progress upp ON upp.question_set_id = p.question_set_id +WHERE + l.unit_module_id = $1 + AND upp.user_id = $2 + AND upp.completed_at IS NOT NULL + AND qs.set_type = 'PRACTICE' + AND qs.status = 'PUBLISHED' + AND p.publish_status = 'PUBLISHED'; + +-- name: CountPublishedExamPrepPracticesInUnit :one +SELECT + count(*)::int AS n +FROM + exam_prep.lesson_practices p + INNER JOIN exam_prep.unit_module_lessons l ON l.id = p.unit_module_lesson_id + INNER JOIN exam_prep.unit_modules m ON m.id = l.unit_module_id + INNER JOIN question_sets qs ON qs.id = p.question_set_id +WHERE + m.unit_id = $1 + AND qs.set_type = 'PRACTICE' + AND qs.status = 'PUBLISHED' + AND p.publish_status = 'PUBLISHED'; + +-- name: CountUserCompletedPublishedExamPrepPracticesInUnit :one +SELECT + count(*)::int AS n +FROM + exam_prep.lesson_practices p + INNER JOIN exam_prep.unit_module_lessons l ON l.id = p.unit_module_lesson_id + INNER JOIN exam_prep.unit_modules m ON m.id = l.unit_module_id + INNER JOIN question_sets qs ON qs.id = p.question_set_id + INNER JOIN user_practice_progress upp ON upp.question_set_id = p.question_set_id +WHERE + m.unit_id = $1 + AND upp.user_id = $2 + AND upp.completed_at IS NOT NULL + AND qs.set_type = 'PRACTICE' + AND qs.status = 'PUBLISHED' + AND p.publish_status = 'PUBLISHED'; + +-- name: CountPublishedExamPrepPracticesInCatalogCourse :one +SELECT + count(*)::int AS n +FROM + exam_prep.lesson_practices p + INNER JOIN exam_prep.unit_module_lessons l ON l.id = p.unit_module_lesson_id + INNER JOIN exam_prep.unit_modules m ON m.id = l.unit_module_id + INNER JOIN exam_prep.units u ON u.id = m.unit_id + INNER JOIN question_sets qs ON qs.id = p.question_set_id +WHERE + u.catalog_course_id = $1 + AND qs.set_type = 'PRACTICE' + AND qs.status = 'PUBLISHED' + AND p.publish_status = 'PUBLISHED'; + +-- name: CountUserCompletedPublishedExamPrepPracticesInCatalogCourse :one +SELECT + count(*)::int AS n +FROM + exam_prep.lesson_practices p + INNER JOIN exam_prep.unit_module_lessons l ON l.id = p.unit_module_lesson_id + INNER JOIN exam_prep.unit_modules m ON m.id = l.unit_module_id + INNER JOIN exam_prep.units u ON u.id = m.unit_id + INNER JOIN question_sets qs ON qs.id = p.question_set_id + INNER JOIN user_practice_progress upp ON upp.question_set_id = p.question_set_id +WHERE + u.catalog_course_id = $1 + AND upp.user_id = $2 + AND upp.completed_at IS NOT NULL + AND qs.set_type = 'PRACTICE' + AND qs.status = 'PUBLISHED' + AND p.publish_status = 'PUBLISHED'; diff --git a/db/query/lms_progress.sql b/db/query/lms_progress.sql index 8dd8d68..ddc70c6 100644 --- a/db/query/lms_progress.sql +++ b/db/query/lms_progress.sql @@ -225,72 +225,124 @@ ORDER BY -- name: ListLMSCompletedModuleIDsByUser :many SELECT - lp.module_id -FROM - lms_practices AS lp - INNER JOIN question_sets qs ON qs.id = lp.question_set_id - LEFT JOIN user_practice_progress upp ON upp.question_set_id = lp.question_set_id + scoped.module_id +FROM ( + SELECT + m.id AS module_id, + lp.question_set_id + FROM + modules m + INNER JOIN lms_practices lp ON ( + lp.module_id = m.id + OR lp.lesson_id IN ( + SELECT + id + FROM + lessons + WHERE + module_id = m.id)) + INNER JOIN question_sets qs ON qs.id = lp.question_set_id + WHERE + qs.set_type = 'PRACTICE' + AND qs.status = 'PUBLISHED' + AND lp.publish_status = 'PUBLISHED') scoped + LEFT JOIN user_practice_progress upp ON upp.question_set_id = scoped.question_set_id AND upp.user_id = $1 AND upp.completed_at IS NOT NULL -WHERE - lp.module_id IS NOT NULL - AND qs.set_type = 'PRACTICE' - AND qs.status = 'PUBLISHED' - AND lp.publish_status = 'PUBLISHED' GROUP BY - lp.module_id + scoped.module_id HAVING - count(DISTINCT lp.question_set_id) > 0 - AND count(DISTINCT upp.question_set_id) >= count(DISTINCT lp.question_set_id) + count(DISTINCT scoped.question_set_id) > 0 + AND count(DISTINCT upp.question_set_id) >= count(DISTINCT scoped.question_set_id) ORDER BY max(upp.completed_at) ASC, - lp.module_id ASC; + scoped.module_id ASC; -- name: ListLMSCompletedCourseIDsByUser :many SELECT - lp.course_id -FROM - lms_practices AS lp - INNER JOIN question_sets qs ON qs.id = lp.question_set_id - LEFT JOIN user_practice_progress upp ON upp.question_set_id = lp.question_set_id + scoped.course_id +FROM ( + SELECT + c.id AS course_id, + lp.question_set_id + FROM + courses c + INNER JOIN lms_practices lp ON ( + lp.course_id = c.id + OR lp.module_id IN ( + SELECT + id + FROM + modules + WHERE + course_id = c.id) + OR lp.lesson_id IN ( + SELECT + l.id + FROM + lessons l + INNER JOIN modules m ON m.id = l.module_id + WHERE + m.course_id = c.id)) + INNER JOIN question_sets qs ON qs.id = lp.question_set_id + WHERE + qs.set_type = 'PRACTICE' + AND qs.status = 'PUBLISHED' + AND lp.publish_status = 'PUBLISHED') scoped + LEFT JOIN user_practice_progress upp ON upp.question_set_id = scoped.question_set_id AND upp.user_id = $1 AND upp.completed_at IS NOT NULL -WHERE - lp.course_id IS NOT NULL - AND qs.set_type = 'PRACTICE' - AND qs.status = 'PUBLISHED' - AND lp.publish_status = 'PUBLISHED' GROUP BY - lp.course_id + scoped.course_id HAVING - count(DISTINCT lp.question_set_id) > 0 - AND count(DISTINCT upp.question_set_id) >= count(DISTINCT lp.question_set_id) + count(DISTINCT scoped.question_set_id) > 0 + AND count(DISTINCT upp.question_set_id) >= count(DISTINCT scoped.question_set_id) ORDER BY max(upp.completed_at) ASC, - lp.course_id ASC; + scoped.course_id ASC; -- name: ListLMSCompletedProgramIDsByUser :many SELECT - c.program_id -FROM - lms_practices AS lp - INNER JOIN courses c ON c.id = lp.course_id - INNER JOIN question_sets qs ON qs.id = lp.question_set_id - LEFT JOIN user_practice_progress upp ON upp.question_set_id = lp.question_set_id + scoped.program_id +FROM ( + SELECT + c.program_id, + lp.question_set_id + FROM + courses c + INNER JOIN lms_practices lp ON ( + lp.course_id = c.id + OR lp.module_id IN ( + SELECT + m.id + FROM + modules m + WHERE + m.course_id = c.id) + OR lp.lesson_id IN ( + SELECT + l.id + FROM + lessons l + INNER JOIN modules m ON m.id = l.module_id + WHERE + m.course_id = c.id)) + INNER JOIN question_sets qs ON qs.id = lp.question_set_id + WHERE + qs.set_type = 'PRACTICE' + AND qs.status = 'PUBLISHED' + AND lp.publish_status = 'PUBLISHED') scoped + LEFT JOIN user_practice_progress upp ON upp.question_set_id = scoped.question_set_id AND upp.user_id = $1 AND upp.completed_at IS NOT NULL -WHERE - qs.set_type = 'PRACTICE' - AND qs.status = 'PUBLISHED' - AND lp.publish_status = 'PUBLISHED' GROUP BY - c.program_id + scoped.program_id HAVING - count(DISTINCT lp.question_set_id) > 0 - AND count(DISTINCT upp.question_set_id) >= count(DISTINCT lp.question_set_id) + count(DISTINCT scoped.question_set_id) > 0 + AND count(DISTINCT upp.question_set_id) >= count(DISTINCT scoped.question_set_id) ORDER BY max(upp.completed_at) ASC, - c.program_id ASC; + scoped.program_id ASC; -- Lesson-based progress within a course (all modules). -- name: CountLessonsInCourse :one @@ -340,7 +392,7 @@ WHERE 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 (direct module practices and practices on lessons in the module). -- name: CountPublishedPracticesInModule :one SELECT count(*)::int AS n @@ -348,7 +400,15 @@ FROM lms_practices lp INNER JOIN question_sets qs ON qs.id = lp.question_set_id WHERE - lp.module_id = $1 + ( + lp.module_id = $1 + OR lp.lesson_id IN ( + SELECT + id + FROM + lessons + WHERE + module_id = $1)) AND qs.set_type = 'PRACTICE' AND qs.status = 'PUBLISHED' AND lp.publish_status = 'PUBLISHED'; @@ -361,7 +421,15 @@ FROM INNER JOIN question_sets qs ON qs.id = lp.question_set_id INNER JOIN user_practice_progress upp ON upp.question_set_id = lp.question_set_id WHERE - lp.module_id = $1 + ( + lp.module_id = $1 + OR lp.lesson_id IN ( + SELECT + id + FROM + lessons + WHERE + module_id = $1)) AND upp.user_id = $2 AND upp.completed_at IS NOT NULL AND qs.set_type = 'PRACTICE' @@ -375,7 +443,23 @@ FROM lms_practices lp INNER JOIN question_sets qs ON qs.id = lp.question_set_id WHERE - lp.course_id = $1 + ( + lp.course_id = $1 + OR lp.module_id IN ( + SELECT + id + FROM + modules + WHERE + course_id = $1) + OR lp.lesson_id IN ( + SELECT + l.id + FROM + lessons l + INNER JOIN modules m ON m.id = l.module_id + WHERE + m.course_id = $1)) AND qs.set_type = 'PRACTICE' AND qs.status = 'PUBLISHED' AND lp.publish_status = 'PUBLISHED'; @@ -388,21 +472,61 @@ FROM INNER JOIN question_sets qs ON qs.id = lp.question_set_id INNER JOIN user_practice_progress upp ON upp.question_set_id = lp.question_set_id WHERE - lp.course_id = $1 + ( + lp.course_id = $1 + OR lp.module_id IN ( + SELECT + id + FROM + modules + WHERE + course_id = $1) + OR lp.lesson_id IN ( + SELECT + l.id + FROM + lessons l + INNER JOIN modules m ON m.id = l.module_id + WHERE + m.course_id = $1)) 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: CountPublishedPracticesInProgram :one SELECT count(*)::int AS n FROM lms_practices lp - INNER JOIN courses c ON c.id = lp.course_id INNER JOIN question_sets qs ON qs.id = lp.question_set_id WHERE - c.program_id = $1 + ( + lp.course_id IN ( + SELECT + c.id + FROM + courses c + WHERE + c.program_id = $1) + OR lp.module_id IN ( + SELECT + m.id + FROM + modules m + INNER JOIN courses c ON c.id = m.course_id + WHERE + c.program_id = $1) + OR lp.lesson_id IN ( + SELECT + l.id + FROM + lessons l + INNER JOIN modules m ON m.id = l.module_id + INNER JOIN courses c ON c.id = m.course_id + WHERE + c.program_id = $1)) AND qs.set_type = 'PRACTICE' AND qs.status = 'PUBLISHED' AND lp.publish_status = 'PUBLISHED'; @@ -412,11 +536,34 @@ SELECT count(*)::int AS n FROM lms_practices lp - INNER JOIN courses c ON c.id = lp.course_id INNER JOIN question_sets qs ON qs.id = lp.question_set_id INNER JOIN user_practice_progress upp ON upp.question_set_id = lp.question_set_id WHERE - c.program_id = $1 + ( + lp.course_id IN ( + SELECT + c.id + FROM + courses c + WHERE + c.program_id = $1) + OR lp.module_id IN ( + SELECT + m.id + FROM + modules m + INNER JOIN courses c ON c.id = m.course_id + WHERE + c.program_id = $1) + OR lp.lesson_id IN ( + SELECT + l.id + FROM + lessons l + INNER JOIN modules m ON m.id = l.module_id + INNER JOIN courses c ON c.id = m.course_id + WHERE + c.program_id = $1)) AND upp.user_id = $2 AND upp.completed_at IS NOT NULL AND qs.set_type = 'PRACTICE' diff --git a/gen/db/exam_prep_progress.sql.go b/gen/db/exam_prep_progress.sql.go new file mode 100644 index 0000000..a2957e0 --- /dev/null +++ b/gen/db/exam_prep_progress.sql.go @@ -0,0 +1,214 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: exam_prep_progress.sql + +package dbgen + +import ( + "context" +) + +const CountPublishedExamPrepPracticesInCatalogCourse = `-- name: CountPublishedExamPrepPracticesInCatalogCourse :one +SELECT + count(*)::int AS n +FROM + exam_prep.lesson_practices p + INNER JOIN exam_prep.unit_module_lessons l ON l.id = p.unit_module_lesson_id + INNER JOIN exam_prep.unit_modules m ON m.id = l.unit_module_id + INNER JOIN exam_prep.units u ON u.id = m.unit_id + INNER JOIN question_sets qs ON qs.id = p.question_set_id +WHERE + u.catalog_course_id = $1 + AND qs.set_type = 'PRACTICE' + AND qs.status = 'PUBLISHED' + AND p.publish_status = 'PUBLISHED' +` + +func (q *Queries) CountPublishedExamPrepPracticesInCatalogCourse(ctx context.Context, catalogCourseID int64) (int32, error) { + row := q.db.QueryRow(ctx, CountPublishedExamPrepPracticesInCatalogCourse, catalogCourseID) + var n int32 + err := row.Scan(&n) + return n, err +} + +const CountPublishedExamPrepPracticesInLesson = `-- name: CountPublishedExamPrepPracticesInLesson :one +SELECT + count(*)::int AS n +FROM + exam_prep.lesson_practices p + INNER JOIN question_sets qs ON qs.id = p.question_set_id +WHERE + p.unit_module_lesson_id = $1 + AND qs.set_type = 'PRACTICE' + AND qs.status = 'PUBLISHED' + AND p.publish_status = 'PUBLISHED' +` + +func (q *Queries) CountPublishedExamPrepPracticesInLesson(ctx context.Context, unitModuleLessonID int64) (int32, error) { + row := q.db.QueryRow(ctx, CountPublishedExamPrepPracticesInLesson, unitModuleLessonID) + var n int32 + err := row.Scan(&n) + return n, err +} + +const CountPublishedExamPrepPracticesInModule = `-- name: CountPublishedExamPrepPracticesInModule :one +SELECT + count(*)::int AS n +FROM + exam_prep.lesson_practices p + INNER JOIN exam_prep.unit_module_lessons l ON l.id = p.unit_module_lesson_id + INNER JOIN question_sets qs ON qs.id = p.question_set_id +WHERE + l.unit_module_id = $1 + AND qs.set_type = 'PRACTICE' + AND qs.status = 'PUBLISHED' + AND p.publish_status = 'PUBLISHED' +` + +func (q *Queries) CountPublishedExamPrepPracticesInModule(ctx context.Context, unitModuleID int64) (int32, error) { + row := q.db.QueryRow(ctx, CountPublishedExamPrepPracticesInModule, unitModuleID) + var n int32 + err := row.Scan(&n) + return n, err +} + +const CountPublishedExamPrepPracticesInUnit = `-- name: CountPublishedExamPrepPracticesInUnit :one +SELECT + count(*)::int AS n +FROM + exam_prep.lesson_practices p + INNER JOIN exam_prep.unit_module_lessons l ON l.id = p.unit_module_lesson_id + INNER JOIN exam_prep.unit_modules m ON m.id = l.unit_module_id + INNER JOIN question_sets qs ON qs.id = p.question_set_id +WHERE + m.unit_id = $1 + AND qs.set_type = 'PRACTICE' + AND qs.status = 'PUBLISHED' + AND p.publish_status = 'PUBLISHED' +` + +func (q *Queries) CountPublishedExamPrepPracticesInUnit(ctx context.Context, unitID int64) (int32, error) { + row := q.db.QueryRow(ctx, CountPublishedExamPrepPracticesInUnit, unitID) + var n int32 + err := row.Scan(&n) + return n, err +} + +const CountUserCompletedPublishedExamPrepPracticesInCatalogCourse = `-- name: CountUserCompletedPublishedExamPrepPracticesInCatalogCourse :one +SELECT + count(*)::int AS n +FROM + exam_prep.lesson_practices p + INNER JOIN exam_prep.unit_module_lessons l ON l.id = p.unit_module_lesson_id + INNER JOIN exam_prep.unit_modules m ON m.id = l.unit_module_id + INNER JOIN exam_prep.units u ON u.id = m.unit_id + INNER JOIN question_sets qs ON qs.id = p.question_set_id + INNER JOIN user_practice_progress upp ON upp.question_set_id = p.question_set_id +WHERE + u.catalog_course_id = $1 + AND upp.user_id = $2 + AND upp.completed_at IS NOT NULL + AND qs.set_type = 'PRACTICE' + AND qs.status = 'PUBLISHED' + AND p.publish_status = 'PUBLISHED' +` + +type CountUserCompletedPublishedExamPrepPracticesInCatalogCourseParams struct { + CatalogCourseID int64 `json:"catalog_course_id"` + UserID int64 `json:"user_id"` +} + +func (q *Queries) CountUserCompletedPublishedExamPrepPracticesInCatalogCourse(ctx context.Context, arg CountUserCompletedPublishedExamPrepPracticesInCatalogCourseParams) (int32, error) { + row := q.db.QueryRow(ctx, CountUserCompletedPublishedExamPrepPracticesInCatalogCourse, arg.CatalogCourseID, arg.UserID) + var n int32 + err := row.Scan(&n) + return n, err +} + +const CountUserCompletedPublishedExamPrepPracticesInLesson = `-- name: CountUserCompletedPublishedExamPrepPracticesInLesson :one +SELECT + count(*)::int AS n +FROM + exam_prep.lesson_practices p + INNER JOIN question_sets qs ON qs.id = p.question_set_id + INNER JOIN user_practice_progress upp ON upp.question_set_id = p.question_set_id +WHERE + p.unit_module_lesson_id = $1 + AND upp.user_id = $2 + AND upp.completed_at IS NOT NULL + AND qs.set_type = 'PRACTICE' + AND qs.status = 'PUBLISHED' + AND p.publish_status = 'PUBLISHED' +` + +type CountUserCompletedPublishedExamPrepPracticesInLessonParams struct { + UnitModuleLessonID int64 `json:"unit_module_lesson_id"` + UserID int64 `json:"user_id"` +} + +func (q *Queries) CountUserCompletedPublishedExamPrepPracticesInLesson(ctx context.Context, arg CountUserCompletedPublishedExamPrepPracticesInLessonParams) (int32, error) { + row := q.db.QueryRow(ctx, CountUserCompletedPublishedExamPrepPracticesInLesson, arg.UnitModuleLessonID, arg.UserID) + var n int32 + err := row.Scan(&n) + return n, err +} + +const CountUserCompletedPublishedExamPrepPracticesInModule = `-- name: CountUserCompletedPublishedExamPrepPracticesInModule :one +SELECT + count(*)::int AS n +FROM + exam_prep.lesson_practices p + INNER JOIN exam_prep.unit_module_lessons l ON l.id = p.unit_module_lesson_id + INNER JOIN question_sets qs ON qs.id = p.question_set_id + INNER JOIN user_practice_progress upp ON upp.question_set_id = p.question_set_id +WHERE + l.unit_module_id = $1 + AND upp.user_id = $2 + AND upp.completed_at IS NOT NULL + AND qs.set_type = 'PRACTICE' + AND qs.status = 'PUBLISHED' + AND p.publish_status = 'PUBLISHED' +` + +type CountUserCompletedPublishedExamPrepPracticesInModuleParams struct { + UnitModuleID int64 `json:"unit_module_id"` + UserID int64 `json:"user_id"` +} + +func (q *Queries) CountUserCompletedPublishedExamPrepPracticesInModule(ctx context.Context, arg CountUserCompletedPublishedExamPrepPracticesInModuleParams) (int32, error) { + row := q.db.QueryRow(ctx, CountUserCompletedPublishedExamPrepPracticesInModule, arg.UnitModuleID, arg.UserID) + var n int32 + err := row.Scan(&n) + return n, err +} + +const CountUserCompletedPublishedExamPrepPracticesInUnit = `-- name: CountUserCompletedPublishedExamPrepPracticesInUnit :one +SELECT + count(*)::int AS n +FROM + exam_prep.lesson_practices p + INNER JOIN exam_prep.unit_module_lessons l ON l.id = p.unit_module_lesson_id + INNER JOIN exam_prep.unit_modules m ON m.id = l.unit_module_id + INNER JOIN question_sets qs ON qs.id = p.question_set_id + INNER JOIN user_practice_progress upp ON upp.question_set_id = p.question_set_id +WHERE + m.unit_id = $1 + AND upp.user_id = $2 + AND upp.completed_at IS NOT NULL + AND qs.set_type = 'PRACTICE' + AND qs.status = 'PUBLISHED' + AND p.publish_status = 'PUBLISHED' +` + +type CountUserCompletedPublishedExamPrepPracticesInUnitParams struct { + UnitID int64 `json:"unit_id"` + UserID int64 `json:"user_id"` +} + +func (q *Queries) CountUserCompletedPublishedExamPrepPracticesInUnit(ctx context.Context, arg CountUserCompletedPublishedExamPrepPracticesInUnitParams) (int32, error) { + row := q.db.QueryRow(ctx, CountUserCompletedPublishedExamPrepPracticesInUnit, arg.UnitID, arg.UserID) + var n int32 + err := row.Scan(&n) + return n, err +} diff --git a/gen/db/lms_progress.sql.go b/gen/db/lms_progress.sql.go index b033024..08b5133 100644 --- a/gen/db/lms_progress.sql.go +++ b/gen/db/lms_progress.sql.go @@ -106,7 +106,23 @@ FROM lms_practices lp INNER JOIN question_sets qs ON qs.id = lp.question_set_id WHERE - lp.course_id = $1 + ( + lp.course_id = $1 + OR lp.module_id IN ( + SELECT + id + FROM + modules + WHERE + course_id = $1) + OR lp.lesson_id IN ( + SELECT + l.id + FROM + lessons l + INNER JOIN modules m ON m.id = l.module_id + WHERE + m.course_id = $1)) AND qs.set_type = 'PRACTICE' AND qs.status = 'PUBLISHED' AND lp.publish_status = 'PUBLISHED' @@ -146,13 +162,21 @@ FROM lms_practices lp INNER JOIN question_sets qs ON qs.id = lp.question_set_id WHERE - lp.module_id = $1 + ( + lp.module_id = $1 + OR lp.lesson_id IN ( + SELECT + id + FROM + lessons + WHERE + 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). +// Published practices in a module (direct module practices and practices on lessons in the module). func (q *Queries) CountPublishedPracticesInModule(ctx context.Context, moduleID pgtype.Int8) (int32, error) { row := q.db.QueryRow(ctx, CountPublishedPracticesInModule, moduleID) var n int32 @@ -165,10 +189,33 @@ SELECT count(*)::int AS n FROM lms_practices lp - INNER JOIN courses c ON c.id = lp.course_id INNER JOIN question_sets qs ON qs.id = lp.question_set_id WHERE - c.program_id = $1 + ( + lp.course_id IN ( + SELECT + c.id + FROM + courses c + WHERE + c.program_id = $1) + OR lp.module_id IN ( + SELECT + m.id + FROM + modules m + INNER JOIN courses c ON c.id = m.course_id + WHERE + c.program_id = $1) + OR lp.lesson_id IN ( + SELECT + l.id + FROM + lessons l + INNER JOIN modules m ON m.id = l.module_id + INNER JOIN courses c ON c.id = m.course_id + WHERE + c.program_id = $1)) AND qs.set_type = 'PRACTICE' AND qs.status = 'PUBLISHED' AND lp.publish_status = 'PUBLISHED' @@ -310,11 +357,28 @@ FROM INNER JOIN question_sets qs ON qs.id = lp.question_set_id INNER JOIN user_practice_progress upp ON upp.question_set_id = lp.question_set_id WHERE - lp.course_id = $1 + ( + lp.course_id = $1 + OR lp.module_id IN ( + SELECT + id + FROM + modules + WHERE + course_id = $1) + OR lp.lesson_id IN ( + SELECT + l.id + FROM + lessons l + INNER JOIN modules m ON m.id = l.module_id + WHERE + m.course_id = $1)) AND upp.user_id = $2 AND upp.completed_at IS NOT NULL AND qs.set_type = 'PRACTICE' AND qs.status = 'PUBLISHED' + AND lp.publish_status = 'PUBLISHED' ` type CountUserCompletedPublishedPracticesInCourseParams struct { @@ -365,7 +429,15 @@ FROM INNER JOIN question_sets qs ON qs.id = lp.question_set_id INNER JOIN user_practice_progress upp ON upp.question_set_id = lp.question_set_id WHERE - lp.module_id = $1 + ( + lp.module_id = $1 + OR lp.lesson_id IN ( + SELECT + id + FROM + lessons + WHERE + module_id = $1)) AND upp.user_id = $2 AND upp.completed_at IS NOT NULL AND qs.set_type = 'PRACTICE' @@ -390,11 +462,34 @@ SELECT count(*)::int AS n FROM lms_practices lp - INNER JOIN courses c ON c.id = lp.course_id INNER JOIN question_sets qs ON qs.id = lp.question_set_id INNER JOIN user_practice_progress upp ON upp.question_set_id = lp.question_set_id WHERE - c.program_id = $1 + ( + lp.course_id IN ( + SELECT + c.id + FROM + courses c + WHERE + c.program_id = $1) + OR lp.module_id IN ( + SELECT + m.id + FROM + modules m + INNER JOIN courses c ON c.id = m.course_id + WHERE + c.program_id = $1) + OR lp.lesson_id IN ( + SELECT + l.id + FROM + lessons l + INNER JOIN modules m ON m.id = l.module_id + INNER JOIN courses c ON c.id = m.course_id + WHERE + c.program_id = $1)) AND upp.user_id = $2 AND upp.completed_at IS NOT NULL AND qs.set_type = 'PRACTICE' @@ -640,37 +735,57 @@ func (q *Queries) InsertUserProgramProgress(ctx context.Context, arg InsertUserP const ListLMSCompletedCourseIDsByUser = `-- name: ListLMSCompletedCourseIDsByUser :many SELECT - lp.course_id -FROM - lms_practices AS lp - INNER JOIN question_sets qs ON qs.id = lp.question_set_id - LEFT JOIN user_practice_progress upp ON upp.question_set_id = lp.question_set_id + scoped.course_id +FROM ( + SELECT + c.id AS course_id, + lp.question_set_id + FROM + courses c + INNER JOIN lms_practices lp ON ( + lp.course_id = c.id + OR lp.module_id IN ( + SELECT + id + FROM + modules + WHERE + course_id = c.id) + OR lp.lesson_id IN ( + SELECT + l.id + FROM + lessons l + INNER JOIN modules m ON m.id = l.module_id + WHERE + m.course_id = c.id)) + INNER JOIN question_sets qs ON qs.id = lp.question_set_id + WHERE + qs.set_type = 'PRACTICE' + AND qs.status = 'PUBLISHED' + AND lp.publish_status = 'PUBLISHED') scoped + LEFT JOIN user_practice_progress upp ON upp.question_set_id = scoped.question_set_id AND upp.user_id = $1 AND upp.completed_at IS NOT NULL -WHERE - lp.course_id IS NOT NULL - AND qs.set_type = 'PRACTICE' - AND qs.status = 'PUBLISHED' - AND lp.publish_status = 'PUBLISHED' GROUP BY - lp.course_id + scoped.course_id HAVING - count(DISTINCT lp.question_set_id) > 0 - AND count(DISTINCT upp.question_set_id) >= count(DISTINCT lp.question_set_id) + count(DISTINCT scoped.question_set_id) > 0 + AND count(DISTINCT upp.question_set_id) >= count(DISTINCT scoped.question_set_id) ORDER BY max(upp.completed_at) ASC, - lp.course_id ASC + scoped.course_id ASC ` -func (q *Queries) ListLMSCompletedCourseIDsByUser(ctx context.Context, userID int64) ([]pgtype.Int8, error) { +func (q *Queries) ListLMSCompletedCourseIDsByUser(ctx context.Context, userID int64) ([]int64, error) { rows, err := q.db.Query(ctx, ListLMSCompletedCourseIDsByUser, userID) if err != nil { return nil, err } defer rows.Close() - var items []pgtype.Int8 + var items []int64 for rows.Next() { - var course_id pgtype.Int8 + var course_id int64 if err := rows.Scan(&course_id); err != nil { return nil, err } @@ -728,37 +843,49 @@ func (q *Queries) ListLMSCompletedLessonIDsByUser(ctx context.Context, userID in const ListLMSCompletedModuleIDsByUser = `-- name: ListLMSCompletedModuleIDsByUser :many SELECT - lp.module_id -FROM - lms_practices AS lp - INNER JOIN question_sets qs ON qs.id = lp.question_set_id - LEFT JOIN user_practice_progress upp ON upp.question_set_id = lp.question_set_id + scoped.module_id +FROM ( + SELECT + m.id AS module_id, + lp.question_set_id + FROM + modules m + INNER JOIN lms_practices lp ON ( + lp.module_id = m.id + OR lp.lesson_id IN ( + SELECT + id + FROM + lessons + WHERE + module_id = m.id)) + INNER JOIN question_sets qs ON qs.id = lp.question_set_id + WHERE + qs.set_type = 'PRACTICE' + AND qs.status = 'PUBLISHED' + AND lp.publish_status = 'PUBLISHED') scoped + LEFT JOIN user_practice_progress upp ON upp.question_set_id = scoped.question_set_id AND upp.user_id = $1 AND upp.completed_at IS NOT NULL -WHERE - lp.module_id IS NOT NULL - AND qs.set_type = 'PRACTICE' - AND qs.status = 'PUBLISHED' - AND lp.publish_status = 'PUBLISHED' GROUP BY - lp.module_id + scoped.module_id HAVING - count(DISTINCT lp.question_set_id) > 0 - AND count(DISTINCT upp.question_set_id) >= count(DISTINCT lp.question_set_id) + count(DISTINCT scoped.question_set_id) > 0 + AND count(DISTINCT upp.question_set_id) >= count(DISTINCT scoped.question_set_id) ORDER BY max(upp.completed_at) ASC, - lp.module_id ASC + scoped.module_id ASC ` -func (q *Queries) ListLMSCompletedModuleIDsByUser(ctx context.Context, userID int64) ([]pgtype.Int8, error) { +func (q *Queries) ListLMSCompletedModuleIDsByUser(ctx context.Context, userID int64) ([]int64, error) { rows, err := q.db.Query(ctx, ListLMSCompletedModuleIDsByUser, userID) if err != nil { return nil, err } defer rows.Close() - var items []pgtype.Int8 + var items []int64 for rows.Next() { - var module_id pgtype.Int8 + var module_id int64 if err := rows.Scan(&module_id); err != nil { return nil, err } @@ -772,26 +899,46 @@ func (q *Queries) ListLMSCompletedModuleIDsByUser(ctx context.Context, userID in const ListLMSCompletedProgramIDsByUser = `-- name: ListLMSCompletedProgramIDsByUser :many SELECT - c.program_id -FROM - lms_practices AS lp - INNER JOIN courses c ON c.id = lp.course_id - INNER JOIN question_sets qs ON qs.id = lp.question_set_id - LEFT JOIN user_practice_progress upp ON upp.question_set_id = lp.question_set_id + scoped.program_id +FROM ( + SELECT + c.program_id, + lp.question_set_id + FROM + courses c + INNER JOIN lms_practices lp ON ( + lp.course_id = c.id + OR lp.module_id IN ( + SELECT + m.id + FROM + modules m + WHERE + m.course_id = c.id) + OR lp.lesson_id IN ( + SELECT + l.id + FROM + lessons l + INNER JOIN modules m ON m.id = l.module_id + WHERE + m.course_id = c.id)) + INNER JOIN question_sets qs ON qs.id = lp.question_set_id + WHERE + qs.set_type = 'PRACTICE' + AND qs.status = 'PUBLISHED' + AND lp.publish_status = 'PUBLISHED') scoped + LEFT JOIN user_practice_progress upp ON upp.question_set_id = scoped.question_set_id AND upp.user_id = $1 AND upp.completed_at IS NOT NULL -WHERE - qs.set_type = 'PRACTICE' - AND qs.status = 'PUBLISHED' - AND lp.publish_status = 'PUBLISHED' GROUP BY - c.program_id + scoped.program_id HAVING - count(DISTINCT lp.question_set_id) > 0 - AND count(DISTINCT upp.question_set_id) >= count(DISTINCT lp.question_set_id) + count(DISTINCT scoped.question_set_id) > 0 + AND count(DISTINCT upp.question_set_id) >= count(DISTINCT scoped.question_set_id) ORDER BY max(upp.completed_at) ASC, - c.program_id ASC + scoped.program_id ASC ` func (q *Queries) ListLMSCompletedProgramIDsByUser(ctx context.Context, userID int64) ([]int64, error) { diff --git a/internal/domain/exam_prep_catalog_course.go b/internal/domain/exam_prep_catalog_course.go index cfc7f3b..99f93a2 100644 --- a/internal/domain/exam_prep_catalog_course.go +++ b/internal/domain/exam_prep_catalog_course.go @@ -13,8 +13,9 @@ type ExamPrepCatalogCourse struct { UnitsCount *int64 `json:"units_count,omitempty"` ModulesCount *int64 `json:"modules_count,omitempty"` LessonsCount *int64 `json:"lessons_count,omitempty"` - HasPractice bool `json:"has_practice"` - CreatedAt time.Time `json:"created_at"` + HasPractice bool `json:"has_practice"` + Access *LMSEntityAccess `json:"access,omitempty"` + CreatedAt time.Time `json:"created_at"` UpdatedAt *time.Time `json:"updated_at,omitempty"` } diff --git a/internal/domain/exam_prep_lesson.go b/internal/domain/exam_prep_lesson.go index 841b7ab..30f0a11 100644 --- a/internal/domain/exam_prep_lesson.go +++ b/internal/domain/exam_prep_lesson.go @@ -11,8 +11,9 @@ type ExamPrepLesson struct { Thumbnail *string `json:"thumbnail,omitempty"` Description *string `json:"description,omitempty"` SortOrder int `json:"sort_order"` - HasPractice bool `json:"has_practice"` - CreatedAt time.Time `json:"created_at"` + HasPractice bool `json:"has_practice"` + Access *LMSEntityAccess `json:"access,omitempty"` + CreatedAt time.Time `json:"created_at"` UpdatedAt *time.Time `json:"updated_at,omitempty"` } diff --git a/internal/domain/exam_prep_module.go b/internal/domain/exam_prep_module.go index 0b12a03..81bf1ab 100644 --- a/internal/domain/exam_prep_module.go +++ b/internal/domain/exam_prep_module.go @@ -13,8 +13,9 @@ type ExamPrepModule struct { SortOrder int `json:"sort_order"` LessonsCount *int64 `json:"lessons_count,omitempty"` PracticesCount *int64 `json:"practices_count,omitempty"` - HasPractice bool `json:"has_practice"` - CreatedAt time.Time `json:"created_at"` + HasPractice bool `json:"has_practice"` + Access *LMSEntityAccess `json:"access,omitempty"` + CreatedAt time.Time `json:"created_at"` UpdatedAt *time.Time `json:"updated_at,omitempty"` } diff --git a/internal/domain/exam_prep_unit.go b/internal/domain/exam_prep_unit.go index 07e2cc2..31180c5 100644 --- a/internal/domain/exam_prep_unit.go +++ b/internal/domain/exam_prep_unit.go @@ -13,8 +13,9 @@ type ExamPrepUnit struct { ModulesCount *int64 `json:"modules_count,omitempty"` LessonsCount *int64 `json:"lessons_count,omitempty"` PracticesCount *int64 `json:"practices_count,omitempty"` - HasPractice bool `json:"has_practice"` - CreatedAt time.Time `json:"created_at"` + HasPractice bool `json:"has_practice"` + Access *LMSEntityAccess `json:"access,omitempty"` + CreatedAt time.Time `json:"created_at"` UpdatedAt *time.Time `json:"updated_at,omitempty"` } diff --git a/internal/repository/exam_prep_progress.go b/internal/repository/exam_prep_progress.go new file mode 100644 index 0000000..247179f --- /dev/null +++ b/internal/repository/exam_prep_progress.go @@ -0,0 +1,71 @@ +package repository + +import ( + "context" + + dbgen "Yimaru-Backend/gen/db" +) + +// ExamPrepUserPracticeProgressInLesson returns published practice completion counts scoped to an exam-prep lesson. +func (s *Store) ExamPrepUserPracticeProgressInLesson(ctx context.Context, userID, lessonID int64) (completed, total int32, err error) { + total, err = s.queries.CountPublishedExamPrepPracticesInLesson(ctx, lessonID) + if err != nil { + return 0, 0, err + } + completed, err = s.queries.CountUserCompletedPublishedExamPrepPracticesInLesson(ctx, dbgen.CountUserCompletedPublishedExamPrepPracticesInLessonParams{ + UnitModuleLessonID: lessonID, + UserID: userID, + }) + if err != nil { + return 0, 0, err + } + return completed, total, nil +} + +// ExamPrepUserPracticeProgressInModule returns published practice completion counts in an exam-prep module. +func (s *Store) ExamPrepUserPracticeProgressInModule(ctx context.Context, userID, moduleID int64) (completed, total int32, err error) { + total, err = s.queries.CountPublishedExamPrepPracticesInModule(ctx, moduleID) + if err != nil { + return 0, 0, err + } + completed, err = s.queries.CountUserCompletedPublishedExamPrepPracticesInModule(ctx, dbgen.CountUserCompletedPublishedExamPrepPracticesInModuleParams{ + UnitModuleID: moduleID, + UserID: userID, + }) + if err != nil { + return 0, 0, err + } + return completed, total, nil +} + +// ExamPrepUserPracticeProgressInUnit returns published practice completion counts in an exam-prep unit. +func (s *Store) ExamPrepUserPracticeProgressInUnit(ctx context.Context, userID, unitID int64) (completed, total int32, err error) { + total, err = s.queries.CountPublishedExamPrepPracticesInUnit(ctx, unitID) + if err != nil { + return 0, 0, err + } + completed, err = s.queries.CountUserCompletedPublishedExamPrepPracticesInUnit(ctx, dbgen.CountUserCompletedPublishedExamPrepPracticesInUnitParams{ + UnitID: unitID, + UserID: userID, + }) + if err != nil { + return 0, 0, err + } + return completed, total, nil +} + +// ExamPrepUserPracticeProgressInCatalogCourse returns published practice completion counts in a catalog course. +func (s *Store) ExamPrepUserPracticeProgressInCatalogCourse(ctx context.Context, userID, catalogCourseID int64) (completed, total int32, err error) { + total, err = s.queries.CountPublishedExamPrepPracticesInCatalogCourse(ctx, catalogCourseID) + if err != nil { + return 0, 0, err + } + completed, err = s.queries.CountUserCompletedPublishedExamPrepPracticesInCatalogCourse(ctx, dbgen.CountUserCompletedPublishedExamPrepPracticesInCatalogCourseParams{ + CatalogCourseID: catalogCourseID, + UserID: userID, + }) + if err != nil { + return 0, 0, err + } + return completed, total, nil +} diff --git a/internal/repository/lms_progress_tx.go b/internal/repository/lms_progress_tx.go index 2a582df..bcde9bf 100644 --- a/internal/repository/lms_progress_tx.go +++ b/internal/repository/lms_progress_tx.go @@ -2,9 +2,11 @@ package repository import ( "context" + "errors" "fmt" dbgen "Yimaru-Backend/gen/db" + "github.com/jackc/pgx/v5" ) // CompleteLessonForUser records lesson completion for sequential lesson gating and @@ -60,6 +62,13 @@ func (s *Store) CompletePracticeForUser(ctx context.Context, userID, questionSet scope, err := q.GetPracticeScopeByQuestionSetID(ctx, questionSetID) if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + // Exam-prep practices are not in lms_practices; completion is tracked in user_practice_progress only. + if err := tx.Commit(ctx); err != nil { + return fmt.Errorf("commit: %w", err) + } + return nil + } return err } var ( diff --git a/internal/repository/lms_user_progress_snapshot.go b/internal/repository/lms_user_progress_snapshot.go index 8c833d6..e3aca32 100644 --- a/internal/repository/lms_user_progress_snapshot.go +++ b/internal/repository/lms_user_progress_snapshot.go @@ -29,8 +29,8 @@ func (s *Store) GetLMSUserProgressSnapshot(ctx context.Context, userID int64) (d } return domain.LMSUserProgress{ LessonIDs: pgInt8IDsToInt64(lessons), - ModuleIDs: pgInt8IDsToInt64(mods), - CourseIDs: pgInt8IDsToInt64(courses), + ModuleIDs: int64IDsOrEmpty(mods), + CourseIDs: int64IDsOrEmpty(courses), ProgramIDs: int64IDsOrEmpty(programs), }, nil } diff --git a/internal/services/lmsprogress/service.go b/internal/services/lmsprogress/service.go index 039a2f2..d8e1033 100644 --- a/internal/services/lmsprogress/service.go +++ b/internal/services/lmsprogress/service.go @@ -234,6 +234,66 @@ func (s *Service) ApplyAccessLesson(ctx context.Context, role domain.Role, userI return nil } +// ApplyExamPrepAccessCatalogCourse sets progress on an exam-prep catalog course for learner roles. +func (s *Service) ApplyExamPrepAccessCatalogCourse(ctx context.Context, role domain.Role, userID int64, cc *domain.ExamPrepCatalogCourse) error { + if !role.IsCustomerLearnerRole() { + cc.Access = nil + return nil + } + comp, tot, err := s.store.ExamPrepUserPracticeProgressInCatalogCourse(ctx, userID, cc.ID) + if err != nil { + return err + } + done := lmsProgressComplete(comp, tot) + cc.Access = buildLMSEntityAccess(true, "", done, comp, tot) + return nil +} + +// ApplyExamPrepAccessUnit sets progress on an exam-prep unit for learner roles. +func (s *Service) ApplyExamPrepAccessUnit(ctx context.Context, role domain.Role, userID int64, u *domain.ExamPrepUnit) error { + if !role.IsCustomerLearnerRole() { + u.Access = nil + return nil + } + comp, tot, err := s.store.ExamPrepUserPracticeProgressInUnit(ctx, userID, u.ID) + if err != nil { + return err + } + done := lmsProgressComplete(comp, tot) + u.Access = buildLMSEntityAccess(true, "", done, comp, tot) + return nil +} + +// ApplyExamPrepAccessModule sets progress on an exam-prep module for learner roles. +func (s *Service) ApplyExamPrepAccessModule(ctx context.Context, role domain.Role, userID int64, m *domain.ExamPrepModule) error { + if !role.IsCustomerLearnerRole() { + m.Access = nil + return nil + } + comp, tot, err := s.store.ExamPrepUserPracticeProgressInModule(ctx, userID, m.ID) + if err != nil { + return err + } + done := lmsProgressComplete(comp, tot) + m.Access = buildLMSEntityAccess(true, "", done, comp, tot) + return nil +} + +// ApplyExamPrepAccessLesson sets progress on an exam-prep lesson for learner roles. +func (s *Service) ApplyExamPrepAccessLesson(ctx context.Context, role domain.Role, userID int64, les *domain.ExamPrepLesson) error { + if !role.IsCustomerLearnerRole() { + les.Access = nil + return nil + } + comp, tot, err := s.store.ExamPrepUserPracticeProgressInLesson(ctx, userID, les.ID) + if err != nil { + return err + } + done := lmsProgressComplete(comp, tot) + les.Access = buildLMSEntityAccess(true, "", done, comp, tot) + return nil +} + func lmsProgressComplete(completed, total int32) bool { return total > 0 && completed >= total } diff --git a/internal/web_server/handlers/exam_prep_catalog_course_handler.go b/internal/web_server/handlers/exam_prep_catalog_course_handler.go index 467282b..38a8147 100644 --- a/internal/web_server/handlers/exam_prep_catalog_course_handler.go +++ b/internal/web_server/handlers/exam_prep_catalog_course_handler.go @@ -116,10 +116,18 @@ func (h *Handler) ListExamPrepCatalogCourses(c *fiber.Ctx) error { end = total } + page := filtered[start:end] + if err := h.applyExamPrepAccessCatalogCourses(c.Context(), c, page); err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to build catalog course list", + Error: err.Error(), + }) + } + return c.JSON(domain.Response{ Message: "Catalog courses retrieved successfully", Data: fiber.Map{ - "catalog_courses": filtered[start:end], + "catalog_courses": page, "total_count": total, "limit": limit, "offset": offset, @@ -136,6 +144,12 @@ func (h *Handler) ListExamPrepCatalogCourses(c *fiber.Ctx) error { Error: err.Error(), }) } + if err := h.applyExamPrepAccessCatalogCourses(c.Context(), c, items); err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to build catalog course list", + Error: err.Error(), + }) + } return c.JSON(domain.Response{ Message: "Catalog courses retrieved successfully", Data: fiber.Map{ @@ -218,6 +232,12 @@ func (h *Handler) GetExamPrepCatalogCourseByID(c *fiber.Ctx) error { Error: err.Error(), }) } + if err := h.applyExamPrepAccessCatalogCourse(c.Context(), c, &out); err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to build catalog course", + Error: err.Error(), + }) + } return c.JSON(domain.Response{ Message: "Catalog course retrieved successfully", Data: out, diff --git a/internal/web_server/handlers/exam_prep_lesson_handler.go b/internal/web_server/handlers/exam_prep_lesson_handler.go index 4ff669b..41540d1 100644 --- a/internal/web_server/handlers/exam_prep_lesson_handler.go +++ b/internal/web_server/handlers/exam_prep_lesson_handler.go @@ -87,6 +87,12 @@ func (h *Handler) ListExamPrepLessonsByUnitModule(c *fiber.Ctx) error { Error: err.Error(), }) } + if err := h.applyExamPrepAccessLessons(c.Context(), c, items); err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to build lesson list", + Error: err.Error(), + }) + } return c.JSON(domain.Response{ Message: "Lessons retrieved successfully", Data: fiber.Map{ @@ -175,6 +181,12 @@ func (h *Handler) GetExamPrepLessonByID(c *fiber.Ctx) error { Error: err.Error(), }) } + if err := h.applyExamPrepAccessLesson(c.Context(), c, &les); err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to build lesson", + Error: err.Error(), + }) + } return c.JSON(domain.Response{ Message: "Lesson retrieved successfully", Data: les, diff --git a/internal/web_server/handlers/exam_prep_module_handler.go b/internal/web_server/handlers/exam_prep_module_handler.go index 3180eae..cb49a4e 100644 --- a/internal/web_server/handlers/exam_prep_module_handler.go +++ b/internal/web_server/handlers/exam_prep_module_handler.go @@ -85,6 +85,12 @@ func (h *Handler) ListExamPrepModulesByUnit(c *fiber.Ctx) error { Error: err.Error(), }) } + if err := h.applyExamPrepAccessModules(c.Context(), c, items); err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to build module list", + Error: err.Error(), + }) + } return c.JSON(domain.Response{ Message: "Modules retrieved successfully", Data: fiber.Map{ @@ -175,6 +181,12 @@ func (h *Handler) GetExamPrepModuleByID(c *fiber.Ctx) error { Error: err.Error(), }) } + if err := h.applyExamPrepAccessModule(c.Context(), c, &out); err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to build module", + Error: err.Error(), + }) + } return c.JSON(domain.Response{ Message: "Module retrieved successfully", Data: out, diff --git a/internal/web_server/handlers/exam_prep_progress_helper.go b/internal/web_server/handlers/exam_prep_progress_helper.go new file mode 100644 index 0000000..b4107cb --- /dev/null +++ b/internal/web_server/handlers/exam_prep_progress_helper.go @@ -0,0 +1,125 @@ +package handlers + +import ( + "context" + + "Yimaru-Backend/internal/domain" + + "github.com/gofiber/fiber/v2" +) + +func (h *Handler) applyExamPrepAccessCatalogCourses(ctx context.Context, c *fiber.Ctx, items []domain.ExamPrepCatalogCourse) error { + role, _ := c.Locals("role").(domain.Role) + if !role.IsCustomerLearnerRole() { + return nil + } + userID, ok := c.Locals("user_id").(int64) + if !ok || userID == 0 { + return nil + } + for i := range items { + if err := h.lmsProgressSvc.ApplyExamPrepAccessCatalogCourse(ctx, role, userID, &items[i]); err != nil { + return err + } + } + return nil +} + +func (h *Handler) applyExamPrepAccessCatalogCourse(ctx context.Context, c *fiber.Ctx, item *domain.ExamPrepCatalogCourse) error { + role, _ := c.Locals("role").(domain.Role) + if !role.IsCustomerLearnerRole() { + return nil + } + userID, ok := c.Locals("user_id").(int64) + if !ok || userID == 0 { + return nil + } + return h.lmsProgressSvc.ApplyExamPrepAccessCatalogCourse(ctx, role, userID, item) +} + +func (h *Handler) applyExamPrepAccessUnits(ctx context.Context, c *fiber.Ctx, items []domain.ExamPrepUnit) error { + role, _ := c.Locals("role").(domain.Role) + if !role.IsCustomerLearnerRole() { + return nil + } + userID, ok := c.Locals("user_id").(int64) + if !ok || userID == 0 { + return nil + } + for i := range items { + if err := h.lmsProgressSvc.ApplyExamPrepAccessUnit(ctx, role, userID, &items[i]); err != nil { + return err + } + } + return nil +} + +func (h *Handler) applyExamPrepAccessUnit(ctx context.Context, c *fiber.Ctx, item *domain.ExamPrepUnit) error { + role, _ := c.Locals("role").(domain.Role) + if !role.IsCustomerLearnerRole() { + return nil + } + userID, ok := c.Locals("user_id").(int64) + if !ok || userID == 0 { + return nil + } + return h.lmsProgressSvc.ApplyExamPrepAccessUnit(ctx, role, userID, item) +} + +func (h *Handler) applyExamPrepAccessModules(ctx context.Context, c *fiber.Ctx, items []domain.ExamPrepModule) error { + role, _ := c.Locals("role").(domain.Role) + if !role.IsCustomerLearnerRole() { + return nil + } + userID, ok := c.Locals("user_id").(int64) + if !ok || userID == 0 { + return nil + } + for i := range items { + if err := h.lmsProgressSvc.ApplyExamPrepAccessModule(ctx, role, userID, &items[i]); err != nil { + return err + } + } + return nil +} + +func (h *Handler) applyExamPrepAccessModule(ctx context.Context, c *fiber.Ctx, item *domain.ExamPrepModule) error { + role, _ := c.Locals("role").(domain.Role) + if !role.IsCustomerLearnerRole() { + return nil + } + userID, ok := c.Locals("user_id").(int64) + if !ok || userID == 0 { + return nil + } + return h.lmsProgressSvc.ApplyExamPrepAccessModule(ctx, role, userID, item) +} + +func (h *Handler) applyExamPrepAccessLessons(ctx context.Context, c *fiber.Ctx, items []domain.ExamPrepLesson) error { + role, _ := c.Locals("role").(domain.Role) + if !role.IsCustomerLearnerRole() { + return nil + } + userID, ok := c.Locals("user_id").(int64) + if !ok || userID == 0 { + return nil + } + for i := range items { + if err := h.lmsProgressSvc.ApplyExamPrepAccessLesson(ctx, role, userID, &items[i]); err != nil { + return err + } + } + return nil +} + +func (h *Handler) applyExamPrepAccessLesson(ctx context.Context, c *fiber.Ctx, item *domain.ExamPrepLesson) error { + role, _ := c.Locals("role").(domain.Role) + if !role.IsCustomerLearnerRole() { + return nil + } + userID, ok := c.Locals("user_id").(int64) + if !ok || userID == 0 { + return nil + } + return h.lmsProgressSvc.ApplyExamPrepAccessLesson(ctx, role, userID, item) +} diff --git a/internal/web_server/handlers/exam_prep_unit_handler.go b/internal/web_server/handlers/exam_prep_unit_handler.go index 23ce9e5..053ef70 100644 --- a/internal/web_server/handlers/exam_prep_unit_handler.go +++ b/internal/web_server/handlers/exam_prep_unit_handler.go @@ -92,6 +92,12 @@ func (h *Handler) ListExamPrepUnitsByCatalogCourse(c *fiber.Ctx) error { Error: err.Error(), }) } + if err := h.applyExamPrepAccessUnits(c.Context(), c, items); err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to build unit list", + Error: err.Error(), + }) + } return c.JSON(domain.Response{ Message: "Units retrieved successfully", Data: fiber.Map{ @@ -183,6 +189,12 @@ func (h *Handler) GetExamPrepUnitByID(c *fiber.Ctx) error { Error: err.Error(), }) } + if err := h.applyExamPrepAccessUnit(c.Context(), c, &out); err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to build unit", + Error: err.Error(), + }) + } return c.JSON(domain.Response{ Message: "Unit retrieved successfully", Data: out, diff --git a/internal/web_server/handlers/questions.go b/internal/web_server/handlers/questions.go index 2ae425c..cf83bb7 100644 --- a/internal/web_server/handlers/questions.go +++ b/internal/web_server/handlers/questions.go @@ -1573,6 +1573,14 @@ func (h *Handler) CompletePractice(c *fiber.Ctx) error { }) } set, setErr = h.questionsSvc.GetQuestionSetByID(c.Context(), practice.QuestionSetID) + } else if examPractice, examPracticeErr := h.examPrepSvc.GetExamPrepPracticeByID(c.Context(), id); examPracticeErr == nil { + if !examPractice.VisibleToLearners() { + return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{ + Message: "Only published practices can be completed", + }) + } + set, setErr = h.questionsSvc.GetQuestionSetByID(c.Context(), examPractice.QuestionSetID) + practiceErr = nil } else { // Backward compatibility: also accept question_set.id directly. set, setErr = h.questionsSvc.GetQuestionSetByID(c.Context(), id)