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 qs.status = 'PUBLISHED'
|
||||||
AND lp.publish_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
|
-- name: GetPracticeScopeByQuestionSetID :one
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
|
|
|
||||||
|
|
@ -99,6 +99,48 @@ func (q *Queries) CountModulesInCourse(ctx context.Context, courseID int64) (int
|
||||||
return n, err
|
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
|
const CountPublishedPracticesInCourse = `-- name: CountPublishedPracticesInCourse :one
|
||||||
SELECT
|
SELECT
|
||||||
count(*)::int AS n
|
count(*)::int AS n
|
||||||
|
|
@ -349,6 +391,62 @@ func (q *Queries) CountUserCompletedModulesInCourse(ctx context.Context, arg Cou
|
||||||
return n, err
|
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
|
const CountUserCompletedPublishedPracticesInCourse = `-- name: CountUserCompletedPublishedPracticesInCourse :one
|
||||||
SELECT
|
SELECT
|
||||||
count(*)::int AS n
|
count(*)::int AS n
|
||||||
|
|
|
||||||
|
|
@ -102,3 +102,35 @@ func (s *Store) LmsUserLessonProgressInProgram(ctx context.Context, userID, prog
|
||||||
}
|
}
|
||||||
return completed, total, nil
|
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
|
p.Access = nil
|
||||||
return 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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
done := lmsProgressComplete(comp, tot)
|
|
||||||
ok, reason := true, ""
|
ok, reason := true, ""
|
||||||
if role.UsesLMSSequentialGating() {
|
if role.UsesLMSSequentialGating() {
|
||||||
ok, reason, err = s.CanAccessProgram(ctx, userID, p.ID)
|
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
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
p.Access = buildLMSEntityAccess(ok, reason, done, comp, tot)
|
p.Access = buildLMSEntityAccessFromFraction(ok, reason, done, comp, tot, fraction)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -174,11 +173,10 @@ func (s *Service) ApplyAccessCourse(ctx context.Context, role domain.Role, userI
|
||||||
c.Access = nil
|
c.Access = nil
|
||||||
return 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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
done := lmsProgressComplete(comp, tot)
|
|
||||||
ok, reason := true, ""
|
ok, reason := true, ""
|
||||||
if role.UsesLMSSequentialGating() {
|
if role.UsesLMSSequentialGating() {
|
||||||
ok, reason, err = s.CanAccessCourse(ctx, userID, c.ID)
|
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
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
c.Access = buildLMSEntityAccess(ok, reason, done, comp, tot)
|
c.Access = buildLMSEntityAccessFromFraction(ok, reason, done, comp, tot, fraction)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -196,11 +194,10 @@ func (s *Service) ApplyAccessModule(ctx context.Context, role domain.Role, userI
|
||||||
m.Access = nil
|
m.Access = nil
|
||||||
return 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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
done := lmsProgressComplete(comp, tot)
|
|
||||||
ok, reason := true, ""
|
ok, reason := true, ""
|
||||||
if role.UsesLMSSequentialGating() {
|
if role.UsesLMSSequentialGating() {
|
||||||
ok, reason, err = s.CanAccessModule(ctx, userID, m.ID)
|
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
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
m.Access = buildLMSEntityAccess(ok, reason, done, comp, tot)
|
m.Access = buildLMSEntityAccessFromFraction(ok, reason, done, comp, tot, fraction)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -218,11 +215,10 @@ func (s *Service) ApplyAccessLesson(ctx context.Context, role domain.Role, userI
|
||||||
les.Access = nil
|
les.Access = nil
|
||||||
return 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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
done := lmsProgressComplete(comp, tot)
|
|
||||||
ok, reason := true, ""
|
ok, reason := true, ""
|
||||||
if role.UsesLMSSequentialGating() {
|
if role.UsesLMSSequentialGating() {
|
||||||
ok, reason, err = s.CanAccessLesson(ctx, userID, les.ID)
|
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
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
les.Access = buildLMSEntityAccess(ok, reason, done, comp, tot)
|
les.Access = buildLMSEntityAccessFromFraction(ok, reason, done, comp, tot, fraction)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -240,12 +236,11 @@ func (s *Service) ApplyExamPrepAccessCatalogCourse(ctx context.Context, role dom
|
||||||
cc.Access = nil
|
cc.Access = nil
|
||||||
return 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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
done := lmsProgressComplete(comp, tot)
|
cc.Access = buildLMSEntityAccessFromFraction(true, "", done, comp, tot, fraction)
|
||||||
cc.Access = buildLMSEntityAccess(true, "", done, comp, tot)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -255,12 +250,11 @@ func (s *Service) ApplyExamPrepAccessUnit(ctx context.Context, role domain.Role,
|
||||||
u.Access = nil
|
u.Access = nil
|
||||||
return 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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
done := lmsProgressComplete(comp, tot)
|
u.Access = buildLMSEntityAccessFromFraction(true, "", done, comp, tot, fraction)
|
||||||
u.Access = buildLMSEntityAccess(true, "", done, comp, tot)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -270,12 +264,11 @@ func (s *Service) ApplyExamPrepAccessModule(ctx context.Context, role domain.Rol
|
||||||
m.Access = nil
|
m.Access = nil
|
||||||
return 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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
done := lmsProgressComplete(comp, tot)
|
m.Access = buildLMSEntityAccessFromFraction(true, "", done, comp, tot, fraction)
|
||||||
m.Access = buildLMSEntityAccess(true, "", done, comp, tot)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -285,15 +278,218 @@ func (s *Service) ApplyExamPrepAccessLesson(ctx context.Context, role domain.Rol
|
||||||
les.Access = nil
|
les.Access = nil
|
||||||
return 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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
done := lmsProgressComplete(comp, tot)
|
les.Access = buildLMSEntityAccessFromFraction(true, "", done, comp, tot, fraction)
|
||||||
les.Access = buildLMSEntityAccess(true, "", done, comp, tot)
|
|
||||||
return nil
|
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 {
|
func lmsProgressComplete(completed, total int32) bool {
|
||||||
return total > 0 && completed >= total
|
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
|
// 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.
|
// 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) {
|
func lmsProgressCounts(completed, total int32, isCompleted bool) (c, t, pct int, pctPrecise float64) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user