MOBILE-3757 course: Update cached data after commpletion changed

main
Dani Palou 2021-05-07 11:59:55 +02:00
parent 61a908216d
commit ac8cc189f7
8 changed files with 142 additions and 24 deletions

View File

@ -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",

View File

@ -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<void> {
// Nothing to do.
return;
// Update the module data after a while.
this.debouncedUpdateModule?.();
}
/**
* Fetch module.
*
* @return Promise resolved when done.
*/
protected async fetchModule(): Promise<void> {
const module = await CoreCourse.getModule(this.module.id, this.courseId);
CoreCourseHelper.calculateModuleCompletionData(module, this.courseId);
await CoreCourseHelper.loadModuleOfflineCompletion(this.courseId, module);
this.module = module;
}
/**

View File

@ -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<void> {
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;
});
}));
}
}

View File

@ -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<void> {
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);
}
/**

View File

@ -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;
}

View File

@ -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<void> {
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.
*

View File

@ -23,5 +23,6 @@
"sendemail": "Email",
"student": "Student",
"teacher": "Non-editing teacher",
"userwithid": "User with ID {{id}}",
"webpage": "Web page"
}
}

View File

@ -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<string> {
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.
*