From d1856f5fffc5d3278de8a56caa42de7f4e636a8e Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 4 Oct 2024 14:55:23 +0200 Subject: [PATCH] MOBILE-4660 course: Adapt course downloads to new data structure --- .../activitymodules/activitymodules.ts | 2 +- src/addons/mod/subsection/constants.ts | 15 -- .../subsection/services/handlers/prefetch.ts | 98 ------- .../mod/subsection/subsection.module.ts | 3 - .../pages/course-storage/course-storage.html | 54 ++-- .../pages/course-storage/course-storage.ts | 242 ++++++------------ .../pages/courses-storage/courses-storage.ts | 2 +- .../components/course-format/course-format.ts | 8 +- .../module-navigation/module-navigation.ts | 6 +- .../course/pages/contents/contents.ts | 10 +- .../pages/list-mod-type/list-mod-type.ts | 2 +- .../features/course/services/course-helper.ts | 161 +++++------- src/core/features/course/services/course.ts | 67 ++++- 13 files changed, 252 insertions(+), 418 deletions(-) delete mode 100644 src/addons/mod/subsection/constants.ts delete mode 100644 src/addons/mod/subsection/services/handlers/prefetch.ts diff --git a/src/addons/block/activitymodules/components/activitymodules/activitymodules.ts b/src/addons/block/activitymodules/components/activitymodules/activitymodules.ts index 1f946efba..1c240c1c6 100644 --- a/src/addons/block/activitymodules/components/activitymodules/activitymodules.ts +++ b/src/addons/block/activitymodules/components/activitymodules/activitymodules.ts @@ -70,7 +70,7 @@ export class AddonBlockActivityModulesComponent extends CoreBlockBaseComponent i let modFullNames: Record = {}; const brandedIcons: Record = {}; - const modules = CoreCourseHelper.getSectionsModules(sections, { + const modules = CoreCourse.getSectionsModules(sections, { ignoreSection: section => !CoreCourseHelper.canUserViewSection(section), ignoreModule: module => !CoreCourseHelper.canUserViewModule(module) || !CoreCourse.moduleHasView(module), }); diff --git a/src/addons/mod/subsection/constants.ts b/src/addons/mod/subsection/constants.ts deleted file mode 100644 index ee4912a2b..000000000 --- a/src/addons/mod/subsection/constants.ts +++ /dev/null @@ -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'; diff --git a/src/addons/mod/subsection/services/handlers/prefetch.ts b/src/addons/mod/subsection/services/handlers/prefetch.ts deleted file mode 100644 index 7049468df..000000000 --- a/src/addons/mod/subsection/services/handlers/prefetch.ts +++ /dev/null @@ -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 { - const section = await this.getSection(module, courseId, siteId); - if (!section) { - return; - } - - await CoreCourseHelper.prefetchSections([section], courseId); - } - - /** - * @inheritdoc - */ - async getDownloadSize(module: CoreCourseAnyModuleData, courseId: number): Promise { - 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 { - 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 { - 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); diff --git a/src/addons/mod/subsection/subsection.module.ts b/src/addons/mod/subsection/subsection.module.ts index 4f2d4ad19..a917ddf0e 100644 --- a/src/addons/mod/subsection/subsection.module.ts +++ b/src/addons/mod/subsection/subsection.module.ts @@ -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); }, }, ], diff --git a/src/addons/storagemanager/pages/course-storage/course-storage.html b/src/addons/storagemanager/pages/course-storage/course-storage.html index 0c501b90a..6f5e8398d 100644 --- a/src/addons/storagemanager/pages/course-storage/course-storage.html +++ b/src/addons/storagemanager/pages/course-storage/course-storage.html @@ -16,7 +16,6 @@ - @@ -57,7 +56,7 @@ - + @@ -102,45 +101,46 @@ - - @if (module.subSection) { - + + @if (!isModule(modOrSubsection)) { + } @else { - + *ngIf="downloadEnabled || (!modOrSubsection.calculatingSize && modOrSubsection.totalSize > 0)"> + -

- +

+

- - {{ module.totalSize | + + {{ modOrSubsection.totalSize | coreBytesToSize }} - + {{ 'core.calculating' | translate }}
- - + [statusSubject]="modOrSubsection.name" /> + + 'addon.storagemanager.deletedatafrom' | translate: { name: modOrSubsection.name }" /> -

+

{{ 'core.notdownloadable' | translate }}

diff --git a/src/addons/storagemanager/pages/course-storage/course-storage.ts b/src/addons/storagemanager/pages/course-storage/course-storage.ts index 624c12ea5..ebbdf01f5 100644 --- a/src/addons/storagemanager/pages/course-storage/course-storage.ts +++ b/src/addons/storagemanager/pages/course-storage/course-storage.ts @@ -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 { - 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 { const modal = await CoreLoadings.show('core.deleting', true); - const sections: AddonStorageManagerCourseSection[] = []; + const sections = new Set(); 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 { - 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) => { - module.handlerData?.onDestroy?.(); - }); - - module.handlerData?.onDestroy?.(); - }); + CoreCourse.getSectionsModules(this.sections).forEach((module) => { + 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 { + protected async calculateSectionsStatus(sections: AddonStorageManagerCourseSection[]): Promise { 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 & { +type AddonStorageManagerCourseSection = Omit & { 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; }; diff --git a/src/addons/storagemanager/pages/courses-storage/courses-storage.ts b/src/addons/storagemanager/pages/courses-storage/courses-storage.ts index ca12f9d39..c09367c1d 100644 --- a/src/addons/storagemanager/pages/courses-storage/courses-storage.ts +++ b/src/addons/storagemanager/pages/courses-storage/courses-storage.ts @@ -239,7 +239,7 @@ export class AddonStorageManagerCoursesStoragePage implements OnInit, OnDestroy */ private async calculateDownloadedCourseSize(courseId: number): Promise { const sections = await CoreCourse.getSections(courseId); - const modules = CoreCourseHelper.getSectionsModules(sections); + const modules = CoreCourse.getSectionsModules(sections); return CoreCourseHelper.getModulesDownloadedSize(modules, courseId); } diff --git a/src/core/features/course/components/course-format/course-format.ts b/src/core/features/course/components/course-format/course-format.ts index c3fc1dcce..ca29c70fe 100644 --- a/src/core/features/course/components/course-format/course-format.ts +++ b/src/core/features/course/components/course-format/course-format.ts @@ -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; } diff --git a/src/core/features/course/components/module-navigation/module-navigation.ts b/src/core/features/course/components/module-navigation/module-navigation.ts index 53e460976..75aa69089 100644 --- a/src/core/features/course/components/module-navigation/module-navigation.ts +++ b/src/core/features/course/components/module-navigation/module-navigation.ts @@ -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; diff --git a/src/core/features/course/pages/contents/contents.ts b/src/core/features/course/pages/contents/contents.ts index 1db217366..99148c0e9 100644 --- a/src/core/features/course/pages/contents/contents.ts +++ b/src/core/features/course/pages/contents/contents.ts @@ -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 { // 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); diff --git a/src/core/features/course/pages/list-mod-type/list-mod-type.ts b/src/core/features/course/pages/list-mod-type/list-mod-type.ts index 01e959a87..d4626e320 100644 --- a/src/core/features/course/pages/list-mod-type/list-mod-type.ts +++ b/src/core/features/course/pages/list-mod-type/list-mod-type.ts @@ -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; } diff --git a/src/core/features/course/services/course-helper.ts b/src/core/features/course/services/course-helper.ts index e105880a4..24eb537b9 100644 --- a/src/core/features/course/services/course-helper.ts +++ b/src/core/features/course/services/course-helper.ts @@ -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 = 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 = 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[] = []; - 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; - })); + try { + await this.prefetchCourse(course, sections, handlers, menuHandlers, siteId); + } catch (error) { + success = false; - 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) => { - success = false; - - throw error; - }).finally(() => { + throw error; + } finally { // Course downloaded or failed, notify the progress. - count++; - if (options.onProgress) { - options.onProgress({ count: count, total: total, courseId: course.id, success: success }); - } - }); + 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 => { 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 { @@ -1736,18 +1740,27 @@ export class CoreCourseHelperProvider { * @returns Promise resolved when the section is prefetched. */ protected async syncModulesAndPrefetchSection(section: CoreCourseSectionWithStatus, courseId: number): Promise { - // Sync the modules first. - await CoreCourseModulePrefetchDelegate.syncModules(section.contents, courseId); + const { modules, subsections } = CoreCourse.classifyContents(section.contents); - // 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 syncAndPrefetchModules = async () => { + // Sync the modules first. + await CoreCourseModulePrefetchDelegate.syncModules(modules, courseId); - if (result.status === DownloadStatus.DOWNLOADED || result.status === DownloadStatus.NOT_DOWNLOADABLE) { - // Section is downloaded or not downloadable, nothing to do. - return ; - } + // Validate the section needs to be downloaded and calculate amount of modules that need to be downloaded. + const result = await CoreCourseModulePrefetchDelegate.getModulesStatus(modules, courseId, section.id); - await this.prefetchSingleSection(section, result, courseId); + if (result.status === DownloadStatus.DOWNLOADED || result.status === DownloadStatus.NOT_DOWNLOADABLE) { + // Section is downloaded or not downloadable, nothing to do. + return ; + } + + 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 { 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, CoreCourseModuleData> - >( - sections: Section[], - options: CoreCourseGetSectionsModulesOptions = {}, - ): 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 = { - 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. -}; diff --git a/src/core/features/course/services/course.ts b/src/core/features/course/services/course.ts index 5ae7baa2a..25ae38048 100644 --- a/src/core/features/course/services/course.ts +++ b/src/core/features/course/services/course.ts @@ -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, CoreCourseModuleData> + >( + sections: Section[], + options: CoreCourseGetSectionsModulesOptions = {}, + ): 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, + Section = Extract, + >(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 = { + 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. +};