MOBILE-3757 course: Update cached data after commpletion changed
parent
61a908216d
commit
ac8cc189f7
|
@ -1447,6 +1447,17 @@
|
||||||
"core.course.askadmintosupport": "local_moodlemobileapp",
|
"core.course.askadmintosupport": "local_moodlemobileapp",
|
||||||
"core.course.availablespace": "local_moodlemobileapp",
|
"core.course.availablespace": "local_moodlemobileapp",
|
||||||
"core.course.cannotdeletewhiledownloading": "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.confirmdeletemodulefiles": "local_moodlemobileapp",
|
||||||
"core.course.confirmdeletestoreddata": "local_moodlemobileapp",
|
"core.course.confirmdeletestoreddata": "local_moodlemobileapp",
|
||||||
"core.course.confirmdownload": "local_moodlemobileapp",
|
"core.course.confirmdownload": "local_moodlemobileapp",
|
||||||
|
|
|
@ -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 currentStatus?: string; // The current status of the module. Only if setStatusListener is called.
|
||||||
protected completionObserver?: CoreEventObserver;
|
protected completionObserver?: CoreEventObserver;
|
||||||
protected logger: CoreLogger;
|
protected logger: CoreLogger;
|
||||||
|
protected debouncedUpdateModule?: () => void; // Update the module after a certain time.
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Optional() @Inject('') loggerName: string = 'CoreCourseModuleMainResourceComponent',
|
@Optional() @Inject('') loggerName: string = 'CoreCourseModuleMainResourceComponent',
|
||||||
|
@ -103,9 +104,13 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy,
|
||||||
if (data && data.cmId == this.module.id) {
|
if (data && data.cmId == this.module.id) {
|
||||||
await CoreCourse.invalidateModule(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();
|
this.blog = await AddonBlog.isPluginEnabled();
|
||||||
|
@ -160,7 +165,7 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy,
|
||||||
]));
|
]));
|
||||||
|
|
||||||
if (this.showCompletion) {
|
if (this.showCompletion) {
|
||||||
this.module = await CoreCourse.getModule(this.module.id, this.courseId);
|
this.fetchModule();
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.loadContent(true);
|
await this.loadContent(true);
|
||||||
|
@ -403,8 +408,23 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy,
|
||||||
* @return Promise resolved when done.
|
* @return Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
async onCompletionChange(): Promise<void> {
|
async onCompletionChange(): Promise<void> {
|
||||||
// Nothing to do.
|
// Update the module data after a while.
|
||||||
return;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -16,6 +16,7 @@ import { Component, Input } from '@angular/core';
|
||||||
|
|
||||||
import { CoreCourseModuleCompletionBaseComponent } from '@features/course/classes/module-completion';
|
import { CoreCourseModuleCompletionBaseComponent } from '@features/course/classes/module-completion';
|
||||||
import { CoreCourseModuleWSRuleDetails, CoreCourseProvider } from '@features/course/services/course';
|
import { CoreCourseModuleWSRuleDetails, CoreCourseProvider } from '@features/course/services/course';
|
||||||
|
import { CoreUser } from '@features/user/services/user';
|
||||||
import { Translate } from '@singletons';
|
import { Translate } from '@singletons';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -43,13 +44,13 @@ export class CoreCourseModuleCompletionComponent extends CoreCourseModuleComplet
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
protected calculateData(): void {
|
protected async calculateData(): Promise<void> {
|
||||||
if (!this.completion?.details) {
|
if (!this.completion?.details) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format rules.
|
// 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.statuscomplete = rule.rulevalue.status == CoreCourseProvider.COMPLETION_COMPLETE ||
|
||||||
rule.rulevalue.status == CoreCourseProvider.COMPLETION_COMPLETE_PASS;
|
rule.rulevalue.status == CoreCourseProvider.COMPLETION_COMPLETE_PASS;
|
||||||
rule.statuscompletefail = rule.rulevalue.status == CoreCourseProvider.COMPLETION_COMPLETE_FAIL;
|
rule.statuscompletefail = rule.rulevalue.status == CoreCourseProvider.COMPLETION_COMPLETE_FAIL;
|
||||||
|
@ -57,10 +58,12 @@ export class CoreCourseModuleCompletionComponent extends CoreCourseModuleComplet
|
||||||
rule.accessibleDescription = null;
|
rule.accessibleDescription = null;
|
||||||
|
|
||||||
if (this.completion!.overrideby) {
|
if (this.completion!.overrideby) {
|
||||||
|
const fullName = await CoreUser.getUserFullNameWithDefault(this.completion!.overrideby, this.completion!.courseId);
|
||||||
|
|
||||||
const setByData = {
|
const setByData = {
|
||||||
$a: {
|
$a: {
|
||||||
condition: rule.rulevalue.description,
|
condition: rule.rulevalue.description,
|
||||||
setby: this.completion!.overrideby,
|
setby: fullName,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const overrideStatus = rule.statuscomplete ? 'done' : 'todo';
|
const overrideStatus = rule.statuscomplete ? 'done' : 'todo';
|
||||||
|
@ -69,7 +72,7 @@ export class CoreCourseModuleCompletionComponent extends CoreCourseModuleComplet
|
||||||
}
|
}
|
||||||
|
|
||||||
return rule;
|
return rule;
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChange } from '@angular/core';
|
import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChange } from '@angular/core';
|
||||||
|
|
||||||
import { CoreCourseHelper, CoreCourseModuleCompletionData } from '@features/course/services/course-helper';
|
import { CoreCourseHelper, CoreCourseModuleCompletionData } from '@features/course/services/course-helper';
|
||||||
|
import { CoreUser } from '@features/user/services/user';
|
||||||
import { Translate } from '@singletons';
|
import { Translate } from '@singletons';
|
||||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||||
|
|
||||||
|
@ -41,10 +42,13 @@ export class CoreCourseModuleManualCompletionComponent implements OnInit, OnChan
|
||||||
*/
|
*/
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.manualChangedObserver = CoreEvents.on(CoreEvents.MANUAL_COMPLETION_CHANGED, (data) => {
|
this.manualChangedObserver = CoreEvents.on(CoreEvents.MANUAL_COMPLETION_CHANGED, (data) => {
|
||||||
if (this.completion && this.completion.cmid == data.completion.cmid) {
|
if (!this.completion || this.completion.cmid != data.completion.cmid) {
|
||||||
this.completion = data.completion;
|
return;
|
||||||
this.calculateData();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.completion = data.completion;
|
||||||
|
this.calculateData();
|
||||||
|
this.completionChanged.emit(this.completion);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,17 +64,19 @@ export class CoreCourseModuleManualCompletionComponent implements OnInit, OnChan
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
protected calculateData(): void {
|
protected async calculateData(): Promise<void> {
|
||||||
if (!this.completion?.isautomatic) {
|
if (!this.completion || this.completion.isautomatic) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set an accessible description for manual completions with overridden completion state.
|
// Set an accessible description for manual completions with overridden completion state.
|
||||||
if (this.completion.overrideby) {
|
if (this.completion.overrideby) {
|
||||||
|
const fullName = await CoreUser.getUserFullNameWithDefault(this.completion.overrideby, this.completion.courseId);
|
||||||
|
|
||||||
const setByData = {
|
const setByData = {
|
||||||
$a: {
|
$a: {
|
||||||
activityname: this.moduleName,
|
activityname: this.moduleName,
|
||||||
setby: this.completion.overrideby,
|
setby: fullName,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const setByLangKey = this.completion.state ? 'completion_setby:manual:done' : 'completion_setby:manual:markdone';
|
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);
|
await CoreCourseHelper.changeManualCompletion(this.completion, event);
|
||||||
|
|
||||||
this.calculateData();
|
|
||||||
|
|
||||||
CoreEvents.trigger(CoreEvents.MANUAL_COMPLETION_CHANGED, { completion: this.completion });
|
CoreEvents.trigger(CoreEvents.MANUAL_COMPLETION_CHANGED, { completion: this.completion });
|
||||||
this.completionChanged.emit(this.completion);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -81,6 +81,8 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy {
|
||||||
protected courseStatusObserver?: CoreEventObserver;
|
protected courseStatusObserver?: CoreEventObserver;
|
||||||
protected syncObserver?: CoreEventObserver;
|
protected syncObserver?: CoreEventObserver;
|
||||||
protected isDestroyed = false;
|
protected isDestroyed = false;
|
||||||
|
protected modulesHaveCompletion = false;
|
||||||
|
protected debouncedUpdateCachedCompletion?: () => void; // Update the cached completion after a certain time.
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component being initialized.
|
* Component being initialized.
|
||||||
|
@ -104,6 +106,21 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy {
|
||||||
CoreCourseFormatDelegate.displayEnableDownload(this.course);
|
CoreCourseFormatDelegate.displayEnableDownload(this.course);
|
||||||
this.downloadCourseEnabled = !CoreCourses.isDownloadCourseDisabledInSite();
|
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();
|
this.initListeners();
|
||||||
|
|
||||||
await this.loadData(false, true);
|
await this.loadData(false, true);
|
||||||
|
@ -254,6 +271,8 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy {
|
||||||
|
|
||||||
if (sectionWithModules && typeof sectionWithModules.modules[0].completion != 'undefined') {
|
if (sectionWithModules && typeof sectionWithModules.modules[0].completion != 'undefined') {
|
||||||
// The module already has completion (3.6 onwards). Load the offline completion.
|
// The module already has completion (3.6 onwards). Load the offline completion.
|
||||||
|
this.modulesHaveCompletion = true;
|
||||||
|
|
||||||
await CoreUtils.ignoreErrors(CoreCourseHelper.loadOfflineCompletion(this.course.id, sections));
|
await CoreUtils.ignoreErrors(CoreCourseHelper.loadOfflineCompletion(this.course.id, sections));
|
||||||
} else {
|
} else {
|
||||||
const fetchedData = await CoreUtils.ignoreErrors(
|
const fetchedData = await CoreUtils.ignoreErrors(
|
||||||
|
@ -356,6 +375,8 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy {
|
||||||
// Invalidate the completion.
|
// Invalidate the completion.
|
||||||
await CoreUtils.ignoreErrors(CoreCourse.invalidateSections(this.course.id));
|
await CoreUtils.ignoreErrors(CoreCourse.invalidateSections(this.course.id));
|
||||||
|
|
||||||
|
this.debouncedUpdateCachedCompletion?.();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -195,11 +195,8 @@ export class CoreCourseHelperProvider {
|
||||||
forCoursePage,
|
forCoursePage,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (module.completiondata && module.completion && module.completion > 0) {
|
if (module.completiondata) {
|
||||||
module.completiondata.courseId = courseId;
|
this.calculateModuleCompletionData(module, courseId, courseName);
|
||||||
module.completiondata.courseName = courseName;
|
|
||||||
module.completiondata.tracking = module.completion;
|
|
||||||
module.completiondata.cmid = module.id;
|
|
||||||
} else if (completionStatus && typeof completionStatus[module.id] != 'undefined') {
|
} else if (completionStatus && typeof completionStatus[module.id] != 'undefined') {
|
||||||
// Should not happen on > 3.6. Check if activity has completions and if it's marked.
|
// Should not happen on > 3.6. Check if activity has completions and if it's marked.
|
||||||
const activityStatus = completionStatus[module.id];
|
const activityStatus = completionStatus[module.id];
|
||||||
|
@ -224,6 +221,24 @@ export class CoreCourseHelperProvider {
|
||||||
return { hasContent, sections: formattedSections };
|
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.
|
* 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.
|
* Prefetch all the courses in the array.
|
||||||
*
|
*
|
||||||
|
|
|
@ -23,5 +23,6 @@
|
||||||
"sendemail": "Email",
|
"sendemail": "Email",
|
||||||
"student": "Student",
|
"student": "Student",
|
||||||
"teacher": "Non-editing teacher",
|
"teacher": "Non-editing teacher",
|
||||||
|
"userwithid": "User with ID {{id}}",
|
||||||
"webpage": "Web page"
|
"webpage": "Web page"
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ import { CoreUtils } from '@services/utils/utils';
|
||||||
import { CoreUserOffline } from './user-offline';
|
import { CoreUserOffline } from './user-offline';
|
||||||
import { CoreLogger } from '@singletons/logger';
|
import { CoreLogger } from '@singletons/logger';
|
||||||
import { CoreSite, CoreSiteWSPreSets } from '@classes/site';
|
import { CoreSite, CoreSiteWSPreSets } from '@classes/site';
|
||||||
import { makeSingleton } from '@singletons';
|
import { makeSingleton, Translate } from '@singletons';
|
||||||
import { CoreEvents } from '@singletons/events';
|
import { CoreEvents } from '@singletons/events';
|
||||||
import { CoreStatusWithWarningsWSResponse, CoreWSExternalWarning } from '@services/ws';
|
import { CoreStatusWithWarningsWSResponse, CoreWSExternalWarning } from '@services/ws';
|
||||||
import { CoreError } from '@classes/errors/error';
|
import { CoreError } from '@classes/errors/error';
|
||||||
|
@ -305,6 +305,25 @@ export class CoreUserProvider {
|
||||||
return site.getDb().getRecord(USERS_TABLE_NAME, { id: userId });
|
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.
|
* Get user profile from WS.
|
||||||
*
|
*
|
||||||
|
|
Loading…
Reference in New Issue