From 256183ae64ed0ee08d8fc5374a232b43fdc19853 Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Tue, 2 Jun 2026 02:56:12 -0700 Subject: [PATCH] Align learner progress rollups with practice-scoped lesson counts. Count only children that have published practices at module and above for LMS and exam prep; keep lesson at 100% after one practice and module at 100% after direct module practice. Co-authored-by: Cursor --- internal/repository/exam_prep_progress.go | 15 ++ internal/repository/lms_access.go | 10 ++ internal/services/lmsprogress/service.go | 139 ++++++++++-------- internal/services/lmsprogress/service_test.go | 15 ++ 4 files changed, 120 insertions(+), 59 deletions(-) diff --git a/internal/repository/exam_prep_progress.go b/internal/repository/exam_prep_progress.go index 247179f..e7bf3f2 100644 --- a/internal/repository/exam_prep_progress.go +++ b/internal/repository/exam_prep_progress.go @@ -6,6 +6,21 @@ import ( dbgen "Yimaru-Backend/gen/db" ) +// ExamPrepCountPublishedPracticesInLesson returns published practices attached to an exam-prep lesson. +func (s *Store) ExamPrepCountPublishedPracticesInLesson(ctx context.Context, lessonID int64) (int32, error) { + return s.queries.CountPublishedExamPrepPracticesInLesson(ctx, lessonID) +} + +// ExamPrepCountPublishedPracticesInModule returns published practices under an exam-prep module (via lessons). +func (s *Store) ExamPrepCountPublishedPracticesInModule(ctx context.Context, moduleID int64) (int32, error) { + return s.queries.CountPublishedExamPrepPracticesInModule(ctx, moduleID) +} + +// ExamPrepCountPublishedPracticesInUnit returns published practices under an exam-prep unit. +func (s *Store) ExamPrepCountPublishedPracticesInUnit(ctx context.Context, unitID int64) (int32, error) { + return s.queries.CountPublishedExamPrepPracticesInUnit(ctx, unitID) +} + // 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) diff --git a/internal/repository/lms_access.go b/internal/repository/lms_access.go index 3bde128..5cdb53e 100644 --- a/internal/repository/lms_access.go +++ b/internal/repository/lms_access.go @@ -43,6 +43,16 @@ func (s *Store) LmsCountPublishedPracticesInLesson(ctx context.Context, lessonID return s.queries.CountPublishedPracticesInLesson(ctx, toPgInt8(&lessonID)) } +// LmsCountPublishedPracticesInModule returns published practices in a module (direct + lesson-attached). +func (s *Store) LmsCountPublishedPracticesInModule(ctx context.Context, moduleID int64) (int32, error) { + return s.queries.CountPublishedPracticesInModule(ctx, toPgInt8(&moduleID)) +} + +// LmsCountPublishedPracticesInCourse returns published practices in a course (direct + module + lesson scope). +func (s *Store) LmsCountPublishedPracticesInCourse(ctx context.Context, courseID int64) (int32, error) { + return s.queries.CountPublishedPracticesInCourse(ctx, toPgInt8(&courseID)) +} + // LmsUserPracticeProgressInLesson returns published practice completion counts scoped to a lesson. func (s *Store) LmsUserPracticeProgressInLesson(ctx context.Context, userID, lessonID int64) (completed, total int32, err error) { lessonIDPG := toPgInt8(&lessonID) diff --git a/internal/services/lmsprogress/service.go b/internal/services/lmsprogress/service.go index c04c84d..1652696 100644 --- a/internal/services/lmsprogress/service.go +++ b/internal/services/lmsprogress/service.go @@ -332,11 +332,8 @@ func (s *Service) lmsModuleProgress(ctx context.Context, userID, moduleID int64) doneLessons++ } } - if total == 0 { - return 0, false, 0, 0, nil - } - fraction = float64(doneLessons) / float64(total) - return fraction, fraction >= 1, doneLessons, total, nil + fraction, done, completed, total = practiceScopeFraction(doneLessons, total) + return fraction, done, completed, total, nil } func (s *Service) lmsCourseProgress(ctx context.Context, userID, courseID int64) (fraction float64, done bool, completed, total int32, err error) { @@ -352,25 +349,27 @@ func (s *Service) lmsCourseProgress(ctx context.Context, userID, courseID int64) if err != nil { return 0, false, 0, 0, err } - if len(moduleIDs) == 0 { - return 0, false, 0, 0, nil - } - var sum float64 - var fullDone int32 + var doneModules int32 for _, moduleID := range moduleIDs { - moduleFraction, _, _, _, err := s.lmsModuleProgress(ctx, userID, moduleID) + practiceCount, err := s.store.LmsCountPublishedPracticesInModule(ctx, moduleID) if err != nil { return 0, false, 0, 0, err } - sum += moduleFraction - if moduleFraction >= 1 { - fullDone++ + if practiceCount == 0 { + continue + } + total++ + _, moduleDone, _, _, err := s.lmsModuleProgress(ctx, userID, moduleID) + if err != nil { + return 0, false, 0, 0, err + } + if moduleDone { + doneModules++ } } - total = int32(len(moduleIDs)) - fraction = sum / float64(total) - return fraction, fraction >= 1, fullDone, total, nil + fraction, done, completed, total = practiceScopeFraction(doneModules, total) + return fraction, done, completed, total, nil } func (s *Service) lmsProgramProgress(ctx context.Context, userID, programID int64) (fraction float64, done bool, completed, total int32, err error) { @@ -378,25 +377,27 @@ func (s *Service) lmsProgramProgress(ctx context.Context, userID, programID int6 if err != nil { return 0, false, 0, 0, err } - if len(courseIDs) == 0 { - return 0, false, 0, 0, nil - } - var sum float64 - var fullDone int32 + var doneCourses int32 for _, courseID := range courseIDs { - courseFraction, _, _, _, err := s.lmsCourseProgress(ctx, userID, courseID) + practiceCount, err := s.store.LmsCountPublishedPracticesInCourse(ctx, courseID) if err != nil { return 0, false, 0, 0, err } - sum += courseFraction - if courseFraction >= 1 { - fullDone++ + if practiceCount == 0 { + continue + } + total++ + _, courseDone, _, _, err := s.lmsCourseProgress(ctx, userID, courseID) + if err != nil { + return 0, false, 0, 0, err + } + if courseDone { + doneCourses++ } } - total = int32(len(courseIDs)) - fraction = sum / float64(total) - return fraction, fraction >= 1, fullDone, total, nil + fraction, done, completed, total = practiceScopeFraction(doneCourses, total) + return fraction, done, completed, total, nil } func (s *Service) hasCompletedDirectModulePractice(ctx context.Context, userID, moduleID int64) (bool, error) { @@ -431,11 +432,17 @@ func (s *Service) examPrepModuleProgress(ctx context.Context, userID, moduleID i if err != nil { return 0, false, 0, 0, err } - if len(lessonIDs) == 0 { - return 0, false, 0, 0, nil - } + var doneLessons int32 for _, lessonID := range lessonIDs { + practiceCount, err := s.store.ExamPrepCountPublishedPracticesInLesson(ctx, lessonID) + if err != nil { + return 0, false, 0, 0, err + } + if practiceCount == 0 { + continue + } + total++ lessonFraction, _, _, _, err := s.examPrepLessonProgress(ctx, userID, lessonID) if err != nil { return 0, false, 0, 0, err @@ -444,9 +451,8 @@ func (s *Service) examPrepModuleProgress(ctx context.Context, userID, moduleID i doneLessons++ } } - total = int32(len(lessonIDs)) - fraction = float64(doneLessons) / float64(total) - return fraction, fraction >= 1, doneLessons, total, nil + fraction, done, completed, total = practiceScopeFraction(doneLessons, total) + return fraction, done, completed, total, nil } func (s *Service) examPrepUnitProgress(ctx context.Context, userID, unitID int64) (fraction float64, done bool, completed, total int32, err error) { @@ -454,24 +460,27 @@ func (s *Service) examPrepUnitProgress(ctx context.Context, userID, unitID int64 if err != nil { return 0, false, 0, 0, err } - if len(moduleIDs) == 0 { - return 0, false, 0, 0, nil - } - var sum float64 - var fullDone int32 + + var doneModules int32 for _, moduleID := range moduleIDs { - moduleFraction, _, _, _, err := s.examPrepModuleProgress(ctx, userID, moduleID) + practiceCount, err := s.store.ExamPrepCountPublishedPracticesInModule(ctx, moduleID) if err != nil { return 0, false, 0, 0, err } - sum += moduleFraction - if moduleFraction >= 1 { - fullDone++ + if practiceCount == 0 { + continue + } + total++ + _, moduleDone, _, _, err := s.examPrepModuleProgress(ctx, userID, moduleID) + if err != nil { + return 0, false, 0, 0, err + } + if moduleDone { + doneModules++ } } - total = int32(len(moduleIDs)) - fraction = sum / float64(total) - return fraction, fraction >= 1, fullDone, total, nil + fraction, done, completed, total = practiceScopeFraction(doneModules, total) + return fraction, done, completed, total, nil } func (s *Service) examPrepCatalogCourseProgress(ctx context.Context, userID, catalogCourseID int64) (fraction float64, done bool, completed, total int32, err error) { @@ -479,24 +488,36 @@ func (s *Service) examPrepCatalogCourseProgress(ctx context.Context, userID, cat if err != nil { return 0, false, 0, 0, err } - if len(unitIDs) == 0 { - return 0, false, 0, 0, nil - } - var sum float64 - var fullDone int32 + + var doneUnits int32 for _, unitID := range unitIDs { - unitFraction, _, _, _, err := s.examPrepUnitProgress(ctx, userID, unitID) + practiceCount, err := s.store.ExamPrepCountPublishedPracticesInUnit(ctx, unitID) if err != nil { return 0, false, 0, 0, err } - sum += unitFraction - if unitFraction >= 1 { - fullDone++ + if practiceCount == 0 { + continue + } + total++ + _, unitDone, _, _, err := s.examPrepUnitProgress(ctx, userID, unitID) + if err != nil { + return 0, false, 0, 0, err + } + if unitDone { + doneUnits++ } } - total = int32(len(unitIDs)) - fraction = sum / float64(total) - return fraction, fraction >= 1, fullDone, total, nil + fraction, done, completed, total = practiceScopeFraction(doneUnits, total) + return fraction, done, completed, total, nil +} + +// practiceScopeFraction returns done/total for entities that only count children with published practices. +func practiceScopeFraction(done, total int32) (fraction float64, complete bool, completed, totalOut int32) { + if total == 0 { + return 0, false, 0, 0 + } + fraction = float64(done) / float64(total) + return fraction, fraction >= 1, done, total } func lmsProgressComplete(completed, total int32) bool { diff --git a/internal/services/lmsprogress/service_test.go b/internal/services/lmsprogress/service_test.go index 860cb8a..73045c8 100644 --- a/internal/services/lmsprogress/service_test.go +++ b/internal/services/lmsprogress/service_test.go @@ -75,6 +75,21 @@ func TestLMSProgressCounts(t *testing.T) { } } +func TestPracticeScopeFraction(t *testing.T) { + fraction, done, completed, total := practiceScopeFraction(1, 5) + if fraction != 0.2 || done || completed != 1 || total != 5 { + t.Fatalf("practiceScopeFraction(1,5)=(%v,%v,%d,%d), want (0.2,false,1,5)", fraction, done, completed, total) + } + fraction, done, completed, total = practiceScopeFraction(5, 5) + if fraction != 1 || !done || completed != 5 || total != 5 { + t.Fatalf("practiceScopeFraction(5,5)=(%v,%v,%d,%d), want (1,true,5,5)", fraction, done, completed, total) + } + fraction, done, completed, total = practiceScopeFraction(0, 0) + if fraction != 0 || done || completed != 0 || total != 0 { + t.Fatalf("practiceScopeFraction(0,0)=(%v,%v,%d,%d), want (0,false,0,0)", fraction, done, completed, total) + } +} + func TestLMSProgressComplete(t *testing.T) { tests := []struct { name string