Fix hierarchical learner progress percentage rollups.
Compute program/course/module/lesson progress using lesson-completion rollups from completed practices, with direct module/course practice completion forcing parent completion as required. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
8eaac9206e
commit
474bf3282a
|
|
@ -570,6 +570,62 @@ WHERE
|
|||
AND qs.status = 'PUBLISHED'
|
||||
AND lp.publish_status = 'PUBLISHED';
|
||||
|
||||
-- Published practices directly attached to module_id (not via lesson_id).
|
||||
-- name: CountPublishedDirectPracticesInModule :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'
|
||||
AND lp.publish_status = 'PUBLISHED';
|
||||
|
||||
-- name: CountUserCompletedPublishedDirectPracticesInModule :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'
|
||||
AND lp.publish_status = 'PUBLISHED';
|
||||
|
||||
-- Published practices directly attached to course_id (not via module_id/lesson_id).
|
||||
-- name: CountPublishedDirectPracticesInCourse :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'
|
||||
AND lp.publish_status = 'PUBLISHED';
|
||||
|
||||
-- name: CountUserCompletedPublishedDirectPracticesInCourse :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'
|
||||
AND lp.publish_status = 'PUBLISHED';
|
||||
|
||||
-- name: GetPracticeScopeByQuestionSetID :one
|
||||
SELECT
|
||||
id,
|
||||
|
|
|
|||
|
|
@ -99,6 +99,48 @@ func (q *Queries) CountModulesInCourse(ctx context.Context, courseID int64) (int
|
|||
return n, err
|
||||
}
|
||||
|
||||
const CountPublishedDirectPracticesInCourse = `-- name: CountPublishedDirectPracticesInCourse :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'
|
||||
AND lp.publish_status = 'PUBLISHED'
|
||||
`
|
||||
|
||||
// Published practices directly attached to course_id (not via module_id/lesson_id).
|
||||
func (q *Queries) CountPublishedDirectPracticesInCourse(ctx context.Context, courseID pgtype.Int8) (int32, error) {
|
||||
row := q.db.QueryRow(ctx, CountPublishedDirectPracticesInCourse, courseID)
|
||||
var n int32
|
||||
err := row.Scan(&n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
const CountPublishedDirectPracticesInModule = `-- name: CountPublishedDirectPracticesInModule :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'
|
||||
AND lp.publish_status = 'PUBLISHED'
|
||||
`
|
||||
|
||||
// Published practices directly attached to module_id (not via lesson_id).
|
||||
func (q *Queries) CountPublishedDirectPracticesInModule(ctx context.Context, moduleID pgtype.Int8) (int32, error) {
|
||||
row := q.db.QueryRow(ctx, CountPublishedDirectPracticesInModule, moduleID)
|
||||
var n int32
|
||||
err := row.Scan(&n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
const CountPublishedPracticesInCourse = `-- name: CountPublishedPracticesInCourse :one
|
||||
SELECT
|
||||
count(*)::int AS n
|
||||
|
|
@ -349,6 +391,62 @@ func (q *Queries) CountUserCompletedModulesInCourse(ctx context.Context, arg Cou
|
|||
return n, err
|
||||
}
|
||||
|
||||
const CountUserCompletedPublishedDirectPracticesInCourse = `-- name: CountUserCompletedPublishedDirectPracticesInCourse :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'
|
||||
AND lp.publish_status = 'PUBLISHED'
|
||||
`
|
||||
|
||||
type CountUserCompletedPublishedDirectPracticesInCourseParams struct {
|
||||
CourseID pgtype.Int8 `json:"course_id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) CountUserCompletedPublishedDirectPracticesInCourse(ctx context.Context, arg CountUserCompletedPublishedDirectPracticesInCourseParams) (int32, error) {
|
||||
row := q.db.QueryRow(ctx, CountUserCompletedPublishedDirectPracticesInCourse, arg.CourseID, arg.UserID)
|
||||
var n int32
|
||||
err := row.Scan(&n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
const CountUserCompletedPublishedDirectPracticesInModule = `-- name: CountUserCompletedPublishedDirectPracticesInModule :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'
|
||||
AND lp.publish_status = 'PUBLISHED'
|
||||
`
|
||||
|
||||
type CountUserCompletedPublishedDirectPracticesInModuleParams struct {
|
||||
ModuleID pgtype.Int8 `json:"module_id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) CountUserCompletedPublishedDirectPracticesInModule(ctx context.Context, arg CountUserCompletedPublishedDirectPracticesInModuleParams) (int32, error) {
|
||||
row := q.db.QueryRow(ctx, CountUserCompletedPublishedDirectPracticesInModule, arg.ModuleID, arg.UserID)
|
||||
var n int32
|
||||
err := row.Scan(&n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
const CountUserCompletedPublishedPracticesInCourse = `-- name: CountUserCompletedPublishedPracticesInCourse :one
|
||||
SELECT
|
||||
count(*)::int AS n
|
||||
|
|
|
|||
|
|
@ -102,3 +102,35 @@ func (s *Store) LmsUserLessonProgressInProgram(ctx context.Context, userID, prog
|
|||
}
|
||||
return completed, total, nil
|
||||
}
|
||||
|
||||
func (s *Store) LmsUserDirectPracticeProgressInModule(ctx context.Context, userID, moduleID int64) (completed, total int32, err error) {
|
||||
moduleIDPG := toPgInt8(&moduleID)
|
||||
total, err = s.queries.CountPublishedDirectPracticesInModule(ctx, moduleIDPG)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
completed, err = s.queries.CountUserCompletedPublishedDirectPracticesInModule(ctx, dbgen.CountUserCompletedPublishedDirectPracticesInModuleParams{
|
||||
ModuleID: moduleIDPG,
|
||||
UserID: userID,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
return completed, total, nil
|
||||
}
|
||||
|
||||
func (s *Store) LmsUserDirectPracticeProgressInCourse(ctx context.Context, userID, courseID int64) (completed, total int32, err error) {
|
||||
courseIDPG := toPgInt8(&courseID)
|
||||
total, err = s.queries.CountPublishedDirectPracticesInCourse(ctx, courseIDPG)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
completed, err = s.queries.CountUserCompletedPublishedDirectPracticesInCourse(ctx, dbgen.CountUserCompletedPublishedDirectPracticesInCourseParams{
|
||||
CourseID: courseIDPG,
|
||||
UserID: userID,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
return completed, total, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -152,11 +152,10 @@ func (s *Service) ApplyAccessProgram(ctx context.Context, role domain.Role, user
|
|||
p.Access = nil
|
||||
return nil
|
||||
}
|
||||
comp, tot, err := s.store.LmsUserLessonProgressInProgram(ctx, userID, p.ID)
|
||||
fraction, done, comp, tot, err := s.lmsProgramProgress(ctx, userID, p.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
done := lmsProgressComplete(comp, tot)
|
||||
ok, reason := true, ""
|
||||
if role.UsesLMSSequentialGating() {
|
||||
ok, reason, err = s.CanAccessProgram(ctx, userID, p.ID)
|
||||
|
|
@ -164,7 +163,7 @@ func (s *Service) ApplyAccessProgram(ctx context.Context, role domain.Role, user
|
|||
return err
|
||||
}
|
||||
}
|
||||
p.Access = buildLMSEntityAccess(ok, reason, done, comp, tot)
|
||||
p.Access = buildLMSEntityAccessFromFraction(ok, reason, done, comp, tot, fraction)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -174,11 +173,10 @@ func (s *Service) ApplyAccessCourse(ctx context.Context, role domain.Role, userI
|
|||
c.Access = nil
|
||||
return nil
|
||||
}
|
||||
comp, tot, err := s.store.LmsUserLessonProgressInCourse(ctx, userID, c.ID)
|
||||
fraction, done, comp, tot, err := s.lmsCourseProgress(ctx, userID, c.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
done := lmsProgressComplete(comp, tot)
|
||||
ok, reason := true, ""
|
||||
if role.UsesLMSSequentialGating() {
|
||||
ok, reason, err = s.CanAccessCourse(ctx, userID, c.ID)
|
||||
|
|
@ -186,7 +184,7 @@ func (s *Service) ApplyAccessCourse(ctx context.Context, role domain.Role, userI
|
|||
return err
|
||||
}
|
||||
}
|
||||
c.Access = buildLMSEntityAccess(ok, reason, done, comp, tot)
|
||||
c.Access = buildLMSEntityAccessFromFraction(ok, reason, done, comp, tot, fraction)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -196,11 +194,10 @@ func (s *Service) ApplyAccessModule(ctx context.Context, role domain.Role, userI
|
|||
m.Access = nil
|
||||
return nil
|
||||
}
|
||||
comp, tot, err := s.store.LmsUserLessonProgressInModule(ctx, userID, m.ID)
|
||||
fraction, done, comp, tot, err := s.lmsModuleProgress(ctx, userID, m.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
done := lmsProgressComplete(comp, tot)
|
||||
ok, reason := true, ""
|
||||
if role.UsesLMSSequentialGating() {
|
||||
ok, reason, err = s.CanAccessModule(ctx, userID, m.ID)
|
||||
|
|
@ -208,7 +205,7 @@ func (s *Service) ApplyAccessModule(ctx context.Context, role domain.Role, userI
|
|||
return err
|
||||
}
|
||||
}
|
||||
m.Access = buildLMSEntityAccess(ok, reason, done, comp, tot)
|
||||
m.Access = buildLMSEntityAccessFromFraction(ok, reason, done, comp, tot, fraction)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -218,11 +215,10 @@ func (s *Service) ApplyAccessLesson(ctx context.Context, role domain.Role, userI
|
|||
les.Access = nil
|
||||
return nil
|
||||
}
|
||||
comp, tot, err := s.store.LmsUserPracticeProgressInLesson(ctx, userID, les.ID)
|
||||
fraction, done, comp, tot, err := s.lmsLessonProgress(ctx, userID, les.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
done := lmsProgressComplete(comp, tot)
|
||||
ok, reason := true, ""
|
||||
if role.UsesLMSSequentialGating() {
|
||||
ok, reason, err = s.CanAccessLesson(ctx, userID, les.ID)
|
||||
|
|
@ -230,7 +226,7 @@ func (s *Service) ApplyAccessLesson(ctx context.Context, role domain.Role, userI
|
|||
return err
|
||||
}
|
||||
}
|
||||
les.Access = buildLMSEntityAccess(ok, reason, done, comp, tot)
|
||||
les.Access = buildLMSEntityAccessFromFraction(ok, reason, done, comp, tot, fraction)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -240,12 +236,11 @@ func (s *Service) ApplyExamPrepAccessCatalogCourse(ctx context.Context, role dom
|
|||
cc.Access = nil
|
||||
return nil
|
||||
}
|
||||
comp, tot, err := s.store.ExamPrepUserPracticeProgressInCatalogCourse(ctx, userID, cc.ID)
|
||||
fraction, done, comp, tot, err := s.examPrepCatalogCourseProgress(ctx, userID, cc.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
done := lmsProgressComplete(comp, tot)
|
||||
cc.Access = buildLMSEntityAccess(true, "", done, comp, tot)
|
||||
cc.Access = buildLMSEntityAccessFromFraction(true, "", done, comp, tot, fraction)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -255,12 +250,11 @@ func (s *Service) ApplyExamPrepAccessUnit(ctx context.Context, role domain.Role,
|
|||
u.Access = nil
|
||||
return nil
|
||||
}
|
||||
comp, tot, err := s.store.ExamPrepUserPracticeProgressInUnit(ctx, userID, u.ID)
|
||||
fraction, done, comp, tot, err := s.examPrepUnitProgress(ctx, userID, u.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
done := lmsProgressComplete(comp, tot)
|
||||
u.Access = buildLMSEntityAccess(true, "", done, comp, tot)
|
||||
u.Access = buildLMSEntityAccessFromFraction(true, "", done, comp, tot, fraction)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -270,12 +264,11 @@ func (s *Service) ApplyExamPrepAccessModule(ctx context.Context, role domain.Rol
|
|||
m.Access = nil
|
||||
return nil
|
||||
}
|
||||
comp, tot, err := s.store.ExamPrepUserPracticeProgressInModule(ctx, userID, m.ID)
|
||||
fraction, done, comp, tot, err := s.examPrepModuleProgress(ctx, userID, m.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
done := lmsProgressComplete(comp, tot)
|
||||
m.Access = buildLMSEntityAccess(true, "", done, comp, tot)
|
||||
m.Access = buildLMSEntityAccessFromFraction(true, "", done, comp, tot, fraction)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -285,15 +278,218 @@ func (s *Service) ApplyExamPrepAccessLesson(ctx context.Context, role domain.Rol
|
|||
les.Access = nil
|
||||
return nil
|
||||
}
|
||||
comp, tot, err := s.store.ExamPrepUserPracticeProgressInLesson(ctx, userID, les.ID)
|
||||
fraction, done, comp, tot, err := s.examPrepLessonProgress(ctx, userID, les.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
done := lmsProgressComplete(comp, tot)
|
||||
les.Access = buildLMSEntityAccess(true, "", done, comp, tot)
|
||||
les.Access = buildLMSEntityAccessFromFraction(true, "", done, comp, tot, fraction)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) lmsLessonProgress(ctx context.Context, userID, lessonID int64) (fraction float64, done bool, completed, total int32, err error) {
|
||||
completed, total, err = s.store.LmsUserPracticeProgressInLesson(ctx, userID, lessonID)
|
||||
if err != nil {
|
||||
return 0, false, 0, 0, err
|
||||
}
|
||||
// Lesson is complete once any published practice in that lesson is completed.
|
||||
if total > 0 && completed > 0 {
|
||||
return 1, true, 1, 1, nil
|
||||
}
|
||||
return 0, false, 0, 1, nil
|
||||
}
|
||||
|
||||
func (s *Service) lmsModuleProgress(ctx context.Context, userID, moduleID int64) (fraction float64, done bool, completed, total int32, err error) {
|
||||
directDone, directErr := s.hasCompletedDirectModulePractice(ctx, userID, moduleID)
|
||||
if directErr != nil {
|
||||
return 0, false, 0, 0, directErr
|
||||
}
|
||||
if directDone {
|
||||
return 1, true, 1, 1, nil
|
||||
}
|
||||
|
||||
lessons, _, err := s.store.ListLessonsByModuleID(ctx, moduleID, true, 10000, 0)
|
||||
if err != nil {
|
||||
return 0, false, 0, 0, err
|
||||
}
|
||||
if len(lessons) == 0 {
|
||||
return 0, false, 0, 0, nil
|
||||
}
|
||||
|
||||
var doneLessons int32
|
||||
for _, lesson := range lessons {
|
||||
lessonFraction, _, _, _, err := s.lmsLessonProgress(ctx, userID, lesson.ID)
|
||||
if err != nil {
|
||||
return 0, false, 0, 0, err
|
||||
}
|
||||
if lessonFraction >= 1 {
|
||||
doneLessons++
|
||||
}
|
||||
}
|
||||
total = int32(len(lessons))
|
||||
fraction = float64(doneLessons) / float64(total)
|
||||
return fraction, fraction >= 1, doneLessons, total, nil
|
||||
}
|
||||
|
||||
func (s *Service) lmsCourseProgress(ctx context.Context, userID, courseID int64) (fraction float64, done bool, completed, total int32, err error) {
|
||||
directDone, directErr := s.hasCompletedDirectCoursePractice(ctx, userID, courseID)
|
||||
if directErr != nil {
|
||||
return 0, false, 0, 0, directErr
|
||||
}
|
||||
if directDone {
|
||||
return 1, true, 1, 1, nil
|
||||
}
|
||||
|
||||
moduleIDs, err := s.store.ListModuleIDsByCourse(ctx, courseID)
|
||||
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
|
||||
for _, moduleID := range moduleIDs {
|
||||
moduleFraction, _, _, _, err := s.lmsModuleProgress(ctx, userID, moduleID)
|
||||
if err != nil {
|
||||
return 0, false, 0, 0, err
|
||||
}
|
||||
sum += moduleFraction
|
||||
if moduleFraction >= 1 {
|
||||
fullDone++
|
||||
}
|
||||
}
|
||||
total = int32(len(moduleIDs))
|
||||
fraction = sum / float64(total)
|
||||
return fraction, fraction >= 1, fullDone, total, nil
|
||||
}
|
||||
|
||||
func (s *Service) lmsProgramProgress(ctx context.Context, userID, programID int64) (fraction float64, done bool, completed, total int32, err error) {
|
||||
courseIDs, err := s.store.ListCourseIDsByProgram(ctx, programID)
|
||||
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
|
||||
for _, courseID := range courseIDs {
|
||||
courseFraction, _, _, _, err := s.lmsCourseProgress(ctx, userID, courseID)
|
||||
if err != nil {
|
||||
return 0, false, 0, 0, err
|
||||
}
|
||||
sum += courseFraction
|
||||
if courseFraction >= 1 {
|
||||
fullDone++
|
||||
}
|
||||
}
|
||||
total = int32(len(courseIDs))
|
||||
fraction = sum / float64(total)
|
||||
return fraction, fraction >= 1, fullDone, total, nil
|
||||
}
|
||||
|
||||
func (s *Service) hasCompletedDirectModulePractice(ctx context.Context, userID, moduleID int64) (bool, error) {
|
||||
completed, total, err := s.store.LmsUserDirectPracticeProgressInModule(ctx, userID, moduleID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return total > 0 && completed > 0, nil
|
||||
}
|
||||
|
||||
func (s *Service) hasCompletedDirectCoursePractice(ctx context.Context, userID, courseID int64) (bool, error) {
|
||||
completed, total, err := s.store.LmsUserDirectPracticeProgressInCourse(ctx, userID, courseID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return total > 0 && completed > 0, nil
|
||||
}
|
||||
|
||||
func (s *Service) examPrepLessonProgress(ctx context.Context, userID, lessonID int64) (fraction float64, done bool, completed, total int32, err error) {
|
||||
completed, total, err = s.store.ExamPrepUserPracticeProgressInLesson(ctx, userID, lessonID)
|
||||
if err != nil {
|
||||
return 0, false, 0, 0, err
|
||||
}
|
||||
if total > 0 && completed > 0 {
|
||||
return 1, true, 1, 1, nil
|
||||
}
|
||||
return 0, false, 0, 1, nil
|
||||
}
|
||||
|
||||
func (s *Service) examPrepModuleProgress(ctx context.Context, userID, moduleID int64) (fraction float64, done bool, completed, total int32, err error) {
|
||||
lessonIDs, err := s.store.ListExamPrepUnitModuleLessonIDsByUnitModule(ctx, moduleID)
|
||||
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 {
|
||||
lessonFraction, _, _, _, err := s.examPrepLessonProgress(ctx, userID, lessonID)
|
||||
if err != nil {
|
||||
return 0, false, 0, 0, err
|
||||
}
|
||||
if lessonFraction >= 1 {
|
||||
doneLessons++
|
||||
}
|
||||
}
|
||||
total = int32(len(lessonIDs))
|
||||
fraction = float64(doneLessons) / float64(total)
|
||||
return fraction, fraction >= 1, doneLessons, total, nil
|
||||
}
|
||||
|
||||
func (s *Service) examPrepUnitProgress(ctx context.Context, userID, unitID int64) (fraction float64, done bool, completed, total int32, err error) {
|
||||
moduleIDs, err := s.store.ListExamPrepUnitModuleIDsByUnit(ctx, unitID)
|
||||
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
|
||||
for _, moduleID := range moduleIDs {
|
||||
moduleFraction, _, _, _, err := s.examPrepModuleProgress(ctx, userID, moduleID)
|
||||
if err != nil {
|
||||
return 0, false, 0, 0, err
|
||||
}
|
||||
sum += moduleFraction
|
||||
if moduleFraction >= 1 {
|
||||
fullDone++
|
||||
}
|
||||
}
|
||||
total = int32(len(moduleIDs))
|
||||
fraction = sum / float64(total)
|
||||
return fraction, fraction >= 1, fullDone, total, nil
|
||||
}
|
||||
|
||||
func (s *Service) examPrepCatalogCourseProgress(ctx context.Context, userID, catalogCourseID int64) (fraction float64, done bool, completed, total int32, err error) {
|
||||
unitIDs, err := s.store.ListExamPrepUnitIDsByCatalogCourse(ctx, catalogCourseID)
|
||||
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
|
||||
for _, unitID := range unitIDs {
|
||||
unitFraction, _, _, _, err := s.examPrepUnitProgress(ctx, userID, unitID)
|
||||
if err != nil {
|
||||
return 0, false, 0, 0, err
|
||||
}
|
||||
sum += unitFraction
|
||||
if unitFraction >= 1 {
|
||||
fullDone++
|
||||
}
|
||||
}
|
||||
total = int32(len(unitIDs))
|
||||
fraction = sum / float64(total)
|
||||
return fraction, fraction >= 1, fullDone, total, nil
|
||||
}
|
||||
|
||||
func lmsProgressComplete(completed, total int32) bool {
|
||||
return total > 0 && completed >= total
|
||||
}
|
||||
|
|
@ -311,6 +507,41 @@ func buildLMSEntityAccess(ok bool, reason string, done bool, completed, total in
|
|||
}
|
||||
}
|
||||
|
||||
func buildLMSEntityAccessFromFraction(ok bool, reason string, done bool, completed, total int32, fraction float64) *domain.LMSEntityAccess {
|
||||
c := int(completed)
|
||||
t := int(total)
|
||||
if c < 0 {
|
||||
c = 0
|
||||
}
|
||||
if t < 0 {
|
||||
t = 0
|
||||
}
|
||||
if done && t > 0 {
|
||||
c = t
|
||||
fraction = 1
|
||||
}
|
||||
if fraction < 0 {
|
||||
fraction = 0
|
||||
}
|
||||
if fraction > 1 {
|
||||
fraction = 1
|
||||
}
|
||||
pctPrecise := math.Round(fraction*10000) / 100
|
||||
pct := int(pctPrecise)
|
||||
if pct > 100 {
|
||||
pct = 100
|
||||
}
|
||||
return &domain.LMSEntityAccess{
|
||||
IsAccessible: ok,
|
||||
IsCompleted: done,
|
||||
Reason: reasonIf(ok, reason),
|
||||
CompletedCount: c,
|
||||
TotalCount: t,
|
||||
ProgressPercent: pct,
|
||||
ProgressPercentPrecise: pctPrecise,
|
||||
}
|
||||
}
|
||||
|
||||
// lmsProgressCounts maps DB lesson completion counts to UI fields. Percent is 0–100; completed
|
||||
// and total are aligned with isCompleted when the entity is fully done.
|
||||
func lmsProgressCounts(completed, total int32, isCompleted bool) (c, t, pct int, pctPrecise float64) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user