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 <cursoragent@cursor.com>
611 lines
18 KiB
Go
611 lines
18 KiB
Go
package lmsprogress
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"math"
|
||
|
||
"Yimaru-Backend/internal/domain"
|
||
"Yimaru-Backend/internal/repository"
|
||
|
||
"github.com/jackc/pgx/v5"
|
||
)
|
||
|
||
const (
|
||
errPrevProgram = "Complete the previous program before accessing this one."
|
||
errPrevCourse = "Complete the previous course in this program first."
|
||
errPrevModule = "Complete the previous module in this course first."
|
||
errPrevLesson = "Complete the previous lesson in this module first."
|
||
)
|
||
|
||
// Service enforces sequential LMS access for learners and records lesson progress.
|
||
type Service struct {
|
||
store *repository.Store
|
||
}
|
||
|
||
func NewService(store *repository.Store) *Service {
|
||
return &Service{store: store}
|
||
}
|
||
|
||
// CompleteLessonForUser records lesson completion and rolls up to module, course, and program when applicable.
|
||
func (s *Service) CompleteLessonForUser(ctx context.Context, userID, lessonID int64) error {
|
||
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)
|
||
}
|
||
|
||
// CanAccessProgram returns whether the user may use content under this program (previous program must be fully completed if any).
|
||
func (s *Service) CanAccessProgram(ctx context.Context, userID, programID int64) (ok bool, reason string, err error) {
|
||
if _, err := s.store.GetProgramByID(ctx, programID); err != nil {
|
||
return false, "", err
|
||
}
|
||
prev, err := s.store.LmsGetPreviousProgram(ctx, programID)
|
||
if err != nil {
|
||
if errors.Is(err, pgx.ErrNoRows) {
|
||
return true, "", nil
|
||
}
|
||
return false, "", err
|
||
}
|
||
has, err := s.store.LmsUserHasProgramProgress(ctx, userID, prev.ID)
|
||
if err != nil {
|
||
return false, "", err
|
||
}
|
||
if !has {
|
||
return false, errPrevProgram, nil
|
||
}
|
||
return true, "", nil
|
||
}
|
||
|
||
// CanAccessCourse requires the parent program to be accessible and the previous course in the program to be completed.
|
||
func (s *Service) CanAccessCourse(ctx context.Context, userID, courseID int64) (ok bool, reason string, err error) {
|
||
c, err := s.store.GetCourseByID(ctx, courseID)
|
||
if err != nil {
|
||
return false, "", err
|
||
}
|
||
ok, reason, err = s.CanAccessProgram(ctx, userID, c.ProgramID)
|
||
if err != nil || !ok {
|
||
return ok, reason, err
|
||
}
|
||
prev, err := s.store.LmsGetPreviousCourseInProgram(ctx, courseID)
|
||
if err != nil {
|
||
if errors.Is(err, pgx.ErrNoRows) {
|
||
return true, "", nil
|
||
}
|
||
return false, "", err
|
||
}
|
||
has, err := s.store.LmsUserHasCourseProgress(ctx, userID, prev.ID)
|
||
if err != nil {
|
||
return false, "", err
|
||
}
|
||
if !has {
|
||
return false, errPrevCourse, nil
|
||
}
|
||
return true, "", nil
|
||
}
|
||
|
||
// CanAccessModule requires the course (and its program chain) to be accessible and the previous module in the course to be completed.
|
||
func (s *Service) CanAccessModule(ctx context.Context, userID, moduleID int64) (ok bool, reason string, err error) {
|
||
m, err := s.store.GetModuleByID(ctx, moduleID)
|
||
if err != nil {
|
||
return false, "", err
|
||
}
|
||
ok, reason, err = s.CanAccessCourse(ctx, userID, m.CourseID)
|
||
if err != nil || !ok {
|
||
return ok, reason, err
|
||
}
|
||
prev, err := s.store.LmsGetPreviousModuleInCourse(ctx, moduleID)
|
||
if err != nil {
|
||
if errors.Is(err, pgx.ErrNoRows) {
|
||
return true, "", nil
|
||
}
|
||
return false, "", err
|
||
}
|
||
has, err := s.store.LmsUserHasModuleProgress(ctx, userID, prev.ID)
|
||
if err != nil {
|
||
return false, "", err
|
||
}
|
||
if !has {
|
||
return false, errPrevModule, nil
|
||
}
|
||
return true, "", nil
|
||
}
|
||
|
||
// CanAccessLesson requires the module chain to be accessible and the previous lesson in the module
|
||
// to be completed based on published practice completion in that lesson.
|
||
func (s *Service) CanAccessLesson(ctx context.Context, userID, lessonID int64) (ok bool, reason string, err error) {
|
||
lesson, err := s.store.GetLessonByID(ctx, lessonID)
|
||
if err != nil {
|
||
return false, "", err
|
||
}
|
||
ok, reason, err = s.CanAccessModule(ctx, userID, lesson.ModuleID)
|
||
if err != nil || !ok {
|
||
return ok, reason, err
|
||
}
|
||
prev, err := s.store.LmsGetPreviousLessonInModule(ctx, lessonID)
|
||
if err != nil {
|
||
if errors.Is(err, pgx.ErrNoRows) {
|
||
return true, "", nil
|
||
}
|
||
return false, "", err
|
||
}
|
||
|
||
// Lesson unlock for STUDENT now follows practice completion, not deprecated lesson-complete writes.
|
||
prevCompletedPractices, prevTotalPractices, err := s.store.LmsUserPracticeProgressInLesson(ctx, userID, prev.ID)
|
||
if err != nil {
|
||
return false, "", err
|
||
}
|
||
if !lmsProgressComplete(prevCompletedPractices, prevTotalPractices) {
|
||
return false, errPrevLesson, nil
|
||
}
|
||
return true, "", nil
|
||
}
|
||
|
||
// ApplyAccessProgram sets p.Access for learner roles. Staff roles omit Access from JSON.
|
||
// STUDENT: is_accessible reflects sequential prerequisites; OPEN_LEARNER: always true.
|
||
func (s *Service) ApplyAccessProgram(ctx context.Context, role domain.Role, userID int64, p *domain.Program) error {
|
||
if !role.IsCustomerLearnerRole() {
|
||
p.Access = nil
|
||
return nil
|
||
}
|
||
fraction, done, comp, tot, err := s.lmsProgramProgress(ctx, userID, p.ID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
ok, reason := true, ""
|
||
if role.UsesLMSSequentialGating() {
|
||
ok, reason, err = s.CanAccessProgram(ctx, userID, p.ID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
}
|
||
p.Access = buildLMSEntityAccessFromFraction(ok, reason, done, comp, tot, fraction)
|
||
return nil
|
||
}
|
||
|
||
// ApplyAccessCourse sets c.Access for learner roles.
|
||
func (s *Service) ApplyAccessCourse(ctx context.Context, role domain.Role, userID int64, c *domain.Course) error {
|
||
if !role.IsCustomerLearnerRole() {
|
||
c.Access = nil
|
||
return nil
|
||
}
|
||
fraction, done, comp, tot, err := s.lmsCourseProgress(ctx, userID, c.ID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
ok, reason := true, ""
|
||
if role.UsesLMSSequentialGating() {
|
||
ok, reason, err = s.CanAccessCourse(ctx, userID, c.ID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
}
|
||
c.Access = buildLMSEntityAccessFromFraction(ok, reason, done, comp, tot, fraction)
|
||
return nil
|
||
}
|
||
|
||
// ApplyAccessModule sets m.Access for learner roles.
|
||
func (s *Service) ApplyAccessModule(ctx context.Context, role domain.Role, userID int64, m *domain.Module) error {
|
||
if !role.IsCustomerLearnerRole() {
|
||
m.Access = nil
|
||
return nil
|
||
}
|
||
fraction, done, comp, tot, err := s.lmsModuleProgress(ctx, userID, m.ID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
ok, reason := true, ""
|
||
if role.UsesLMSSequentialGating() {
|
||
ok, reason, err = s.CanAccessModule(ctx, userID, m.ID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
}
|
||
m.Access = buildLMSEntityAccessFromFraction(ok, reason, done, comp, tot, fraction)
|
||
return nil
|
||
}
|
||
|
||
// ApplyAccessLesson sets l.Access for learner roles.
|
||
func (s *Service) ApplyAccessLesson(ctx context.Context, role domain.Role, userID int64, les *domain.Lesson) error {
|
||
if !role.IsCustomerLearnerRole() {
|
||
les.Access = nil
|
||
return nil
|
||
}
|
||
fraction, done, comp, tot, err := s.lmsLessonProgress(ctx, userID, les.ID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
ok, reason := true, ""
|
||
if role.UsesLMSSequentialGating() {
|
||
ok, reason, err = s.CanAccessLesson(ctx, userID, les.ID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
}
|
||
les.Access = buildLMSEntityAccessFromFraction(ok, reason, done, comp, tot, fraction)
|
||
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
|
||
}
|
||
fraction, done, comp, tot, err := s.examPrepCatalogCourseProgress(ctx, userID, cc.ID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
cc.Access = buildLMSEntityAccessFromFraction(true, "", done, comp, tot, fraction)
|
||
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
|
||
}
|
||
fraction, done, comp, tot, err := s.examPrepUnitProgress(ctx, userID, u.ID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
u.Access = buildLMSEntityAccessFromFraction(true, "", done, comp, tot, fraction)
|
||
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
|
||
}
|
||
fraction, done, comp, tot, err := s.examPrepModuleProgress(ctx, userID, m.ID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
m.Access = buildLMSEntityAccessFromFraction(true, "", done, comp, tot, fraction)
|
||
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
|
||
}
|
||
fraction, done, comp, tot, err := s.examPrepLessonProgress(ctx, userID, les.ID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
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
|
||
}
|
||
var doneLessons int32
|
||
for _, lesson := range lessons {
|
||
practiceCount, err := s.store.LmsCountPublishedPracticesInLesson(ctx, lesson.ID)
|
||
if err != nil {
|
||
return 0, false, 0, 0, err
|
||
}
|
||
if practiceCount == 0 {
|
||
continue
|
||
}
|
||
total++
|
||
lessonFraction, _, _, _, err := s.lmsLessonProgress(ctx, userID, lesson.ID)
|
||
if err != nil {
|
||
return 0, false, 0, 0, err
|
||
}
|
||
if lessonFraction >= 1 {
|
||
doneLessons++
|
||
}
|
||
}
|
||
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) {
|
||
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
|
||
}
|
||
|
||
var doneModules int32
|
||
for _, moduleID := range moduleIDs {
|
||
practiceCount, err := s.store.LmsCountPublishedPracticesInModule(ctx, moduleID)
|
||
if err != nil {
|
||
return 0, false, 0, 0, err
|
||
}
|
||
if practiceCount == 0 {
|
||
continue
|
||
}
|
||
total++
|
||
_, moduleDone, _, _, err := s.lmsModuleProgress(ctx, userID, moduleID)
|
||
if err != nil {
|
||
return 0, false, 0, 0, err
|
||
}
|
||
if moduleDone {
|
||
doneModules++
|
||
}
|
||
}
|
||
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) {
|
||
courseIDs, err := s.store.ListCourseIDsByProgram(ctx, programID)
|
||
if err != nil {
|
||
return 0, false, 0, 0, err
|
||
}
|
||
|
||
var doneCourses int32
|
||
for _, courseID := range courseIDs {
|
||
practiceCount, err := s.store.LmsCountPublishedPracticesInCourse(ctx, courseID)
|
||
if err != nil {
|
||
return 0, false, 0, 0, err
|
||
}
|
||
if practiceCount == 0 {
|
||
continue
|
||
}
|
||
total++
|
||
_, courseDone, _, _, err := s.lmsCourseProgress(ctx, userID, courseID)
|
||
if err != nil {
|
||
return 0, false, 0, 0, err
|
||
}
|
||
if courseDone {
|
||
doneCourses++
|
||
}
|
||
}
|
||
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) {
|
||
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
|
||
}
|
||
|
||
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
|
||
}
|
||
if lessonFraction >= 1 {
|
||
doneLessons++
|
||
}
|
||
}
|
||
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) {
|
||
moduleIDs, err := s.store.ListExamPrepUnitModuleIDsByUnit(ctx, unitID)
|
||
if err != nil {
|
||
return 0, false, 0, 0, err
|
||
}
|
||
|
||
var doneModules int32
|
||
for _, moduleID := range moduleIDs {
|
||
practiceCount, err := s.store.ExamPrepCountPublishedPracticesInModule(ctx, moduleID)
|
||
if err != nil {
|
||
return 0, false, 0, 0, err
|
||
}
|
||
if practiceCount == 0 {
|
||
continue
|
||
}
|
||
total++
|
||
_, moduleDone, _, _, err := s.examPrepModuleProgress(ctx, userID, moduleID)
|
||
if err != nil {
|
||
return 0, false, 0, 0, err
|
||
}
|
||
if moduleDone {
|
||
doneModules++
|
||
}
|
||
}
|
||
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) {
|
||
unitIDs, err := s.store.ListExamPrepUnitIDsByCatalogCourse(ctx, catalogCourseID)
|
||
if err != nil {
|
||
return 0, false, 0, 0, err
|
||
}
|
||
|
||
var doneUnits int32
|
||
for _, unitID := range unitIDs {
|
||
practiceCount, err := s.store.ExamPrepCountPublishedPracticesInUnit(ctx, unitID)
|
||
if err != nil {
|
||
return 0, false, 0, 0, err
|
||
}
|
||
if practiceCount == 0 {
|
||
continue
|
||
}
|
||
total++
|
||
_, unitDone, _, _, err := s.examPrepUnitProgress(ctx, userID, unitID)
|
||
if err != nil {
|
||
return 0, false, 0, 0, err
|
||
}
|
||
if unitDone {
|
||
doneUnits++
|
||
}
|
||
}
|
||
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 {
|
||
return total > 0 && completed >= total
|
||
}
|
||
|
||
func buildLMSEntityAccess(ok bool, reason string, done bool, completed, total int32) *domain.LMSEntityAccess {
|
||
c, t, pct, pctPrecise := lmsProgressCounts(completed, total, done)
|
||
return &domain.LMSEntityAccess{
|
||
IsAccessible: ok,
|
||
IsCompleted: done,
|
||
Reason: reasonIf(ok, reason),
|
||
CompletedCount: c,
|
||
TotalCount: t,
|
||
ProgressPercent: pct,
|
||
ProgressPercentPrecise: pctPrecise,
|
||
}
|
||
}
|
||
|
||
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) {
|
||
c, t = int(completed), int(total)
|
||
if t < 0 {
|
||
t = 0
|
||
}
|
||
if c < 0 {
|
||
c = 0
|
||
}
|
||
if isCompleted {
|
||
if t > 0 {
|
||
return t, t, 100, 100
|
||
}
|
||
return c, t, 100, 100
|
||
}
|
||
if t == 0 {
|
||
return 0, 0, 0, 0
|
||
}
|
||
pct = (c * 100) / t
|
||
if pct > 100 {
|
||
pct = 100
|
||
}
|
||
pctPrecise = math.Round((float64(c)*10000)/float64(t)) / 100
|
||
if pctPrecise > 100 {
|
||
pctPrecise = 100
|
||
}
|
||
return c, t, pct, pctPrecise
|
||
}
|
||
|
||
func reasonIf(ok bool, r string) string {
|
||
if ok {
|
||
return ""
|
||
}
|
||
return r
|
||
}
|