diff --git a/scripts/langindex.json b/scripts/langindex.json index e96142599..8d321dcc6 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -1447,6 +1447,17 @@ "core.course.askadmintosupport": "local_moodlemobileapp", "core.course.availablespace": "local_moodlemobileapp", "core.course.cannotdeletewhiledownloading": "local_moodlemobileapp", + "core.course.completion_automatic:done": "course", + "core.course.completion_automatic:failed": "course", + "core.course.completion_automatic:todo": "course", + "core.course.completion_manual:aria:done": "course", + "core.course.completion_manual:aria:markdone": "course", + "core.course.completion_manual:markdone": "course", + "core.course.completion_setby:auto:done": "course", + "core.course.completion_setby:auto:todo": "course", + "core.course.completion_setby:manual:done": "course", + "core.course.completion_setby:manual:markdone": "course", + "core.course.completionrequirements": "course", "core.course.confirmdeletemodulefiles": "local_moodlemobileapp", "core.course.confirmdeletestoreddata": "local_moodlemobileapp", "core.course.confirmdownload": "local_moodlemobileapp", diff --git a/src/core/features/course/classes/main-resource-component.ts b/src/core/features/course/classes/main-resource-component.ts index b53613f17..f94976061 100644 --- a/src/core/features/course/classes/main-resource-component.ts +++ b/src/core/features/course/classes/main-resource-component.ts @@ -79,6 +79,7 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, protected currentStatus?: string; // The current status of the module. Only if setStatusListener is called. protected completionObserver?: CoreEventObserver; protected logger: CoreLogger; + protected debouncedUpdateModule?: () => void; // Update the module after a certain time. constructor( @Optional() @Inject('') loggerName: string = 'CoreCourseModuleMainResourceComponent', @@ -103,9 +104,13 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, if (data && data.cmId == this.module.id) { await CoreCourse.invalidateModule(this.module.id); - this.module = await CoreCourse.getModule(this.module.id, this.courseId); + this.fetchModule(); } }); + + this.debouncedUpdateModule = CoreUtils.debounce(() => { + this.fetchModule(); + }, 10000); } this.blog = await AddonBlog.isPluginEnabled(); @@ -160,7 +165,7 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, ])); if (this.showCompletion) { - this.module = await CoreCourse.getModule(this.module.id, this.courseId); + this.fetchModule(); } await this.loadContent(true); @@ -403,8 +408,23 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, * @return Promise resolved when done. */ async onCompletionChange(): Promise { - // Nothing to do. - return; + // Update the module data after a while. + this.debouncedUpdateModule?.(); + } + + /** + * Fetch module. + * + * @return Promise resolved when done. + */ + protected async fetchModule(): Promise { + const module = await CoreCourse.getModule(this.module.id, this.courseId); + + CoreCourseHelper.calculateModuleCompletionData(module, this.courseId); + + await CoreCourseHelper.loadModuleOfflineCompletion(this.courseId, module); + + this.module = module; } /** diff --git a/src/core/features/course/components/module-completion/module-completion.ts b/src/core/features/course/components/module-completion/module-completion.ts index a147c7b03..92fbb0b0e 100644 --- a/src/core/features/course/components/module-completion/module-completion.ts +++ b/src/core/features/course/components/module-completion/module-completion.ts @@ -16,6 +16,7 @@ import { Component, Input } from '@angular/core'; import { CoreCourseModuleCompletionBaseComponent } from '@features/course/classes/module-completion'; import { CoreCourseModuleWSRuleDetails, CoreCourseProvider } from '@features/course/services/course'; +import { CoreUser } from '@features/user/services/user'; import { Translate } from '@singletons'; /** @@ -43,13 +44,13 @@ export class CoreCourseModuleCompletionComponent extends CoreCourseModuleComplet /** * @inheritdoc */ - protected calculateData(): void { + protected async calculateData(): Promise { if (!this.completion?.details) { return; } // Format rules. - this.details = this.completion.details.map((rule: CompletionRule) => { + this.details = await Promise.all(this.completion.details.map(async (rule: CompletionRule) => { rule.statuscomplete = rule.rulevalue.status == CoreCourseProvider.COMPLETION_COMPLETE || rule.rulevalue.status == CoreCourseProvider.COMPLETION_COMPLETE_PASS; rule.statuscompletefail = rule.rulevalue.status == CoreCourseProvider.COMPLETION_COMPLETE_FAIL; @@ -57,10 +58,12 @@ export class CoreCourseModuleCompletionComponent extends CoreCourseModuleComplet rule.accessibleDescription = null; if (this.completion!.overrideby) { + const fullName = await CoreUser.getUserFullNameWithDefault(this.completion!.overrideby, this.completion!.courseId); + const setByData = { $a: { condition: rule.rulevalue.description, - setby: this.completion!.overrideby, + setby: fullName, }, }; const overrideStatus = rule.statuscomplete ? 'done' : 'todo'; @@ -69,7 +72,7 @@ export class CoreCourseModuleCompletionComponent extends CoreCourseModuleComplet } return rule; - }); + })); } } diff --git a/src/core/features/course/components/module-manual-completion/module-manual-completion.ts b/src/core/features/course/components/module-manual-completion/module-manual-completion.ts index 3cab6465e..68e8ff927 100644 --- a/src/core/features/course/components/module-manual-completion/module-manual-completion.ts +++ b/src/core/features/course/components/module-manual-completion/module-manual-completion.ts @@ -15,6 +15,7 @@ import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChange } from '@angular/core'; import { CoreCourseHelper, CoreCourseModuleCompletionData } from '@features/course/services/course-helper'; +import { CoreUser } from '@features/user/services/user'; import { Translate } from '@singletons'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; @@ -41,10 +42,13 @@ export class CoreCourseModuleManualCompletionComponent implements OnInit, OnChan */ ngOnInit(): void { this.manualChangedObserver = CoreEvents.on(CoreEvents.MANUAL_COMPLETION_CHANGED, (data) => { - if (this.completion && this.completion.cmid == data.completion.cmid) { - this.completion = data.completion; - this.calculateData(); + if (!this.completion || this.completion.cmid != data.completion.cmid) { + return; } + + this.completion = data.completion; + this.calculateData(); + this.completionChanged.emit(this.completion); }); } @@ -60,17 +64,19 @@ export class CoreCourseModuleManualCompletionComponent implements OnInit, OnChan /** * @inheritdoc */ - protected calculateData(): void { - if (!this.completion?.isautomatic) { + protected async calculateData(): Promise { + if (!this.completion || this.completion.isautomatic) { return; } // Set an accessible description for manual completions with overridden completion state. if (this.completion.overrideby) { + const fullName = await CoreUser.getUserFullNameWithDefault(this.completion.overrideby, this.completion.courseId); + const setByData = { $a: { activityname: this.moduleName, - setby: this.completion.overrideby, + setby: fullName, }, }; const setByLangKey = this.completion.state ? 'completion_setby:manual:done' : 'completion_setby:manual:markdone'; @@ -93,10 +99,7 @@ export class CoreCourseModuleManualCompletionComponent implements OnInit, OnChan await CoreCourseHelper.changeManualCompletion(this.completion, event); - this.calculateData(); - CoreEvents.trigger(CoreEvents.MANUAL_COMPLETION_CHANGED, { completion: this.completion }); - this.completionChanged.emit(this.completion); } /** diff --git a/src/core/features/course/pages/contents/contents.ts b/src/core/features/course/pages/contents/contents.ts index 9a9c2fb75..3cf7f8da6 100644 --- a/src/core/features/course/pages/contents/contents.ts +++ b/src/core/features/course/pages/contents/contents.ts @@ -81,6 +81,8 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy { protected courseStatusObserver?: CoreEventObserver; protected syncObserver?: CoreEventObserver; protected isDestroyed = false; + protected modulesHaveCompletion = false; + protected debouncedUpdateCachedCompletion?: () => void; // Update the cached completion after a certain time. /** * Component being initialized. @@ -104,6 +106,21 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy { CoreCourseFormatDelegate.displayEnableDownload(this.course); this.downloadCourseEnabled = !CoreCourses.isDownloadCourseDisabledInSite(); + this.debouncedUpdateCachedCompletion = CoreUtils.debounce(() => { + if (this.modulesHaveCompletion) { + CoreUtils.ignoreErrors(CoreCourse.getSections(this.course.id, false, true)); + } else { + CoreUtils.ignoreErrors(CoreCourse.getActivitiesCompletionStatus( + this.course.id, + undefined, + undefined, + false, + false, + false, + )); + } + }, 30000); + this.initListeners(); await this.loadData(false, true); @@ -254,6 +271,8 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy { if (sectionWithModules && typeof sectionWithModules.modules[0].completion != 'undefined') { // The module already has completion (3.6 onwards). Load the offline completion. + this.modulesHaveCompletion = true; + await CoreUtils.ignoreErrors(CoreCourseHelper.loadOfflineCompletion(this.course.id, sections)); } else { const fetchedData = await CoreUtils.ignoreErrors( @@ -356,6 +375,8 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy { // Invalidate the completion. await CoreUtils.ignoreErrors(CoreCourse.invalidateSections(this.course.id)); + this.debouncedUpdateCachedCompletion?.(); + return; } diff --git a/src/core/features/course/services/course-helper.ts b/src/core/features/course/services/course-helper.ts index b8e1ee6b4..fa7f9423d 100644 --- a/src/core/features/course/services/course-helper.ts +++ b/src/core/features/course/services/course-helper.ts @@ -195,11 +195,8 @@ export class CoreCourseHelperProvider { forCoursePage, ); - if (module.completiondata && module.completion && module.completion > 0) { - module.completiondata.courseId = courseId; - module.completiondata.courseName = courseName; - module.completiondata.tracking = module.completion; - module.completiondata.cmid = module.id; + if (module.completiondata) { + this.calculateModuleCompletionData(module, courseId, courseName); } else if (completionStatus && typeof completionStatus[module.id] != 'undefined') { // Should not happen on > 3.6. Check if activity has completions and if it's marked. const activityStatus = completionStatus[module.id]; @@ -224,6 +221,24 @@ export class CoreCourseHelperProvider { return { hasContent, sections: formattedSections }; } + /** + * Calculate completion data of a module. + * + * @param module Module. + * @param courseId Course ID of the module. + * @param courseName Course name. + */ + calculateModuleCompletionData(module: CoreCourseModule, courseId: number, courseName?: string): void { + if (!module.completiondata || !module.completion) { + return; + } + + module.completiondata.courseId = courseId; + module.completiondata.courseName = courseName; + module.completiondata.tracking = module.completion; + module.completiondata.cmid = module.id; + } + /** * Calculate the status of a section. * @@ -1177,6 +1192,31 @@ export class CoreCourseHelperProvider { } } + /** + * Load offline completion for a certain module. + * This should be used in 3.6 sites or higher, where the course contents already include the completion. + * + * @param courseId The course to get the completion. + * @param mmodule The module. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when done. + */ + async loadModuleOfflineCompletion(courseId: number, module: CoreCourseModule, siteId?: string): Promise { + if (!module.completiondata) { + return; + } + + const offlineCompletions = await CoreCourseOffline.getCourseManualCompletions(courseId, siteId); + + const offlineCompletion = offlineCompletions.find(completion => completion.cmid == module.id); + + if (offlineCompletion && offlineCompletion.timecompleted >= module.completiondata.timecompleted * 1000) { + // The module has offline completion. Load it. + module.completiondata.state = offlineCompletion.completed; + module.completiondata.offline = true; + } + } + /** * Prefetch all the courses in the array. * diff --git a/src/core/features/user/lang.json b/src/core/features/user/lang.json index 528fe4c5c..610e0fae4 100644 --- a/src/core/features/user/lang.json +++ b/src/core/features/user/lang.json @@ -23,5 +23,6 @@ "sendemail": "Email", "student": "Student", "teacher": "Non-editing teacher", + "userwithid": "User with ID {{id}}", "webpage": "Web page" -} \ No newline at end of file +} diff --git a/src/core/features/user/services/user.ts b/src/core/features/user/services/user.ts index 1ba500218..6033f8efd 100644 --- a/src/core/features/user/services/user.ts +++ b/src/core/features/user/services/user.ts @@ -21,7 +21,7 @@ import { CoreUtils } from '@services/utils/utils'; import { CoreUserOffline } from './user-offline'; import { CoreLogger } from '@singletons/logger'; import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; -import { makeSingleton } from '@singletons'; +import { makeSingleton, Translate } from '@singletons'; import { CoreEvents } from '@singletons/events'; import { CoreStatusWithWarningsWSResponse, CoreWSExternalWarning } from '@services/ws'; import { CoreError } from '@classes/errors/error'; @@ -305,6 +305,25 @@ export class CoreUserProvider { return site.getDb().getRecord(USERS_TABLE_NAME, { id: userId }); } + /** + * Get a user fullname, using a default text if user not found. + * + * @param userId User ID. + * @param courseId Course ID. + * @param siteId Site ID. + * @return Promise resolved with user name. + */ + async getUserFullNameWithDefault(userId: number, courseId?: number, siteId?: string): Promise { + try { + const user = await CoreUser.getProfile(userId, courseId, true, siteId); + + return user.fullname; + + } catch { + return Translate.instant('core.user.userwithid', { id: userId }); + } + } + /** * Get user profile from WS. *