Require lesson and practice completion for LMS rollups.
Update lesson and practice completion flows to cascade module/course/program progress only when both lesson completion and related published practice completion criteria are met, and align progress counters with the new rule. Made-with: Cursor
This commit is contained in:
parent
8c116f4a0b
commit
9027b65011
|
|
@ -246,3 +246,95 @@ FROM
|
|||
WHERE
|
||||
c.program_id = $1
|
||||
AND ulp.user_id = $2;
|
||||
|
||||
-- Published practices in a module (module-level and lesson-level practices should carry module_id).
|
||||
-- name: CountPublishedPracticesInModule :one
|
||||
SELECT
|
||||
count(*)::int AS n
|
||||
FROM
|
||||
lms_practices lp
|
||||
INNER JOIN question_sets qs ON qs.id = lp.question_set_id
|
||||
WHERE
|
||||
lp.module_id = $1
|
||||
AND qs.set_type = 'PRACTICE'
|
||||
AND qs.status = 'PUBLISHED';
|
||||
|
||||
-- name: CountUserCompletedPublishedPracticesInModule :one
|
||||
SELECT
|
||||
count(*)::int AS n
|
||||
FROM
|
||||
lms_practices lp
|
||||
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
|
||||
AND upp.user_id = $2
|
||||
AND upp.completed_at IS NOT NULL
|
||||
AND qs.set_type = 'PRACTICE'
|
||||
AND qs.status = 'PUBLISHED';
|
||||
|
||||
-- name: CountPublishedPracticesInCourse :one
|
||||
SELECT
|
||||
count(*)::int AS n
|
||||
FROM
|
||||
lms_practices lp
|
||||
INNER JOIN question_sets qs ON qs.id = lp.question_set_id
|
||||
WHERE
|
||||
lp.course_id = $1
|
||||
AND qs.set_type = 'PRACTICE'
|
||||
AND qs.status = 'PUBLISHED';
|
||||
|
||||
-- name: CountUserCompletedPublishedPracticesInCourse :one
|
||||
SELECT
|
||||
count(*)::int AS n
|
||||
FROM
|
||||
lms_practices lp
|
||||
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
|
||||
AND upp.user_id = $2
|
||||
AND upp.completed_at IS NOT NULL
|
||||
AND qs.set_type = 'PRACTICE'
|
||||
AND qs.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
|
||||
AND qs.set_type = 'PRACTICE'
|
||||
AND qs.status = 'PUBLISHED';
|
||||
|
||||
-- name: CountUserCompletedPublishedPracticesInProgram :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
|
||||
INNER JOIN user_practice_progress upp ON upp.question_set_id = lp.question_set_id
|
||||
WHERE
|
||||
c.program_id = $1
|
||||
AND upp.user_id = $2
|
||||
AND upp.completed_at IS NOT NULL
|
||||
AND qs.set_type = 'PRACTICE'
|
||||
AND qs.status = 'PUBLISHED';
|
||||
|
||||
-- name: GetPracticeScopeByQuestionSetID :one
|
||||
SELECT
|
||||
id,
|
||||
course_id,
|
||||
module_id,
|
||||
lesson_id
|
||||
FROM
|
||||
lms_practices
|
||||
WHERE
|
||||
question_set_id = $1
|
||||
ORDER BY
|
||||
id DESC
|
||||
LIMIT 1;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ package dbgen
|
|||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const CountCoursesInProgram = `-- name: CountCoursesInProgram :one
|
||||
|
|
@ -94,6 +96,65 @@ func (q *Queries) CountModulesInCourse(ctx context.Context, courseID int64) (int
|
|||
return n, err
|
||||
}
|
||||
|
||||
const CountPublishedPracticesInCourse = `-- name: CountPublishedPracticesInCourse :one
|
||||
SELECT
|
||||
count(*)::int AS n
|
||||
FROM
|
||||
lms_practices lp
|
||||
INNER JOIN question_sets qs ON qs.id = lp.question_set_id
|
||||
WHERE
|
||||
lp.course_id = $1
|
||||
AND qs.set_type = 'PRACTICE'
|
||||
AND qs.status = 'PUBLISHED'
|
||||
`
|
||||
|
||||
func (q *Queries) CountPublishedPracticesInCourse(ctx context.Context, courseID pgtype.Int8) (int32, error) {
|
||||
row := q.db.QueryRow(ctx, CountPublishedPracticesInCourse, courseID)
|
||||
var n int32
|
||||
err := row.Scan(&n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
const CountPublishedPracticesInModule = `-- name: CountPublishedPracticesInModule :one
|
||||
SELECT
|
||||
count(*)::int AS n
|
||||
FROM
|
||||
lms_practices lp
|
||||
INNER JOIN question_sets qs ON qs.id = lp.question_set_id
|
||||
WHERE
|
||||
lp.module_id = $1
|
||||
AND qs.set_type = 'PRACTICE'
|
||||
AND qs.status = 'PUBLISHED'
|
||||
`
|
||||
|
||||
// Published practices in a module (module-level and lesson-level practices should carry module_id).
|
||||
func (q *Queries) CountPublishedPracticesInModule(ctx context.Context, moduleID pgtype.Int8) (int32, error) {
|
||||
row := q.db.QueryRow(ctx, CountPublishedPracticesInModule, moduleID)
|
||||
var n int32
|
||||
err := row.Scan(&n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
const CountPublishedPracticesInProgram = `-- 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
|
||||
AND qs.set_type = 'PRACTICE'
|
||||
AND qs.status = 'PUBLISHED'
|
||||
`
|
||||
|
||||
func (q *Queries) CountPublishedPracticesInProgram(ctx context.Context, programID int64) (int32, error) {
|
||||
row := q.db.QueryRow(ctx, CountPublishedPracticesInProgram, programID)
|
||||
var n int32
|
||||
err := row.Scan(&n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
const CountUserCompletedCoursesInProgram = `-- name: CountUserCompletedCoursesInProgram :one
|
||||
SELECT
|
||||
count(*)::int AS n
|
||||
|
|
@ -212,6 +273,122 @@ func (q *Queries) CountUserCompletedModulesInCourse(ctx context.Context, arg Cou
|
|||
return n, err
|
||||
}
|
||||
|
||||
const CountUserCompletedPublishedPracticesInCourse = `-- name: CountUserCompletedPublishedPracticesInCourse :one
|
||||
SELECT
|
||||
count(*)::int AS n
|
||||
FROM
|
||||
lms_practices lp
|
||||
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
|
||||
AND upp.user_id = $2
|
||||
AND upp.completed_at IS NOT NULL
|
||||
AND qs.set_type = 'PRACTICE'
|
||||
AND qs.status = 'PUBLISHED'
|
||||
`
|
||||
|
||||
type CountUserCompletedPublishedPracticesInCourseParams struct {
|
||||
CourseID pgtype.Int8 `json:"course_id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) CountUserCompletedPublishedPracticesInCourse(ctx context.Context, arg CountUserCompletedPublishedPracticesInCourseParams) (int32, error) {
|
||||
row := q.db.QueryRow(ctx, CountUserCompletedPublishedPracticesInCourse, arg.CourseID, arg.UserID)
|
||||
var n int32
|
||||
err := row.Scan(&n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
const CountUserCompletedPublishedPracticesInModule = `-- name: CountUserCompletedPublishedPracticesInModule :one
|
||||
SELECT
|
||||
count(*)::int AS n
|
||||
FROM
|
||||
lms_practices lp
|
||||
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
|
||||
AND upp.user_id = $2
|
||||
AND upp.completed_at IS NOT NULL
|
||||
AND qs.set_type = 'PRACTICE'
|
||||
AND qs.status = 'PUBLISHED'
|
||||
`
|
||||
|
||||
type CountUserCompletedPublishedPracticesInModuleParams struct {
|
||||
ModuleID pgtype.Int8 `json:"module_id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) CountUserCompletedPublishedPracticesInModule(ctx context.Context, arg CountUserCompletedPublishedPracticesInModuleParams) (int32, error) {
|
||||
row := q.db.QueryRow(ctx, CountUserCompletedPublishedPracticesInModule, arg.ModuleID, arg.UserID)
|
||||
var n int32
|
||||
err := row.Scan(&n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
const CountUserCompletedPublishedPracticesInProgram = `-- name: CountUserCompletedPublishedPracticesInProgram :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
|
||||
INNER JOIN user_practice_progress upp ON upp.question_set_id = lp.question_set_id
|
||||
WHERE
|
||||
c.program_id = $1
|
||||
AND upp.user_id = $2
|
||||
AND upp.completed_at IS NOT NULL
|
||||
AND qs.set_type = 'PRACTICE'
|
||||
AND qs.status = 'PUBLISHED'
|
||||
`
|
||||
|
||||
type CountUserCompletedPublishedPracticesInProgramParams struct {
|
||||
ProgramID int64 `json:"program_id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) CountUserCompletedPublishedPracticesInProgram(ctx context.Context, arg CountUserCompletedPublishedPracticesInProgramParams) (int32, error) {
|
||||
row := q.db.QueryRow(ctx, CountUserCompletedPublishedPracticesInProgram, arg.ProgramID, arg.UserID)
|
||||
var n int32
|
||||
err := row.Scan(&n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
const GetPracticeScopeByQuestionSetID = `-- name: GetPracticeScopeByQuestionSetID :one
|
||||
SELECT
|
||||
id,
|
||||
course_id,
|
||||
module_id,
|
||||
lesson_id
|
||||
FROM
|
||||
lms_practices
|
||||
WHERE
|
||||
question_set_id = $1
|
||||
ORDER BY
|
||||
id DESC
|
||||
LIMIT 1
|
||||
`
|
||||
|
||||
type GetPracticeScopeByQuestionSetIDRow struct {
|
||||
ID int64 `json:"id"`
|
||||
CourseID pgtype.Int8 `json:"course_id"`
|
||||
ModuleID pgtype.Int8 `json:"module_id"`
|
||||
LessonID pgtype.Int8 `json:"lesson_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetPracticeScopeByQuestionSetID(ctx context.Context, questionSetID int64) (GetPracticeScopeByQuestionSetIDRow, error) {
|
||||
row := q.db.QueryRow(ctx, GetPracticeScopeByQuestionSetID, questionSetID)
|
||||
var i GetPracticeScopeByQuestionSetIDRow
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.CourseID,
|
||||
&i.ModuleID,
|
||||
&i.LessonID,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const GetPreviousCourseInProgram = `-- name: GetPreviousCourseInProgram :one
|
||||
SELECT
|
||||
c2.id, c2.program_id, c2.name, c2.description, c2.thumbnail, c2.created_at, c2.updated_at, c2.sort_order
|
||||
|
|
|
|||
|
|
@ -38,50 +38,89 @@ func (s *Store) LmsUserHasLessonProgress(ctx context.Context, userID, lessonID i
|
|||
return s.queries.UserHasLessonProgress(ctx, dbgen.UserHasLessonProgressParams{UserID: userID, LessonID: lessonID})
|
||||
}
|
||||
|
||||
// LmsUserLessonProgressInModule returns completed and total lesson counts in a module (for progress UI).
|
||||
// LmsUserLessonProgressInModule returns combined completed/total counts for lessons + published practices in a module.
|
||||
func (s *Store) LmsUserLessonProgressInModule(ctx context.Context, userID, moduleID int64) (completed, total int32, err error) {
|
||||
total, err = s.queries.CountLessonsInModule(ctx, moduleID)
|
||||
lessonTotal, err := s.queries.CountLessonsInModule(ctx, moduleID)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
completed, err = s.queries.CountUserCompletedLessonsInModule(ctx, dbgen.CountUserCompletedLessonsInModuleParams{
|
||||
lessonCompleted, err := s.queries.CountUserCompletedLessonsInModule(ctx, dbgen.CountUserCompletedLessonsInModuleParams{
|
||||
ModuleID: moduleID,
|
||||
UserID: userID,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
return completed, total, nil
|
||||
}
|
||||
|
||||
// LmsUserLessonProgressInCourse returns completed and total lesson counts in a course (all modules).
|
||||
func (s *Store) LmsUserLessonProgressInCourse(ctx context.Context, userID, courseID int64) (completed, total int32, err error) {
|
||||
total, err = s.queries.CountLessonsInCourse(ctx, courseID)
|
||||
practiceTotal, err := s.queries.CountPublishedPracticesInModule(ctx, toPgInt8(&moduleID))
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
completed, err = s.queries.CountUserCompletedLessonsInCourse(ctx, dbgen.CountUserCompletedLessonsInCourseParams{
|
||||
practiceCompleted, err := s.queries.CountUserCompletedPublishedPracticesInModule(ctx, dbgen.CountUserCompletedPublishedPracticesInModuleParams{
|
||||
ModuleID: toPgInt8(&moduleID),
|
||||
UserID: userID,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
total = lessonTotal + practiceTotal
|
||||
completed = lessonCompleted + practiceCompleted
|
||||
return completed, total, nil
|
||||
}
|
||||
|
||||
// LmsUserLessonProgressInCourse returns combined completed/total counts for lessons + published practices in a course.
|
||||
func (s *Store) LmsUserLessonProgressInCourse(ctx context.Context, userID, courseID int64) (completed, total int32, err error) {
|
||||
lessonTotal, err := s.queries.CountLessonsInCourse(ctx, courseID)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
lessonCompleted, err := s.queries.CountUserCompletedLessonsInCourse(ctx, dbgen.CountUserCompletedLessonsInCourseParams{
|
||||
CourseID: courseID,
|
||||
UserID: userID,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
return completed, total, nil
|
||||
}
|
||||
|
||||
// LmsUserLessonProgressInProgram returns completed and total lesson counts in a program (all courses).
|
||||
func (s *Store) LmsUserLessonProgressInProgram(ctx context.Context, userID, programID int64) (completed, total int32, err error) {
|
||||
total, err = s.queries.CountLessonsInProgram(ctx, programID)
|
||||
practiceTotal, err := s.queries.CountPublishedPracticesInCourse(ctx, toPgInt8(&courseID))
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
completed, err = s.queries.CountUserCompletedLessonsInProgram(ctx, dbgen.CountUserCompletedLessonsInProgramParams{
|
||||
practiceCompleted, err := s.queries.CountUserCompletedPublishedPracticesInCourse(ctx, dbgen.CountUserCompletedPublishedPracticesInCourseParams{
|
||||
CourseID: toPgInt8(&courseID),
|
||||
UserID: userID,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
total = lessonTotal + practiceTotal
|
||||
completed = lessonCompleted + practiceCompleted
|
||||
return completed, total, nil
|
||||
}
|
||||
|
||||
// LmsUserLessonProgressInProgram returns combined completed/total counts for lessons + published practices in a program.
|
||||
func (s *Store) LmsUserLessonProgressInProgram(ctx context.Context, userID, programID int64) (completed, total int32, err error) {
|
||||
lessonTotal, err := s.queries.CountLessonsInProgram(ctx, programID)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
lessonCompleted, err := s.queries.CountUserCompletedLessonsInProgram(ctx, dbgen.CountUserCompletedLessonsInProgramParams{
|
||||
ProgramID: programID,
|
||||
UserID: userID,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
practiceTotal, err := s.queries.CountPublishedPracticesInProgram(ctx, programID)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
practiceCompleted, err := s.queries.CountUserCompletedPublishedPracticesInProgram(ctx, dbgen.CountUserCompletedPublishedPracticesInProgramParams{
|
||||
ProgramID: programID,
|
||||
UserID: userID,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
total = lessonTotal + practiceTotal
|
||||
completed = lessonCompleted + practiceCompleted
|
||||
return completed, total, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ import (
|
|||
dbgen "Yimaru-Backend/gen/db"
|
||||
)
|
||||
|
||||
// CompleteLessonForUser records lesson completion and cascades to module, course, and program when the user
|
||||
// has fully completed the preceding scope. Runs in a single transaction.
|
||||
// CompleteLessonForUser records lesson completion and cascades completion upward when
|
||||
// both lesson and related practice requirements are satisfied.
|
||||
func (s *Store) CompleteLessonForUser(ctx context.Context, userID, lessonID int64) error {
|
||||
q, tx, err := s.BeginTx(ctx)
|
||||
if err != nil {
|
||||
|
|
@ -31,56 +31,162 @@ func (s *Store) CompleteLessonForUser(ctx context.Context, userID, lessonID int6
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
nLess, err := q.CountLessonsInModule(ctx, lesson.ModuleID)
|
||||
if err != nil {
|
||||
|
||||
if err := s.cascadeLMSCompletion(ctx, q, userID, mod.ID, crs.ID, crs.ProgramID); err != nil {
|
||||
return err
|
||||
}
|
||||
nDoneLess, err := q.CountUserCompletedLessonsInModule(ctx, dbgen.CountUserCompletedLessonsInModuleParams{
|
||||
ModuleID: lesson.ModuleID,
|
||||
UserID: userID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if nLess > 0 && nDoneLess >= nLess {
|
||||
if err := q.InsertUserModuleProgress(ctx, dbgen.InsertUserModuleProgressParams{UserID: userID, ModuleID: mod.ID}); err != nil {
|
||||
return err
|
||||
}
|
||||
nMods, err := q.CountModulesInCourse(ctx, mod.CourseID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
nDoneMods, err := q.CountUserCompletedModulesInCourse(ctx, dbgen.CountUserCompletedModulesInCourseParams{
|
||||
CourseID: mod.CourseID,
|
||||
UserID: userID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if nMods > 0 && nDoneMods >= nMods {
|
||||
if err := q.InsertUserCourseProgress(ctx, dbgen.InsertUserCourseProgressParams{UserID: userID, CourseID: crs.ID}); err != nil {
|
||||
return err
|
||||
}
|
||||
nCr, err := q.CountCoursesInProgram(ctx, crs.ProgramID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
nCrDone, err := q.CountUserCompletedCoursesInProgram(ctx, dbgen.CountUserCompletedCoursesInProgramParams{
|
||||
ProgramID: crs.ProgramID,
|
||||
UserID: userID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if nCr > 0 && nCrDone >= nCr {
|
||||
if err := q.InsertUserProgramProgress(ctx, dbgen.InsertUserProgramProgressParams{UserID: userID, ProgramID: crs.ProgramID}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return fmt.Errorf("commit: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CompletePracticeForUser records practice completion and cascades completion upward when
|
||||
// both lesson and related practice requirements are satisfied.
|
||||
func (s *Store) CompletePracticeForUser(ctx context.Context, userID, questionSetID int64) error {
|
||||
q, tx, err := s.BeginTx(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("begin tx: %w", err)
|
||||
}
|
||||
defer func() { _ = tx.Rollback(ctx) }()
|
||||
|
||||
if _, err := q.MarkPracticeCompleted(ctx, dbgen.MarkPracticeCompletedParams{
|
||||
UserID: userID,
|
||||
QuestionSetID: questionSetID,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
scope, err := q.GetPracticeScopeByQuestionSetID(ctx, questionSetID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !scope.ModuleID.Valid {
|
||||
return fmt.Errorf("practice %d is not linked to a module", questionSetID)
|
||||
}
|
||||
|
||||
mod, err := q.GetModuleByID(ctx, scope.ModuleID.Int64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
crs, err := q.GetCourseByID(ctx, mod.CourseID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.cascadeLMSCompletion(ctx, q, userID, mod.ID, crs.ID, crs.ProgramID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return fmt.Errorf("commit: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) cascadeLMSCompletion(ctx context.Context, q *dbgen.Queries, userID, moduleID, courseID, programID int64) error {
|
||||
moduleLessonsTotal, err := q.CountLessonsInModule(ctx, moduleID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
moduleLessonsDone, err := q.CountUserCompletedLessonsInModule(ctx, dbgen.CountUserCompletedLessonsInModuleParams{
|
||||
ModuleID: moduleID,
|
||||
UserID: userID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
modulePracticesTotal, err := q.CountPublishedPracticesInModule(ctx, toPgInt8(&moduleID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
modulePracticesDone, err := q.CountUserCompletedPublishedPracticesInModule(ctx, dbgen.CountUserCompletedPublishedPracticesInModuleParams{
|
||||
ModuleID: toPgInt8(&moduleID),
|
||||
UserID: userID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
moduleLessonsComplete := moduleLessonsTotal > 0 && moduleLessonsDone >= moduleLessonsTotal
|
||||
modulePracticesComplete := modulePracticesDone >= modulePracticesTotal
|
||||
if !moduleLessonsComplete || !modulePracticesComplete {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := q.InsertUserModuleProgress(ctx, dbgen.InsertUserModuleProgressParams{UserID: userID, ModuleID: moduleID}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
nMods, err := q.CountModulesInCourse(ctx, courseID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
nDoneMods, err := q.CountUserCompletedModulesInCourse(ctx, dbgen.CountUserCompletedModulesInCourseParams{
|
||||
CourseID: courseID,
|
||||
UserID: userID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
coursePracticesTotal, err := q.CountPublishedPracticesInCourse(ctx, toPgInt8(&courseID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
coursePracticesDone, err := q.CountUserCompletedPublishedPracticesInCourse(ctx, dbgen.CountUserCompletedPublishedPracticesInCourseParams{
|
||||
CourseID: toPgInt8(&courseID),
|
||||
UserID: userID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
courseModulesComplete := nMods > 0 && nDoneMods >= nMods
|
||||
coursePracticesComplete := coursePracticesDone >= coursePracticesTotal
|
||||
if !courseModulesComplete || !coursePracticesComplete {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := q.InsertUserCourseProgress(ctx, dbgen.InsertUserCourseProgressParams{UserID: userID, CourseID: courseID}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
nCr, err := q.CountCoursesInProgram(ctx, programID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
nCrDone, err := q.CountUserCompletedCoursesInProgram(ctx, dbgen.CountUserCompletedCoursesInProgramParams{
|
||||
ProgramID: programID,
|
||||
UserID: userID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
programPracticesTotal, err := q.CountPublishedPracticesInProgram(ctx, programID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
programPracticesDone, err := q.CountUserCompletedPublishedPracticesInProgram(ctx, dbgen.CountUserCompletedPublishedPracticesInProgramParams{
|
||||
ProgramID: programID,
|
||||
UserID: userID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
programCoursesComplete := nCr > 0 && nCrDone >= nCr
|
||||
programPracticesComplete := programPracticesDone >= programPracticesTotal
|
||||
if !programCoursesComplete || !programPracticesComplete {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := q.InsertUserProgramProgress(ctx, dbgen.InsertUserProgramProgressParams{UserID: userID, ProgramID: programID}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,11 @@ func (s *Service) CompleteLessonForUser(ctx context.Context, userID, lessonID in
|
|||
return s.store.CompleteLessonForUser(ctx, userID, lessonID)
|
||||
}
|
||||
|
||||
// CompletePracticeForUser records practice completion and rolls up to module, course, and program when applicable.
|
||||
func (s *Service) CompletePracticeForUser(ctx context.Context, userID, questionSetID int64) error {
|
||||
return s.store.CompletePracticeForUser(ctx, userID, questionSetID)
|
||||
}
|
||||
|
||||
// GetMyProgress returns completed lesson, module, course, and program IDs for the user.
|
||||
func (s *Service) GetMyProgress(ctx context.Context, userID int64) (domain.LMSUserProgress, error) {
|
||||
return s.store.GetLMSUserProgressSnapshot(ctx, userID)
|
||||
|
|
|
|||
|
|
@ -1317,7 +1317,7 @@ func (h *Handler) CompletePractice(c *fiber.Ctx) error {
|
|||
})
|
||||
}
|
||||
|
||||
if err := h.questionsSvc.MarkPracticeCompleted(c.Context(), userID, set.ID); err != nil {
|
||||
if err := h.lmsProgressSvc.CompletePracticeForUser(c.Context(), userID, set.ID); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to complete practice",
|
||||
Error: err.Error(),
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user