MOBILE-3915 course: Update course progress when completion changes
parent
5e29e65325
commit
7971e71e57
|
@ -19,8 +19,6 @@ import {
|
||||||
OnChanges,
|
OnChanges,
|
||||||
OnDestroy,
|
OnDestroy,
|
||||||
SimpleChange,
|
SimpleChange,
|
||||||
Output,
|
|
||||||
EventEmitter,
|
|
||||||
ViewChildren,
|
ViewChildren,
|
||||||
QueryList,
|
QueryList,
|
||||||
Type,
|
Type,
|
||||||
|
@ -31,12 +29,9 @@ import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-comp
|
||||||
import { CoreCourseAnyCourseData } from '@features/courses/services/courses';
|
import { CoreCourseAnyCourseData } from '@features/courses/services/courses';
|
||||||
import {
|
import {
|
||||||
CoreCourse,
|
CoreCourse,
|
||||||
CoreCourseModuleCompletionStatus,
|
|
||||||
CoreCourseProvider,
|
CoreCourseProvider,
|
||||||
} from '@features/course/services/course';
|
} from '@features/course/services/course';
|
||||||
import {
|
import {
|
||||||
CoreCourseModuleData,
|
|
||||||
CoreCourseModuleCompletionData,
|
|
||||||
CoreCourseSection,
|
CoreCourseSection,
|
||||||
} from '@features/course/services/course-helper';
|
} from '@features/course/services/course-helper';
|
||||||
import { CoreCourseFormatDelegate } from '@features/course/services/format-delegate';
|
import { CoreCourseFormatDelegate } from '@features/course/services/format-delegate';
|
||||||
|
@ -56,7 +51,7 @@ import { CoreCourseModuleDelegate } from '@features/course/services/module-deleg
|
||||||
*
|
*
|
||||||
* Example usage:
|
* Example usage:
|
||||||
*
|
*
|
||||||
* <core-course-format [course]="course" [sections]="sections" (completionChanged)="onCompletionChange()"></core-course-format>
|
* <core-course-format [course]="course" [sections]="sections"></core-course-format>
|
||||||
*/
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'core-course-format',
|
selector: 'core-course-format',
|
||||||
|
@ -72,7 +67,6 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
@Input() initialSectionId?: number; // The section to load first (by ID).
|
@Input() initialSectionId?: number; // The section to load first (by ID).
|
||||||
@Input() initialSectionNumber?: number; // The section to load first (by number).
|
@Input() initialSectionNumber?: number; // The section to load first (by number).
|
||||||
@Input() moduleId?: number; // The module ID to scroll to. Must be inside the initial selected section.
|
@Input() moduleId?: number; // The module ID to scroll to. Must be inside the initial selected section.
|
||||||
@Output() completionChanged = new EventEmitter<CoreCourseModuleCompletionData>(); // Notify when any module completion changes.
|
|
||||||
|
|
||||||
@ViewChildren(CoreDynamicComponent) dynamicComponents?: QueryList<CoreDynamicComponent>;
|
@ViewChildren(CoreDynamicComponent) dynamicComponents?: QueryList<CoreDynamicComponent>;
|
||||||
|
|
||||||
|
@ -95,11 +89,9 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
allSectionsId: number = CoreCourseProvider.ALL_SECTIONS_ID;
|
allSectionsId: number = CoreCourseProvider.ALL_SECTIONS_ID;
|
||||||
stealthModulesSectionId: number = CoreCourseProvider.STEALTH_MODULES_SECTION_ID;
|
stealthModulesSectionId: number = CoreCourseProvider.STEALTH_MODULES_SECTION_ID;
|
||||||
loaded = false;
|
loaded = false;
|
||||||
progress?: number;
|
|
||||||
highlighted?: string;
|
highlighted?: string;
|
||||||
|
|
||||||
protected selectTabObserver?: CoreEventObserver;
|
protected selectTabObserver?: CoreEventObserver;
|
||||||
protected completionObserver?: CoreEventObserver;
|
|
||||||
protected lastCourseFormat?: string;
|
protected lastCourseFormat?: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -141,36 +133,6 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// The completion of any of the modules have changed.
|
|
||||||
this.completionObserver = CoreEvents.on(CoreEvents.COMPLETION_CHANGED, (data) => {
|
|
||||||
if (data.completion.courseId != this.course.id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Emit a new event for other components.
|
|
||||||
this.completionChanged.emit(data.completion);
|
|
||||||
|
|
||||||
if (data.completion.valueused !== false || !this.course || !('progress' in this.course) ||
|
|
||||||
typeof this.course.progress != 'number') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the completion value is not used, the page won't be reloaded, so update the progress bar.
|
|
||||||
const completionModules = (<CoreCourseModuleData[]> [])
|
|
||||||
.concat(...this.sections.map((section) => section.modules))
|
|
||||||
.map((module) => module.completion && module.completion > 0 ? 1 : module.completion)
|
|
||||||
.reduce((accumulator, currentValue) => (accumulator || 0) + (currentValue || 0), 0);
|
|
||||||
|
|
||||||
const moduleProgressPercent = 100 / (completionModules || 1);
|
|
||||||
// Use min/max here to avoid floating point rounding errors over/under-flowing the progress bar.
|
|
||||||
if (data.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE) {
|
|
||||||
this.course.progress = Math.min(100, this.course.progress + moduleProgressPercent);
|
|
||||||
} else {
|
|
||||||
this.course.progress = Math.max(0, this.course.progress - moduleProgressPercent);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.updateProgress();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -187,8 +149,6 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
this.displayBlocks = CoreCourseFormatDelegate.displayBlocks(this.course);
|
this.displayBlocks = CoreCourseFormatDelegate.displayBlocks(this.course);
|
||||||
|
|
||||||
this.hasBlocks = await CoreBlockHelper.hasCourseBlocks(this.course.id);
|
this.hasBlocks = await CoreBlockHelper.hasCourseBlocks(this.course.id);
|
||||||
|
|
||||||
this.updateProgress();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (changes.sections && this.sections) {
|
if (changes.sections && this.sections) {
|
||||||
|
@ -205,7 +165,6 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
this.data.initialSectionId = this.initialSectionId;
|
this.data.initialSectionId = this.initialSectionId;
|
||||||
this.data.initialSectionNumber = this.initialSectionNumber;
|
this.data.initialSectionNumber = this.initialSectionNumber;
|
||||||
this.data.moduleId = this.moduleId;
|
this.data.moduleId = this.moduleId;
|
||||||
this.data.completionChanged = this.completionChanged;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -542,25 +501,6 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
section.id != CoreCourseProvider.STEALTH_MODULES_SECTION_ID;
|
section.id != CoreCourseProvider.STEALTH_MODULES_SECTION_ID;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Update course progress.
|
|
||||||
*/
|
|
||||||
protected updateProgress(): void {
|
|
||||||
if (
|
|
||||||
!this.course ||
|
|
||||||
!('progress' in this.course) ||
|
|
||||||
typeof this.course.progress !== 'number' ||
|
|
||||||
this.course.progress < 0 ||
|
|
||||||
this.course.completionusertracked === false
|
|
||||||
) {
|
|
||||||
this.progress = undefined;
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.progress = this.course.progress;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type CoreCourseSectionToDisplay = CoreCourseSection & {
|
type CoreCourseSectionToDisplay = CoreCourseSection & {
|
||||||
|
|
|
@ -20,7 +20,7 @@ import { CoreCourseFormatDelegate } from '../../services/format-delegate';
|
||||||
import { CoreCourseOptionsDelegate } from '../../services/course-options-delegate';
|
import { CoreCourseOptionsDelegate } from '../../services/course-options-delegate';
|
||||||
import { CoreCourseAnyCourseData } from '@features/courses/services/courses';
|
import { CoreCourseAnyCourseData } from '@features/courses/services/courses';
|
||||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||||
import { CoreCourse } from '@features/course/services/course';
|
import { CoreCourse, CoreCourseModuleCompletionStatus, CoreCourseWSSection } from '@features/course/services/course';
|
||||||
import { CoreCourseHelper, CoreCourseModuleData } from '@features/course/services/course-helper';
|
import { CoreCourseHelper, CoreCourseModuleData } from '@features/course/services/course-helper';
|
||||||
import { CoreUtils } from '@services/utils/utils';
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
import { CoreTextUtils } from '@services/utils/text';
|
import { CoreTextUtils } from '@services/utils/text';
|
||||||
|
@ -52,6 +52,8 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy {
|
||||||
|
|
||||||
protected currentPagePath = '';
|
protected currentPagePath = '';
|
||||||
protected selectTabObserver: CoreEventObserver;
|
protected selectTabObserver: CoreEventObserver;
|
||||||
|
protected completionObserver: CoreEventObserver;
|
||||||
|
protected sections: CoreCourseWSSection[] = []; // List of course sections.
|
||||||
protected firstTabName?: string;
|
protected firstTabName?: string;
|
||||||
protected module?: CoreCourseModuleData;
|
protected module?: CoreCourseModuleData;
|
||||||
protected modParams?: Params;
|
protected modParams?: Params;
|
||||||
|
@ -83,6 +85,34 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// The completion of any of the modules have changed.
|
||||||
|
this.completionObserver = CoreEvents.on(CoreEvents.COMPLETION_CHANGED, (data) => {
|
||||||
|
if (data.completion.courseId != this.course?.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.completion.valueused !== false || !this.course || !('progress' in this.course) ||
|
||||||
|
typeof this.course.progress != 'number') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the completion value is not used, the page won't be reloaded, so update the progress bar.
|
||||||
|
const completionModules = (<CoreCourseModuleData[]> [])
|
||||||
|
.concat(...this.sections.map((section) => section.modules))
|
||||||
|
.map((module) => module.completion && module.completion > 0 ? 1 : module.completion)
|
||||||
|
.reduce((accumulator, currentValue) => (accumulator || 0) + (currentValue || 0), 0);
|
||||||
|
|
||||||
|
const moduleProgressPercent = 100 / (completionModules || 1);
|
||||||
|
// Use min/max here to avoid floating point rounding errors over/under-flowing the progress bar.
|
||||||
|
if (data.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE) {
|
||||||
|
this.course.progress = Math.min(100, this.course.progress + moduleProgressPercent);
|
||||||
|
} else {
|
||||||
|
this.course.progress = Math.max(0, this.course.progress - moduleProgressPercent);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateProgress();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -206,14 +236,14 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy {
|
||||||
this.updateProgress();
|
this.updateProgress();
|
||||||
|
|
||||||
// Load sections.
|
// Load sections.
|
||||||
const sections = await CoreUtils.ignoreErrors(CoreCourse.getSections(this.course.id, false, true));
|
this.sections = await CoreUtils.ignoreErrors(CoreCourse.getSections(this.course.id, false, true), []);
|
||||||
|
|
||||||
if (!sections) {
|
if (!this.sections) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the title again now that we have sections.
|
// Get the title again now that we have sections.
|
||||||
this.title = CoreCourseFormatDelegate.getCourseTitle(this.course, sections);
|
this.title = CoreCourseFormatDelegate.getCourseTitle(this.course, this.sections);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -224,6 +254,7 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy {
|
||||||
|
|
||||||
CoreNavigator.decreaseRouteDepth(path.replace(/(\/deep)+/, ''));
|
CoreNavigator.decreaseRouteDepth(path.replace(/(\/deep)+/, ''));
|
||||||
this.selectTabObserver?.off();
|
this.selectTabObserver?.off();
|
||||||
|
this.completionObserver?.off();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in New Issue