MOBILE-4660 course: Adapt course downloads to new data structure

main
Dani Palou 2024-10-04 14:55:23 +02:00
parent d384752113
commit d1856f5fff
13 changed files with 252 additions and 418 deletions

View File

@ -70,7 +70,7 @@ export class AddonBlockActivityModulesComponent extends CoreBlockBaseComponent i
let modFullNames: Record<string, string> = {};
const brandedIcons: Record<string, boolean|undefined> = {};
const modules = CoreCourseHelper.getSectionsModules(sections, {
const modules = CoreCourse.getSectionsModules(sections, {
ignoreSection: section => !CoreCourseHelper.canUserViewSection(section),
ignoreModule: module => !CoreCourseHelper.canUserViewModule(module) || !CoreCourse.moduleHasView(module),
});

View File

@ -1,15 +0,0 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
export const ADDON_MOD_SUBSECTION_COMPONENT = 'mmaModSubsection';

View File

@ -1,98 +0,0 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { CoreCourseResourcePrefetchHandlerBase } from '@features/course/classes/resource-prefetch-handler';
import { CoreCourse, CoreCourseAnyModuleData, CoreCourseWSSection } from '@features/course/services/course';
import { makeSingleton } from '@singletons';
import { ADDON_MOD_SUBSECTION_COMPONENT } from '../../constants';
import { CoreCourseHelper, CoreCourseModuleData } from '@features/course/services/course-helper';
import { CoreFileSizeSum } from '@services/plugin-file-delegate';
import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate';
import { CoreSites } from '@services/sites';
/**
* Handler to prefetch subsections.
*/
@Injectable({ providedIn: 'root' })
export class AddonModSubsectionPrefetchHandlerService extends CoreCourseResourcePrefetchHandlerBase {
name = 'AddonModSubsection';
modName = 'subsection';
component = ADDON_MOD_SUBSECTION_COMPONENT;
/**
* @inheritdoc
*/
protected async performDownloadOrPrefetch(
siteId: string,
module: CoreCourseModuleData,
courseId: number,
): Promise<void> {
const section = await this.getSection(module, courseId, siteId);
if (!section) {
return;
}
await CoreCourseHelper.prefetchSections([section], courseId);
}
/**
* @inheritdoc
*/
async getDownloadSize(module: CoreCourseAnyModuleData, courseId: number): Promise<CoreFileSizeSum> {
const section = await this.getSection(module, courseId);
if (!section) {
return { size: 0, total: true };
}
return await CoreCourseModulePrefetchDelegate.getDownloadSize(section.modules, courseId);
}
/**
* @inheritdoc
*/
async getDownloadedSize(module: CoreCourseAnyModuleData, courseId: number): Promise<number> {
const section = await this.getSection(module, courseId);
if (!section) {
return 0;
}
return CoreCourseHelper.getModulesDownloadedSize(section.modules, courseId);
}
/**
* Get the section of a module.
*
* @param module Module.
* @param courseId Course ID.
* @param siteId Site ID. If not defined, current site.
* @returns Promise resolved with the section if found.
*/
protected async getSection(
module: CoreCourseAnyModuleData,
courseId: number,
siteId?: string,
): Promise<CoreCourseWSSection | undefined> {
siteId = siteId ?? CoreSites.getCurrentSiteId();
const sections = await CoreCourse.getSections(courseId, false, true, undefined, siteId);
return sections.find((section) =>
section.component === 'mod_subsection' && section.itemid === module.instance);
}
}
export const AddonModSubsectionPrefetchHandler = makeSingleton(AddonModSubsectionPrefetchHandlerService);

View File

@ -15,8 +15,6 @@
import { APP_INITIALIZER, NgModule } from '@angular/core';
import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate';
import { AddonModSubsectionIndexLinkHandler } from './services/handlers/index-link';
import { AddonModSubsectionPrefetchHandler } from './services/handlers/prefetch';
import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate';
@NgModule({
providers: [
@ -25,7 +23,6 @@ import { CoreCourseModulePrefetchDelegate } from '@features/course/services/modu
multi: true,
useValue: () => {
CoreContentLinksDelegate.registerHandler(AddonModSubsectionIndexLinkHandler.instance);
CoreCourseModulePrefetchDelegate.registerHandler(AddonModSubsectionPrefetchHandler.instance);
},
},
],

View File

@ -16,7 +16,6 @@
</ion-label>
</ion-item>
<ion-card class="wholecourse">
<ion-card-header>
<ion-card-title>
@ -57,7 +56,7 @@
<ng-template #sectionCard let-section="section">
<ion-card class="section" *ngIf="section.modules.length > 0" [id]="'addons-course-storage-'+section.id">
<ion-card class="section" *ngIf="section.contents.length > 0" [id]="'addons-course-storage-'+section.id">
<ion-accordion [value]="section.id" toggleIconSlot="start">
<ion-item [detail]="false" slot="header" class="card-header">
<ion-label>
@ -102,45 +101,46 @@
</ion-item>
<ion-card-content slot="content">
<ng-container *ngIf="section.expanded">
<ng-container *ngFor="let module of section.modules">
@if (module.subSection) {
<ng-container *ngTemplateOutlet="sectionCard; context: { section: module.subSection }" />
<ng-container *ngFor="let modOrSubsection of section.contents">
@if (!isModule(modOrSubsection)) {
<ng-container *ngTemplateOutlet="sectionCard; context: { section: modOrSubsection }" />
} @else {
<ion-item class="core-course-storage-activity"
*ngIf="downloadEnabled || (!module.calculatingSize && module.totalSize > 0)">
<core-mod-icon slot="start" *ngIf="module.handlerData.icon" [modicon]="module.handlerData.icon"
[modname]="module.modname" [componentId]="module.instance" [fallbackTranslation]="module.modplural"
[isBranded]="module.branded" />
*ngIf="downloadEnabled || (!modOrSubsection.calculatingSize && modOrSubsection.totalSize > 0)">
<core-mod-icon slot="start" *ngIf="modOrSubsection.handlerData.icon"
[modicon]="modOrSubsection.handlerData.icon" [modname]="modOrSubsection.modname"
[componentId]="modOrSubsection.instance" [fallbackTranslation]="modOrSubsection.modplural"
[isBranded]="modOrSubsection.branded" />
<ion-label class="ion-text-wrap">
<p class="item-heading {{module.handlerData!.class}} addon-storagemanager-module-size">
<core-format-text [text]="module.handlerData.title" [courseId]="module.course" contextLevel="module"
[contextInstanceId]="module.id" [adaptImg]="false" />
<p class="item-heading {{modOrSubsection.handlerData!.class}} addon-storagemanager-module-size">
<core-format-text [text]="modOrSubsection.handlerData.title" [courseId]="modOrSubsection.course"
contextLevel="module" [contextInstanceId]="modOrSubsection.id" [adaptImg]="false" />
</p>
<ion-badge [color]="module.downloadStatus === statusDownloaded ? 'success' : 'light'"
*ngIf="!module.calculatingSize && module.totalSize > 0">
<ion-icon name="fam-cloud-done" *ngIf="module.downloadStatus === statusDownloaded"
[attr.aria-label]="'core.downloaded' | translate" />{{ module.totalSize |
<ion-badge [color]="modOrSubsection.downloadStatus === statusDownloaded ? 'success' : 'light'"
*ngIf="!modOrSubsection.calculatingSize && modOrSubsection.totalSize > 0">
<ion-icon name="fam-cloud-done" *ngIf="modOrSubsection.downloadStatus === statusDownloaded"
[attr.aria-label]="'core.downloaded' | translate" />{{ modOrSubsection.totalSize |
coreBytesToSize }}
</ion-badge>
<ion-badge color="light" *ngIf="module.calculatingSize ||
(section.isDownloading && module.downloadStatus === statusDownloaded)">
<ion-badge color="light" *ngIf="modOrSubsection.calculatingSize ||
(section.isDownloading && modOrSubsection.downloadStatus === statusDownloaded)">
{{ 'core.calculating' | translate }}
</ion-badge>
</ion-label>
<div class="storage-buttons" slot="end">
<core-download-refresh *ngIf="downloadEnabled && module.handlerData?.showDownloadButton &&
module.downloadStatus !== statusDownloaded" [status]="module.downloadStatus" [enabled]="true"
[canTrustDownload]="true" [loading]="module.spinner || module.handlerData.spinner"
(action)="prefetchModule(module)"
<core-download-refresh *ngIf="downloadEnabled && modOrSubsection.handlerData?.showDownloadButton &&
modOrSubsection.downloadStatus !== statusDownloaded" [status]="modOrSubsection.downloadStatus" [enabled]="true"
[canTrustDownload]="true" [loading]="modOrSubsection.spinner || modOrSubsection.handlerData.spinner"
(action)="prefetchModule(modOrSubsection)"
[statusesTranslatable]="{notdownloaded: 'addon.storagemanager.downloaddatafrom' }"
[statusSubject]="module.name" />
<ion-button fill="clear" (click)="deleteForModule($event, module)"
*ngIf="!module.calculatingSize && module.totalSize > 0" color="danger">
[statusSubject]="modOrSubsection.name" />
<ion-button fill="clear" (click)="deleteForModule($event, modOrSubsection)"
*ngIf="!modOrSubsection.calculatingSize && modOrSubsection.totalSize > 0" color="danger">
<ion-icon name="fas-trash" slot="icon-only" [attr.aria-label]="
'addon.storagemanager.deletedatafrom' | translate: { name: module.name }" />
'addon.storagemanager.deletedatafrom' | translate: { name: modOrSubsection.name }" />
</ion-button>
<p *ngIf="!downloadEnabled || !module.handlerData?.showDownloadButton" class="sr-only">
<p *ngIf="!downloadEnabled || !modOrSubsection.handlerData?.showDownloadButton" class="sr-only">
{{ 'core.notdownloadable' | translate }}
</p>
</div>

View File

@ -14,10 +14,11 @@
import { CoreConstants, DownloadStatus } from '@/core/constants';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, OnDestroy, OnInit } from '@angular/core';
import { CoreCourse, CoreCourseProvider } from '@features/course/services/course';
import { CoreCourse, CoreCourseProvider, sectionContentIsModule } from '@features/course/services/course';
import {
CoreCourseHelper,
CoreCourseModuleData,
CoreCourseSection,
CoreCourseSectionWithStatus,
CorePrefetchStatusInfo,
} from '@features/course/services/course-helper';
@ -31,9 +32,8 @@ import { CoreNavigator } from '@services/navigator';
import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { Translate } from '@singletons';
import { CoreArray } from '@singletons/array';
import { CoreDom } from '@singletons/dom';
import { CoreEventObserver, CoreEvents, CoreEventSectionStatusChangedData } from '@singletons/events';
import { CoreEventObserver, CoreEvents } from '@singletons/events';
/**
* Page that displays the amount of file storage used by each activity on the course, and allows
@ -66,6 +66,7 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
};
statusDownloaded = DownloadStatus.DOWNLOADED;
isModule = sectionContentIsModule;
protected siteUpdatedObserver?: CoreEventObserver;
protected courseStatusObserver?: CoreEventObserver;
@ -116,30 +117,8 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
const sections = (await CoreCourse.getSections(this.courseId, false, true))
.filter((section) => !CoreCourseHelper.isSectionStealth(section));
const sectionsToRender = (await CoreCourseHelper.addHandlerDataForModules(sections, this.courseId)).sections
.map(section => ({
...section,
totalSize: 0,
calculatingSize: false,
expanded: section.id === initialSectionId,
modules: section.modules.map(module => ({
...module,
totalSize: 0,
calculatingSize: false,
})),
}));
const subSections = sectionsToRender.filter((section) => section.component === 'mod_subsection');
this.sections = sectionsToRender.filter((section) => section.component !== 'mod_subsection');
this.sections.forEach((section) => {
section.modules.forEach((module) => {
if (module.modname === 'subsection') {
module.subSection = subSections.find((section) =>
section.component === 'mod_subsection' && section.itemid === module.instance);
}
});
});
this.sections = (await CoreCourseHelper.addHandlerDataForModules(sections, this.courseId)).sections
.map(section => this.formatSection(section));
this.loaded = true;
@ -165,6 +144,33 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
this.changeDetectorRef.markForCheck();
}
/**
* Format a section.
*
* @param section Section to format.
* @param expanded Whether section should be expanded.
* @returns Formatted section,
*/
protected formatSection(section: CoreCourseSection, expanded = false): AddonStorageManagerCourseSection {
return {
...section,
totalSize: 0,
calculatingSize: true,
expanded: expanded,
contents: section.contents.map(modOrSubsection => {
if (sectionContentIsModule(modOrSubsection)) {
return {
...modOrSubsection,
totalSize: 0,
calculatingSize: false,
};
}
return this.formatSection(modOrSubsection, expanded);
}),
};
}
/**
* Init course prefetch information.
*/
@ -221,12 +227,12 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
}
// Get the affected section.
const sectionFinder = CoreCourseHelper.findSectionWithSubsection(this.sections, data.sectionId);
if (!sectionFinder?.section) {
const { section } = CoreCourseHelper.findSection(this.sections, { id: data.sectionId });
if (!section) {
return;
}
const section = sectionFinder.section;
// @todo: Handle parents too? It seems the SECTION_STATUS_CHANGED event is never triggered.
// Check if the affected section is being downloaded.
// If so, we don't update section status because it'll already be updated when the download finishes.
@ -247,30 +253,9 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
);
this.moduleStatusObserver = CoreEvents.on(CoreEvents.PACKAGE_STATUS_CHANGED, (data) => {
let moduleFound: AddonStorageManagerModule | undefined;
this.sections.some((section) =>
section.modules.some((module) => {
if (module.id === data.componentId &&
module.prefetchHandler &&
data.component === module.prefetchHandler?.component) {
moduleFound = module;
return true;
} else if (module.subSection) {
return module.subSection.modules.some((module) => {
if (module.id === data.componentId &&
module.prefetchHandler &&
data.component === module.prefetchHandler?.component) {
moduleFound = module;
return true;
}
});
}
return false;
}));
const modules = CoreCourse.getSectionsModules(this.sections);
const moduleFound = modules.find(module => module.id === data.componentId && module.prefetchHandler &&
data.component === module.prefetchHandler?.component);
if (!moduleFound) {
return;
@ -278,13 +263,6 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
// Call determineModuleStatus to get the right status to display.
const status = CoreCourseModulePrefetchDelegate.determineModuleStatus(moduleFound, data.status);
if (moduleFound.subSection) {
const data: CoreEventSectionStatusChangedData = {
sectionId: moduleFound.subSection.id,
courseId: this.courseId,
};
CoreEvents.trigger(CoreEvents.SECTION_STATUS_CHANGED, data, CoreSites.getCurrentSiteId());
}
// Update the status.
this.updateModuleStatus(moduleFound, status);
@ -307,22 +285,15 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
* @param sections Modules.
*/
protected async updateSizes(sections: AddonStorageManagerCourseSection[]): Promise<void> {
sections = CoreArray.unique(sections, 'id');
this.calculatingSize = true;
sections.forEach((section) => {
CoreCourseHelper.flattenSections(sections).forEach((section) => {
section.calculatingSize = true;
section.modules.map((module) => {
if (module.subSection) {
module.subSection.calculatingSize = true;
}
});
});
this.changeDetectorRef.markForCheck();
// Update only affected module sections.
const modules = this.getAllModulesList(sections);
const modules = CoreCourse.getSectionsModules(sections);
await Promise.all(modules.map(async (module) => {
await this.calculateModuleSize(module);
}));
@ -333,13 +304,12 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
this.changeDetectorRef.markForCheck();
section.modules.forEach((module) => {
if (module.subSection) {
updateSectionSize(module.subSection);
module.totalSize = module.subSection.totalSize;
section.contents.forEach((modOrSubsection) => {
if (!sectionContentIsModule(modOrSubsection)) {
updateSectionSize(modOrSubsection);
}
section.totalSize += module.totalSize ?? 0;
section.totalSize += modOrSubsection.totalSize ?? 0;
this.changeDetectorRef.markForCheck();
});
section.calculatingSize = false;
@ -390,7 +360,8 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
return;
}
const modules = this.getAllModulesList(this.sections).filter((module) => module.totalSize && module.totalSize > 0);
const modules = CoreCourse.getSectionsModules(this.sections)
.filter((module) => module.totalSize && module.totalSize > 0);
await this.deleteModules(modules);
}
@ -420,22 +391,7 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
return;
}
const modules: AddonStorageManagerModule[] = [];
section.modules.forEach((module) => {
if (module.subSection) {
module.subSection.modules.forEach((module) => {
if (module.totalSize && module.totalSize > 0) {
modules.push(module);
}
});
return;
}
if (module.totalSize && module.totalSize > 0) {
modules.push(module);
}
});
const modules = CoreCourse.getSectionsModules([section]).filter((module) => module.totalSize && module.totalSize > 0);
await this.deleteModules(modules);
}
@ -481,16 +437,17 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
protected async deleteModules(modules: AddonStorageManagerModule[]): Promise<void> {
const modal = await CoreLoadings.show('core.deleting', true);
const sections: AddonStorageManagerCourseSection[] = [];
const sections = new Set<AddonStorageManagerCourseSection>();
const promises = modules.map(async (module) => {
// Remove the files.
await CoreCourseHelper.removeModuleStoredData(module, this.courseId);
module.totalSize = 0;
const sectionFinder = CoreCourseHelper.findSectionWithSubsection(this.sections, module.section);
if (sectionFinder?.section) {
sections.push(sectionFinder?.section);
const { section, parents } = CoreCourseHelper.findSection(this.sections, { id: module.section });
const rootSection = parents[0] ?? section;
if (rootSection && !sections.has(rootSection)) {
sections.add(rootSection);
}
});
@ -501,7 +458,7 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
} finally {
modal.dismiss();
await this.updateSizes(sections);
await this.updateSizes(Array.from(sections));
this.changeDetectorRef.markForCheck();
}
@ -583,9 +540,10 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
} finally {
module.spinner = false;
const sectionFinder = CoreCourseHelper.findSectionWithSubsection(this.sections, module.section);
if (sectionFinder?.section) {
await this.updateSizes([sectionFinder?.section]);
const { section, parents } = CoreCourseHelper.findSection(this.sections, { id: module.section });
const rootSection = parents[0] ?? section;
if (rootSection) {
await this.updateSizes([rootSection]);
}
}
}
@ -614,18 +572,20 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
* @param section Section to check.
*/
protected async calculateModulesStatusOnSection(section: AddonStorageManagerCourseSection): Promise<void> {
await Promise.all(section.modules.map(async (module) => {
if (module.handlerData?.showDownloadButton) {
module.spinner = true;
// Listen for changes on this module status, even if download isn't enabled.
module.prefetchHandler = CoreCourseModulePrefetchDelegate.getPrefetchHandlerFor(module.modname);
const status = await CoreCourseModulePrefetchDelegate.getModuleStatus(module, this.courseId);
await Promise.all(section.contents.map(async (modOrSubsection) => {
if (!sectionContentIsModule(modOrSubsection)) {
await this.calculateModulesStatusOnSection(modOrSubsection);
this.updateModuleStatus(module, status);
return;
}
if (module.subSection) {
await this.calculateModulesStatusOnSection(module.subSection);
if (modOrSubsection.handlerData?.showDownloadButton) {
modOrSubsection.spinner = true;
// Listen for changes on this module status, even if download isn't enabled.
modOrSubsection.prefetchHandler = CoreCourseModulePrefetchDelegate.getPrefetchHandlerFor(modOrSubsection.modname);
const status = await CoreCourseModulePrefetchDelegate.getModuleStatus(modOrSubsection, this.courseId);
this.updateModuleStatus(modOrSubsection, status);
}
}));
}
@ -714,29 +674,6 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
}
}
/**
* Get all modules list.
*
* @param sections Sections to get the modules from.
* @returns All modules list.
*/
protected getAllModulesList(sections: AddonStorageManagerCourseSection[]): AddonStorageManagerModule[] {
const modules: AddonStorageManagerModule[] = [];
sections.forEach((section) => {
section.modules.forEach((module) => {
modules.push(module);
if (module.subSection) {
module.subSection.modules.forEach((module) => {
modules.push(module);
});
}
});
});
return modules;
}
/**
* Calculate the size of the modules.
*
@ -770,19 +707,16 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
*/
accordionGroupChange(event?: AccordionGroupChangeEventDetail): void {
const sectionIds = event?.value as string[] ?? this.accordionMultipleValue;
this.sections.forEach((section) => {
const allSections = CoreCourseHelper.flattenSections(this.sections);
allSections.forEach((section) => {
section.expanded = false;
section.modules.forEach((section) => {
if (section.subSection) {
section.subSection.expanded = false;
}
});
});
sectionIds.forEach((sectionId) => {
const sectionToExpand = CoreCourseHelper.findSectionById(this.sections, Number(sectionId));
if (sectionToExpand) {
sectionToExpand.expanded = true;
const section = allSections.find((section) => section.id === Number(sectionId));
if (section) {
section.expanded = true;
}
});
}
@ -796,15 +730,10 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
this.moduleStatusObserver?.off();
this.siteUpdatedObserver?.off();
this.sections.forEach((section) => {
section.modules.forEach((module) => {
module.subSection?.modules.forEach((module) => {
CoreCourse.getSectionsModules(this.sections).forEach((module) => {
module.handlerData?.onDestroy?.();
});
module.handlerData?.onDestroy?.();
});
});
this.isDestroyed = true;
}
@ -813,9 +742,7 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
*
* @param sections Sections to calculate their status.
*/
protected async calculateSectionsStatus(
sections: AddonStorageManagerCourseSection[],
): Promise<void> {
protected async calculateSectionsStatus(sections: AddonStorageManagerCourseSection[]): Promise<void> {
if (!sections) {
return;
}
@ -829,12 +756,6 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
section.isCalculating = true;
await this.calculateModulesStatusOnSection(section);
await CoreCourseHelper.calculateSectionStatus(section, this.courseId, false, false);
await Promise.all(section.modules.map(async (module) => {
if (module.subSection) {
return CoreCourseHelper.calculateSectionStatus(module.subSection, this.courseId, false, false);
}
}));
} finally {
section.isCalculating = false;
}
@ -843,11 +764,11 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
}
type AddonStorageManagerCourseSection = Omit<CoreCourseSectionWithStatus, 'modules'> & {
type AddonStorageManagerCourseSection = Omit<CoreCourseSectionWithStatus, 'contents'> & {
totalSize: number;
calculatingSize: boolean;
expanded: boolean;
modules: AddonStorageManagerModule[];
contents: (AddonStorageManagerCourseSection | AddonStorageManagerModule)[];
};
type AddonStorageManagerModule = CoreCourseModuleData & {
@ -856,5 +777,4 @@ type AddonStorageManagerModule = CoreCourseModuleData & {
prefetchHandler?: CoreCourseModulePrefetchHandler;
spinner?: boolean;
downloadStatus?: DownloadStatus;
subSection?: AddonStorageManagerCourseSection;
};

View File

@ -239,7 +239,7 @@ export class AddonStorageManagerCoursesStoragePage implements OnInit, OnDestroy
*/
private async calculateDownloadedCourseSize(courseId: number): Promise<number> {
const sections = await CoreCourse.getSections(courseId);
const modules = CoreCourseHelper.getSectionsModules(sections);
const modules = CoreCourse.getSectionsModules(sections);
return CoreCourseHelper.getModulesDownloadedSize(modules, courseId);
}

View File

@ -374,7 +374,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
section = lastModuleSection || section;
moduleId = lastModuleSection ? this.lastModuleViewed.cmId : undefined;
} else {
const modules = CoreCourseHelper.getSectionsModules([currentSectionData.section]);
const modules = CoreCourse.getSectionsModules([currentSectionData.section]);
if (modules.some(module => module.id === this.lastModuleViewed?.cmId)) {
// Last module viewed is inside the highlighted section.
moduleId = this.lastModuleViewed.cmId;
@ -665,7 +665,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
continue;
}
const sectionModules = CoreCourseHelper.getSectionsModules([this.sections[this.lastShownSectionIndex]]);
const sectionModules = CoreCourse.getSectionsModules([this.sections[this.lastShownSectionIndex]]);
modulesLoaded += sectionModules.reduce((total, module) =>
!CoreCourseHelper.isModuleStealth(module, this.sections[this.lastShownSectionIndex]) ? total + 1 : total, 0);
@ -817,9 +817,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
});
sectionIds?.forEach((sectionId) => {
const sId = Number(sectionId);
const section = allSections.find((section) => section.id === sId);
const section = allSections.find((section) => section.id === Number(sectionId));
if (section) {
section.expanded = true;
}

View File

@ -102,7 +102,7 @@ export class CoreCourseModuleNavigationComponent implements OnInit, OnDestroy {
const sections = await CoreCourse.getSections(this.courseId, false, true, preSets);
const modules = await CoreCourseHelper.getSectionsModules(sections, {
const modules = await CoreCourse.getSectionsModules(sections, {
ignoreSection: (section) => !this.isSectionAvailable(section),
});
@ -115,7 +115,7 @@ export class CoreCourseModuleNavigationComponent implements OnInit, OnDestroy {
if (checkNext) {
// Find next Module.
this.nextModule = undefined;
for (let i = currentModuleIndex + 1; i < modules.length && this.nextModule == undefined; i++) {
for (let i = currentModuleIndex + 1; i < modules.length && this.nextModule === undefined; i++) {
const module = modules[i];
if (this.isModuleAvailable(module)) {
this.nextModule = module;
@ -126,7 +126,7 @@ export class CoreCourseModuleNavigationComponent implements OnInit, OnDestroy {
if (checkPrevious) {
// Find previous Module.
this.previousModule = undefined;
for (let i = currentModuleIndex - 1; i >= 0 && this.previousModule == undefined; i--) {
for (let i = currentModuleIndex - 1; i >= 0 && this.previousModule === undefined; i--) {
const module = modules[i];
if (this.isModuleAvailable(module)) {
this.previousModule = module;

View File

@ -27,6 +27,7 @@ import {
import {
CoreCourseHelper,
CoreCourseModuleCompletionData,
CoreCourseModuleData,
CoreCourseSection,
} from '@features/course/services/course-helper';
import { CoreCourseFormatDelegate } from '@features/course/services/format-delegate';
@ -215,10 +216,11 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy, CoreRefreshCon
protected async loadSections(refresh?: boolean): Promise<void> {
// Get all the sections.
const sections = await CoreCourse.getSections(this.course.id, false, true);
let modules: CoreCourseModuleData[] | undefined;
if (refresh) {
// Invalidate the recently downloaded module list. To ensure info can be prefetched.
const modules = CoreCourseHelper.getSectionsModules(sections);
modules = CoreCourse.getSectionsModules(sections);
await CoreCourseModulePrefetchDelegate.invalidateModules(modules, this.course.id);
}
@ -227,7 +229,9 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy, CoreRefreshCon
// Get the completion status.
if (CoreCoursesHelper.isCompletionEnabledInCourse(this.course)) {
const modules = CoreCourseHelper.getSectionsModules(sections);
if (!modules) {
modules = CoreCourse.getSectionsModules(sections);
}
if (modules[0]?.completion !== undefined) {
// The module already has completion (3.6 onwards). Load the offline completion.
@ -334,7 +338,7 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy, CoreRefreshCon
if (this.sections) {
// If the completion value is not used, the page won't be reloaded, so update the progress bar.
const completionModules = CoreCourseHelper.getSectionsModules(this.sections)
const completionModules = CoreCourse.getSectionsModules(this.sections)
.map((module) => module.completion && module.completion > 0 ? 1 : module.completion)
.reduce((accumulator, currentValue) => (accumulator || 0) + (currentValue || 0), 0);

View File

@ -175,7 +175,7 @@ export class CoreCourseListModTypePage implements OnInit {
while (this.lastShownSectionIndex < this.sections.length - 1 && modulesLoaded < CoreCourseListModTypePage.PAGE_LENGTH) {
this.lastShownSectionIndex++;
const sectionModules = CoreCourseHelper.getSectionsModules([this.sections[this.lastShownSectionIndex]]);
const sectionModules = CoreCourse.getSectionsModules([this.sections[this.lastShownSectionIndex]]);
modulesLoaded += sectionModules.length;
}

View File

@ -78,7 +78,6 @@ import { CoreEnrolAction, CoreEnrolDelegate } from '@features/enrol/services/enr
import { LazyRoutesModule } from '@/app/app-routing.module';
import { CoreModals } from '@services/modals';
import { CoreLoadings } from '@services/loadings';
import { ArrayElement } from '@/core/utils/types';
/**
* Prefetch info of a module.
@ -288,11 +287,11 @@ export class CoreCourseHelperProvider {
throw new CoreError('Invalid section');
}
const sectionWithStatus = <CoreCourseSectionWithStatus> section;
// Get the status of this section based on their modules.
const { modules, subsections } = CoreCourse.classifyContents(section.contents);
// Get the status of this section.
const statusData = await CoreCourseModulePrefetchDelegate.getModulesStatus(
section.contents,
modules,
courseId,
section.id,
refresh,
@ -300,12 +299,20 @@ export class CoreCourseHelperProvider {
checkUpdates,
);
// Now calculate status of subsections, and add them to the status data. Each subsection counts as 1 item in the section.
await Promise.all(subsections.map(async (subsection) => {
const subsectionStatus = await this.calculateSectionStatus(subsection, courseId, refresh, checkUpdates);
statusData.total++;
statusData.status = CoreFilepool.determinePackagesStatus(statusData.status, subsectionStatus.statusData.status);
}));
// Check if it's being downloaded.
const downloadId = this.getSectionDownloadId(section);
if (CoreCourseModulePrefetchDelegate.isBeingDownloaded(downloadId)) {
statusData.status = DownloadStatus.DOWNLOADING;
}
const sectionWithStatus = <CoreCourseSectionWithStatus> section;
sectionWithStatus.downloadStatus = statusData.status;
// Set this section data.
@ -404,42 +411,28 @@ export class CoreCourseHelperProvider {
let count = 0;
const promises = courses.map(async (course) => {
const subPromises: Promise<void>[] = [];
let sections: CoreCourseWSSection[];
let handlers: CoreCourseOptionsHandlerToDisplay[] = [];
let menuHandlers: CoreCourseOptionsMenuHandlerToDisplay[] = [];
let success = true;
// Get the sections and the handlers.
subPromises.push(CoreCourse.getSections(course.id, false, true).then((courseSections) => {
sections = courseSections;
const [sections, handlers, menuHandlers] = await Promise.all([
CoreCourse.getSections(course.id, false, true),
CoreCourseOptionsDelegate.getHandlersToDisplay(course, false),
CoreCourseOptionsDelegate.getMenuHandlersToDisplay(course, false),
]);
return;
}));
subPromises.push(CoreCourseOptionsDelegate.getHandlersToDisplay(course, false).then((cHandlers) => {
handlers = cHandlers;
return;
}));
subPromises.push(CoreCourseOptionsDelegate.getMenuHandlersToDisplay(course, false).then((mHandlers) => {
menuHandlers = mHandlers;
return;
}));
return Promise.all(subPromises).then(() => this.prefetchCourse(course, sections, handlers, menuHandlers, siteId))
.catch((error) => {
try {
await this.prefetchCourse(course, sections, handlers, menuHandlers, siteId);
} catch (error) {
success = false;
throw error;
}).finally(() => {
} finally {
// Course downloaded or failed, notify the progress.
count++;
if (options.onProgress) {
options.onProgress({ count: count, total: total, courseId: course.id, success: success });
}
});
}
});
if (options.onProgress) {
@ -469,20 +462,31 @@ export class CoreCourseHelperProvider {
total: true,
};
await Promise.all(sections.map(async (section) => {
const getSectionSize = async (section: CoreCourseWSSection): Promise<CoreFileSizeSum> => {
if (section.id === CoreCourseProvider.ALL_SECTIONS_ID) {
return;
return { size: 0, total: true };
}
const sectionSize = await CoreCourseModulePrefetchDelegate.getDownloadSize(section.modules, courseId);
const { modules, subsections } = CoreCourse.classifyContents(section.contents);
sizeSum.total = sizeSum.total && sectionSize.total;
sizeSum.size += sectionSize.size;
const [modulesSize, subsectionsSizes] = await Promise.all([
CoreCourseModulePrefetchDelegate.getDownloadSize(modules, courseId),
Promise.all(subsections.map((modOrSubsection) => getSectionSize(modOrSubsection))),
]);
// Check if the section has embedded files in the description.
if (!hasEmbeddedFiles && CoreFilepool.extractDownloadableFilesFromHtml(section.summary).length > 0) {
hasEmbeddedFiles = true;
}
return subsectionsSizes.concat(modulesSize).reduce((sizeSum, contentSize) => ({
size: sizeSum.size + contentSize.size,
total: sizeSum.total && contentSize.total,
}), { size: 0, total: true });
};
await Promise.all(sections.map(async (section) => {
await getSectionSize(section);
}));
if (hasEmbeddedFiles) {
@ -1586,7 +1590,7 @@ export class CoreCourseHelperProvider {
// Prefetch other data needed to render the course.
promises.push(CoreCourses.getCoursesByField('id', course.id));
const modules = this.getSectionsModules(sections);
const modules = CoreCourse.getSectionsModules(sections);
if (!modules.length || modules[0].completion === undefined) {
promises.push(CoreCourse.getActivitiesCompletionStatus(course.id));
}
@ -1646,7 +1650,7 @@ export class CoreCourseHelperProvider {
* @param updateAllSections Update all sections status
*/
async prefetchSections(
sections: (CoreCourseSectionWithStatus & CoreCourseSectionWithSubsections)[],
sections: CoreCourseSectionWithStatus[],
courseId: number,
updateAllSections = false,
): Promise<void> {
@ -1736,11 +1740,14 @@ export class CoreCourseHelperProvider {
* @returns Promise resolved when the section is prefetched.
*/
protected async syncModulesAndPrefetchSection(section: CoreCourseSectionWithStatus, courseId: number): Promise<void> {
const { modules, subsections } = CoreCourse.classifyContents(section.contents);
const syncAndPrefetchModules = async () => {
// Sync the modules first.
await CoreCourseModulePrefetchDelegate.syncModules(section.contents, courseId);
await CoreCourseModulePrefetchDelegate.syncModules(modules, courseId);
// Validate the section needs to be downloaded and calculate amount of modules that need to be downloaded.
const result = await CoreCourseModulePrefetchDelegate.getModulesStatus(section.contents, courseId, section.id);
const result = await CoreCourseModulePrefetchDelegate.getModulesStatus(modules, courseId, section.id);
if (result.status === DownloadStatus.DOWNLOADED || result.status === DownloadStatus.NOT_DOWNLOADABLE) {
// Section is downloaded or not downloadable, nothing to do.
@ -1748,6 +1755,12 @@ export class CoreCourseHelperProvider {
}
await this.prefetchSingleSection(section, result, courseId);
};
await Promise.all([
syncAndPrefetchModules(),
Promise.all(subsections.map(subsection => this.prefetchSingleSectionIfNeeded(subsection, courseId))),
]);
}
/**
@ -1860,7 +1873,7 @@ export class CoreCourseHelperProvider {
async deleteCourseFiles(courseId: number): Promise<void> {
const siteId = CoreSites.getCurrentSiteId();
const sections = await CoreCourse.getSections(courseId);
const modules = this.getSectionsModules(sections);
const modules = CoreCourse.getSectionsModules(sections);
await Promise.all([
...modules.map((module) => this.removeModuleStoredData(module, courseId)),
@ -2116,44 +2129,6 @@ export class CoreCourseHelperProvider {
return { section: foundSection, parents: parents.reverse() };
}
/**
* Given a list of sections, returns the list of modules in the sections.
* The modules are ordered in the order of appearance in the course.
*
* @param sections Sections.
* @param options Other options.
* @returns Modules.
*/
getSectionsModules<
Section extends CoreCourseWSSection,
Module = Extract<ArrayElement<Section['contents']>, CoreCourseModuleData>
>(
sections: Section[],
options: CoreCourseGetSectionsModulesOptions<Section, Module> = {},
): Module[] {
let modules: Module[] = [];
sections.forEach((section) => {
if (options.ignoreSection && options.ignoreSection(section)) {
return;
}
section.contents.forEach((modOrSubsection) => {
if (sectionContentIsModule(modOrSubsection)) {
if (options.ignoreModule && options.ignoreModule(modOrSubsection as Module)) {
return;
}
modules.push(modOrSubsection as Module);
} else {
modules = modules.concat(this.getSectionsModules([modOrSubsection], options));
}
});
});
return modules;
}
/**
* Given a list of sections, returns the list of sections and subsections.
*
@ -2284,11 +2259,3 @@ export type CoreCourseGuestAccessInfo = {
*/
passwordRequired?: boolean;
};
/**
* Options for get sections modules.
*/
export type CoreCourseGetSectionsModulesOptions<Section, Module> = {
ignoreSection?: (section: Section) => boolean; // Function to filter sections. Return true to ignore it, false to use it.
ignoreModule?: (module: Module) => boolean; // Function to filter module. Return true to ignore it, false to use it.
};

View File

@ -64,6 +64,7 @@ import { CoreSiteWSPreSets, WSObservable } from '@classes/sites/authenticated-si
import { CoreLoadings } from '@services/loadings';
import { CoreArray } from '@singletons/array';
import { CoreText } from '@singletons/text';
import { ArrayElement } from '@/core/utils/types';
const ROOT_CACHE_KEY = 'mmCourse:';
@ -1074,13 +1075,40 @@ export class CoreCourseProvider {
/**
* Given a list of sections, returns the list of modules in the sections.
* The modules are ordered in the order of appearance in the course.
*
* @param sections Sections.
* @param options Other options.
* @returns Modules.
* @deprecated since 4.5. Use CoreCourseHelper.getSectionsModules instead.
*/
getSectionsModules(sections: CoreCourseWSSection[]): CoreCourseModuleData[] {
return CoreCourseHelper.getSectionsModules(sections);
getSectionsModules<
Section extends CoreCourseWSSection,
Module = Extract<ArrayElement<Section['contents']>, CoreCourseModuleData>
>(
sections: Section[],
options: CoreCourseGetSectionsModulesOptions<Section, Module> = {},
): Module[] {
let modules: Module[] = [];
sections.forEach((section) => {
if (options.ignoreSection && options.ignoreSection(section)) {
return;
}
section.contents.forEach((modOrSubsection) => {
if (sectionContentIsModule(modOrSubsection)) {
if (options.ignoreModule && options.ignoreModule(modOrSubsection as Module)) {
return;
}
modules.push(modOrSubsection as Module);
} else {
modules = modules.concat(this.getSectionsModules([modOrSubsection], options));
}
});
});
return modules;
}
/**
@ -1635,6 +1663,31 @@ export class CoreCourseProvider {
return CoreDomUtils.removeElementFromHtml(availabilityInfo, 'li[data-action="showmore"]');
}
/**
* Given section contents, classify them into modules and sections.
*
* @param contents Contents.
* @returns Classified contents.
*/
classifyContents<
Contents extends CoreCourseModuleOrSection,
Module = Extract<Contents, CoreCourseModuleData>,
Section = Extract<Contents, CoreCourseWSSection>,
>(contents: Contents[]): { modules: Module[]; subsections: Section[] } {
const modules: Module[] = [];
const subsections: Section[] = [];
contents.forEach((content) => {
if (sectionContentIsModule(content)) {
modules.push(content as Module);
} else {
subsections.push(content as unknown as Section);
}
});
return { modules, subsections };
}
}
export const CoreCourse = makeSingleton(CoreCourseProvider);
@ -2069,3 +2122,11 @@ export type CoreCourseGetSectionsOptions = CoreSitesCommonWSOptions & {
includeStealthModules?: boolean; // Defaults to true.
preSets?: CoreSiteWSPreSets;
};
/**
* Options for get sections modules.
*/
export type CoreCourseGetSectionsModulesOptions<Section, Module> = {
ignoreSection?: (section: Section) => boolean; // Function to filter sections. Return true to ignore it, false to use it.
ignoreModule?: (module: Module) => boolean; // Function to filter module. Return true to ignore it, false to use it.
};