progress precentage fix

This commit is contained in:
Yared Yemane 2026-05-27 09:18:25 -07:00
parent d3225ca61a
commit a1c6b3c15a
18 changed files with 1080 additions and 120 deletions

View File

@ -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';

View File

@ -225,72 +225,124 @@ ORDER BY
-- name: ListLMSCompletedModuleIDsByUser :many -- name: ListLMSCompletedModuleIDsByUser :many
SELECT SELECT
lp.module_id scoped.module_id
FROM 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
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
HAVING
count(DISTINCT lp.question_set_id) > 0
AND count(DISTINCT upp.question_set_id) >= count(DISTINCT lp.question_set_id)
ORDER BY
max(upp.completed_at) ASC,
lp.module_id ASC;
-- name: ListLMSCompletedCourseIDsByUser :many
SELECT SELECT
lp.course_id m.id AS module_id,
lp.question_set_id
FROM FROM
lms_practices AS lp modules m
INNER JOIN question_sets qs ON qs.id = lp.question_set_id INNER JOIN lms_practices lp ON (
LEFT JOIN user_practice_progress upp ON upp.question_set_id = lp.question_set_id lp.module_id = m.id
AND upp.user_id = $1 OR lp.lesson_id IN (
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
HAVING
count(DISTINCT lp.question_set_id) > 0
AND count(DISTINCT upp.question_set_id) >= count(DISTINCT lp.question_set_id)
ORDER BY
max(upp.completed_at) ASC,
lp.course_id ASC;
-- name: ListLMSCompletedProgramIDsByUser :many
SELECT SELECT
c.program_id id
FROM FROM
lms_practices AS lp lessons
INNER JOIN courses c ON c.id = lp.course_id WHERE
module_id = m.id))
INNER JOIN question_sets qs ON qs.id = lp.question_set_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
AND upp.user_id = $1
AND upp.completed_at IS NOT NULL
WHERE WHERE
qs.set_type = 'PRACTICE' qs.set_type = 'PRACTICE'
AND qs.status = 'PUBLISHED' AND qs.status = 'PUBLISHED'
AND lp.publish_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
GROUP BY GROUP BY
c.program_id scoped.module_id
HAVING HAVING
count(DISTINCT lp.question_set_id) > 0 count(DISTINCT scoped.question_set_id) > 0
AND count(DISTINCT upp.question_set_id) >= count(DISTINCT lp.question_set_id) AND count(DISTINCT upp.question_set_id) >= count(DISTINCT scoped.question_set_id)
ORDER BY ORDER BY
max(upp.completed_at) ASC, max(upp.completed_at) ASC,
c.program_id ASC; scoped.module_id ASC;
-- name: ListLMSCompletedCourseIDsByUser :many
SELECT
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
GROUP BY
scoped.course_id
HAVING
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,
scoped.course_id ASC;
-- name: ListLMSCompletedProgramIDsByUser :many
SELECT
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
GROUP BY
scoped.program_id
HAVING
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,
scoped.program_id ASC;
-- Lesson-based progress within a course (all modules). -- Lesson-based progress within a course (all modules).
-- name: CountLessonsInCourse :one -- name: CountLessonsInCourse :one
@ -340,7 +392,7 @@ WHERE
AND ulp.user_id = $2 AND ulp.user_id = $2
AND l.publish_status = 'PUBLISHED'; 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 -- name: CountPublishedPracticesInModule :one
SELECT SELECT
count(*)::int AS n count(*)::int AS n
@ -348,7 +400,15 @@ FROM
lms_practices lp lms_practices lp
INNER JOIN question_sets qs ON qs.id = lp.question_set_id INNER JOIN question_sets qs ON qs.id = lp.question_set_id
WHERE 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.set_type = 'PRACTICE'
AND qs.status = 'PUBLISHED' AND qs.status = 'PUBLISHED'
AND lp.publish_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 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 INNER JOIN user_practice_progress upp ON upp.question_set_id = lp.question_set_id
WHERE 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.user_id = $2
AND upp.completed_at IS NOT NULL AND upp.completed_at IS NOT NULL
AND qs.set_type = 'PRACTICE' AND qs.set_type = 'PRACTICE'
@ -375,7 +443,23 @@ FROM
lms_practices lp lms_practices lp
INNER JOIN question_sets qs ON qs.id = lp.question_set_id INNER JOIN question_sets qs ON qs.id = lp.question_set_id
WHERE 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.set_type = 'PRACTICE'
AND qs.status = 'PUBLISHED' AND qs.status = 'PUBLISHED'
AND lp.publish_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 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 INNER JOIN user_practice_progress upp ON upp.question_set_id = lp.question_set_id
WHERE 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.user_id = $2
AND upp.completed_at IS NOT NULL AND upp.completed_at IS NOT NULL
AND qs.set_type = 'PRACTICE' AND qs.set_type = 'PRACTICE'
AND qs.status = 'PUBLISHED'; AND qs.status = 'PUBLISHED'
AND lp.publish_status = 'PUBLISHED';
-- name: CountPublishedPracticesInProgram :one -- name: CountPublishedPracticesInProgram :one
SELECT SELECT
count(*)::int AS n count(*)::int AS n
FROM FROM
lms_practices lp 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 question_sets qs ON qs.id = lp.question_set_id
WHERE 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.set_type = 'PRACTICE'
AND qs.status = 'PUBLISHED' AND qs.status = 'PUBLISHED'
AND lp.publish_status = 'PUBLISHED'; AND lp.publish_status = 'PUBLISHED';
@ -412,11 +536,34 @@ SELECT
count(*)::int AS n count(*)::int AS n
FROM FROM
lms_practices lp 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 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 INNER JOIN user_practice_progress upp ON upp.question_set_id = lp.question_set_id
WHERE 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.user_id = $2
AND upp.completed_at IS NOT NULL AND upp.completed_at IS NOT NULL
AND qs.set_type = 'PRACTICE' AND qs.set_type = 'PRACTICE'

View File

@ -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
}

View File

@ -106,7 +106,23 @@ FROM
lms_practices lp lms_practices lp
INNER JOIN question_sets qs ON qs.id = lp.question_set_id INNER JOIN question_sets qs ON qs.id = lp.question_set_id
WHERE 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.set_type = 'PRACTICE'
AND qs.status = 'PUBLISHED' AND qs.status = 'PUBLISHED'
AND lp.publish_status = 'PUBLISHED' AND lp.publish_status = 'PUBLISHED'
@ -146,13 +162,21 @@ FROM
lms_practices lp lms_practices lp
INNER JOIN question_sets qs ON qs.id = lp.question_set_id INNER JOIN question_sets qs ON qs.id = lp.question_set_id
WHERE 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.set_type = 'PRACTICE'
AND qs.status = 'PUBLISHED' AND qs.status = 'PUBLISHED'
AND lp.publish_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) { func (q *Queries) CountPublishedPracticesInModule(ctx context.Context, moduleID pgtype.Int8) (int32, error) {
row := q.db.QueryRow(ctx, CountPublishedPracticesInModule, moduleID) row := q.db.QueryRow(ctx, CountPublishedPracticesInModule, moduleID)
var n int32 var n int32
@ -165,10 +189,33 @@ SELECT
count(*)::int AS n count(*)::int AS n
FROM FROM
lms_practices lp 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 question_sets qs ON qs.id = lp.question_set_id
WHERE 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.set_type = 'PRACTICE'
AND qs.status = 'PUBLISHED' AND qs.status = 'PUBLISHED'
AND lp.publish_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 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 INNER JOIN user_practice_progress upp ON upp.question_set_id = lp.question_set_id
WHERE 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.user_id = $2
AND upp.completed_at IS NOT NULL AND upp.completed_at IS NOT NULL
AND qs.set_type = 'PRACTICE' AND qs.set_type = 'PRACTICE'
AND qs.status = 'PUBLISHED' AND qs.status = 'PUBLISHED'
AND lp.publish_status = 'PUBLISHED'
` `
type CountUserCompletedPublishedPracticesInCourseParams struct { type CountUserCompletedPublishedPracticesInCourseParams struct {
@ -365,7 +429,15 @@ FROM
INNER JOIN question_sets qs ON qs.id = lp.question_set_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 INNER JOIN user_practice_progress upp ON upp.question_set_id = lp.question_set_id
WHERE 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.user_id = $2
AND upp.completed_at IS NOT NULL AND upp.completed_at IS NOT NULL
AND qs.set_type = 'PRACTICE' AND qs.set_type = 'PRACTICE'
@ -390,11 +462,34 @@ SELECT
count(*)::int AS n count(*)::int AS n
FROM FROM
lms_practices lp 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 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 INNER JOIN user_practice_progress upp ON upp.question_set_id = lp.question_set_id
WHERE 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.user_id = $2
AND upp.completed_at IS NOT NULL AND upp.completed_at IS NOT NULL
AND qs.set_type = 'PRACTICE' AND qs.set_type = 'PRACTICE'
@ -640,37 +735,57 @@ func (q *Queries) InsertUserProgramProgress(ctx context.Context, arg InsertUserP
const ListLMSCompletedCourseIDsByUser = `-- name: ListLMSCompletedCourseIDsByUser :many const ListLMSCompletedCourseIDsByUser = `-- name: ListLMSCompletedCourseIDsByUser :many
SELECT SELECT
lp.course_id scoped.course_id
FROM (
SELECT
c.id AS course_id,
lp.question_set_id
FROM FROM
lms_practices AS lp 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 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 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.user_id = $1
AND upp.completed_at IS NOT NULL 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 GROUP BY
lp.course_id scoped.course_id
HAVING HAVING
count(DISTINCT lp.question_set_id) > 0 count(DISTINCT scoped.question_set_id) > 0
AND count(DISTINCT upp.question_set_id) >= count(DISTINCT lp.question_set_id) AND count(DISTINCT upp.question_set_id) >= count(DISTINCT scoped.question_set_id)
ORDER BY ORDER BY
max(upp.completed_at) ASC, 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) rows, err := q.db.Query(ctx, ListLMSCompletedCourseIDsByUser, userID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer rows.Close() defer rows.Close()
var items []pgtype.Int8 var items []int64
for rows.Next() { for rows.Next() {
var course_id pgtype.Int8 var course_id int64
if err := rows.Scan(&course_id); err != nil { if err := rows.Scan(&course_id); err != nil {
return nil, err return nil, err
} }
@ -728,37 +843,49 @@ func (q *Queries) ListLMSCompletedLessonIDsByUser(ctx context.Context, userID in
const ListLMSCompletedModuleIDsByUser = `-- name: ListLMSCompletedModuleIDsByUser :many const ListLMSCompletedModuleIDsByUser = `-- name: ListLMSCompletedModuleIDsByUser :many
SELECT SELECT
lp.module_id scoped.module_id
FROM (
SELECT
m.id AS module_id,
lp.question_set_id
FROM FROM
lms_practices AS lp 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 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 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.user_id = $1
AND upp.completed_at IS NOT NULL 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 GROUP BY
lp.module_id scoped.module_id
HAVING HAVING
count(DISTINCT lp.question_set_id) > 0 count(DISTINCT scoped.question_set_id) > 0
AND count(DISTINCT upp.question_set_id) >= count(DISTINCT lp.question_set_id) AND count(DISTINCT upp.question_set_id) >= count(DISTINCT scoped.question_set_id)
ORDER BY ORDER BY
max(upp.completed_at) ASC, 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) rows, err := q.db.Query(ctx, ListLMSCompletedModuleIDsByUser, userID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer rows.Close() defer rows.Close()
var items []pgtype.Int8 var items []int64
for rows.Next() { for rows.Next() {
var module_id pgtype.Int8 var module_id int64
if err := rows.Scan(&module_id); err != nil { if err := rows.Scan(&module_id); err != nil {
return nil, err return nil, err
} }
@ -772,26 +899,46 @@ func (q *Queries) ListLMSCompletedModuleIDsByUser(ctx context.Context, userID in
const ListLMSCompletedProgramIDsByUser = `-- name: ListLMSCompletedProgramIDsByUser :many const ListLMSCompletedProgramIDsByUser = `-- name: ListLMSCompletedProgramIDsByUser :many
SELECT SELECT
c.program_id scoped.program_id
FROM (
SELECT
c.program_id,
lp.question_set_id
FROM FROM
lms_practices AS lp courses c
INNER JOIN courses c ON c.id = lp.course_id 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 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
AND upp.user_id = $1
AND upp.completed_at IS NOT NULL
WHERE WHERE
qs.set_type = 'PRACTICE' qs.set_type = 'PRACTICE'
AND qs.status = 'PUBLISHED' AND qs.status = 'PUBLISHED'
AND lp.publish_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
GROUP BY GROUP BY
c.program_id scoped.program_id
HAVING HAVING
count(DISTINCT lp.question_set_id) > 0 count(DISTINCT scoped.question_set_id) > 0
AND count(DISTINCT upp.question_set_id) >= count(DISTINCT lp.question_set_id) AND count(DISTINCT upp.question_set_id) >= count(DISTINCT scoped.question_set_id)
ORDER BY ORDER BY
max(upp.completed_at) ASC, max(upp.completed_at) ASC,
c.program_id ASC scoped.program_id ASC
` `
func (q *Queries) ListLMSCompletedProgramIDsByUser(ctx context.Context, userID int64) ([]int64, error) { func (q *Queries) ListLMSCompletedProgramIDsByUser(ctx context.Context, userID int64) ([]int64, error) {

View File

@ -14,6 +14,7 @@ type ExamPrepCatalogCourse struct {
ModulesCount *int64 `json:"modules_count,omitempty"` ModulesCount *int64 `json:"modules_count,omitempty"`
LessonsCount *int64 `json:"lessons_count,omitempty"` LessonsCount *int64 `json:"lessons_count,omitempty"`
HasPractice bool `json:"has_practice"` HasPractice bool `json:"has_practice"`
Access *LMSEntityAccess `json:"access,omitempty"`
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"`
} }

View File

@ -12,6 +12,7 @@ type ExamPrepLesson struct {
Description *string `json:"description,omitempty"` Description *string `json:"description,omitempty"`
SortOrder int `json:"sort_order"` SortOrder int `json:"sort_order"`
HasPractice bool `json:"has_practice"` HasPractice bool `json:"has_practice"`
Access *LMSEntityAccess `json:"access,omitempty"`
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"`
} }

View File

@ -14,6 +14,7 @@ type ExamPrepModule struct {
LessonsCount *int64 `json:"lessons_count,omitempty"` LessonsCount *int64 `json:"lessons_count,omitempty"`
PracticesCount *int64 `json:"practices_count,omitempty"` PracticesCount *int64 `json:"practices_count,omitempty"`
HasPractice bool `json:"has_practice"` HasPractice bool `json:"has_practice"`
Access *LMSEntityAccess `json:"access,omitempty"`
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"`
} }

View File

@ -14,6 +14,7 @@ type ExamPrepUnit struct {
LessonsCount *int64 `json:"lessons_count,omitempty"` LessonsCount *int64 `json:"lessons_count,omitempty"`
PracticesCount *int64 `json:"practices_count,omitempty"` PracticesCount *int64 `json:"practices_count,omitempty"`
HasPractice bool `json:"has_practice"` HasPractice bool `json:"has_practice"`
Access *LMSEntityAccess `json:"access,omitempty"`
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"`
} }

View File

@ -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
}

View File

@ -2,9 +2,11 @@ package repository
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
dbgen "Yimaru-Backend/gen/db" dbgen "Yimaru-Backend/gen/db"
"github.com/jackc/pgx/v5"
) )
// CompleteLessonForUser records lesson completion for sequential lesson gating and // 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) scope, err := q.GetPracticeScopeByQuestionSetID(ctx, questionSetID)
if err != nil { 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 return err
} }
var ( var (

View File

@ -29,8 +29,8 @@ func (s *Store) GetLMSUserProgressSnapshot(ctx context.Context, userID int64) (d
} }
return domain.LMSUserProgress{ return domain.LMSUserProgress{
LessonIDs: pgInt8IDsToInt64(lessons), LessonIDs: pgInt8IDsToInt64(lessons),
ModuleIDs: pgInt8IDsToInt64(mods), ModuleIDs: int64IDsOrEmpty(mods),
CourseIDs: pgInt8IDsToInt64(courses), CourseIDs: int64IDsOrEmpty(courses),
ProgramIDs: int64IDsOrEmpty(programs), ProgramIDs: int64IDsOrEmpty(programs),
}, nil }, nil
} }

View File

@ -234,6 +234,66 @@ func (s *Service) ApplyAccessLesson(ctx context.Context, role domain.Role, userI
return nil 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 { func lmsProgressComplete(completed, total int32) bool {
return total > 0 && completed >= total return total > 0 && completed >= total
} }

View File

@ -116,10 +116,18 @@ func (h *Handler) ListExamPrepCatalogCourses(c *fiber.Ctx) error {
end = total 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{ return c.JSON(domain.Response{
Message: "Catalog courses retrieved successfully", Message: "Catalog courses retrieved successfully",
Data: fiber.Map{ Data: fiber.Map{
"catalog_courses": filtered[start:end], "catalog_courses": page,
"total_count": total, "total_count": total,
"limit": limit, "limit": limit,
"offset": offset, "offset": offset,
@ -136,6 +144,12 @@ func (h *Handler) ListExamPrepCatalogCourses(c *fiber.Ctx) error {
Error: err.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{ return c.JSON(domain.Response{
Message: "Catalog courses retrieved successfully", Message: "Catalog courses retrieved successfully",
Data: fiber.Map{ Data: fiber.Map{
@ -218,6 +232,12 @@ func (h *Handler) GetExamPrepCatalogCourseByID(c *fiber.Ctx) error {
Error: err.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{ return c.JSON(domain.Response{
Message: "Catalog course retrieved successfully", Message: "Catalog course retrieved successfully",
Data: out, Data: out,

View File

@ -87,6 +87,12 @@ func (h *Handler) ListExamPrepLessonsByUnitModule(c *fiber.Ctx) error {
Error: err.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{ return c.JSON(domain.Response{
Message: "Lessons retrieved successfully", Message: "Lessons retrieved successfully",
Data: fiber.Map{ Data: fiber.Map{
@ -175,6 +181,12 @@ func (h *Handler) GetExamPrepLessonByID(c *fiber.Ctx) error {
Error: err.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{ return c.JSON(domain.Response{
Message: "Lesson retrieved successfully", Message: "Lesson retrieved successfully",
Data: les, Data: les,

View File

@ -85,6 +85,12 @@ func (h *Handler) ListExamPrepModulesByUnit(c *fiber.Ctx) error {
Error: err.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{ return c.JSON(domain.Response{
Message: "Modules retrieved successfully", Message: "Modules retrieved successfully",
Data: fiber.Map{ Data: fiber.Map{
@ -175,6 +181,12 @@ func (h *Handler) GetExamPrepModuleByID(c *fiber.Ctx) error {
Error: err.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{ return c.JSON(domain.Response{
Message: "Module retrieved successfully", Message: "Module retrieved successfully",
Data: out, Data: out,

View File

@ -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)
}

View File

@ -92,6 +92,12 @@ func (h *Handler) ListExamPrepUnitsByCatalogCourse(c *fiber.Ctx) error {
Error: err.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{ return c.JSON(domain.Response{
Message: "Units retrieved successfully", Message: "Units retrieved successfully",
Data: fiber.Map{ Data: fiber.Map{
@ -183,6 +189,12 @@ func (h *Handler) GetExamPrepUnitByID(c *fiber.Ctx) error {
Error: err.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{ return c.JSON(domain.Response{
Message: "Unit retrieved successfully", Message: "Unit retrieved successfully",
Data: out, Data: out,

View File

@ -1573,6 +1573,14 @@ func (h *Handler) CompletePractice(c *fiber.Ctx) error {
}) })
} }
set, setErr = h.questionsSvc.GetQuestionSetByID(c.Context(), practice.QuestionSetID) 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 { } else {
// Backward compatibility: also accept question_set.id directly. // Backward compatibility: also accept question_set.id directly.
set, setErr = h.questionsSvc.GetQuestionSetByID(c.Context(), id) set, setErr = h.questionsSvc.GetQuestionSetByID(c.Context(), id)