forked from CIT/Vmeda.Online
		
	MOBILE-4660 course: Refactor how subsections are handled
This commit doesn't refactor course downloads yet, it will be done in another commit.
This commit is contained in:
		
							parent
							
								
									b5b44a8a1d
								
							
						
					
					
						commit
						a169d9301a
					
				@ -70,44 +70,41 @@ export class AddonBlockActivityModulesComponent extends CoreBlockBaseComponent i
 | 
				
			|||||||
        let modFullNames: Record<string, string> = {};
 | 
					        let modFullNames: Record<string, string> = {};
 | 
				
			||||||
        const brandedIcons: Record<string, boolean|undefined> = {};
 | 
					        const brandedIcons: Record<string, boolean|undefined> = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        sections.forEach((section) => {
 | 
					        const modules = CoreCourseHelper.getSectionsModules(sections, {
 | 
				
			||||||
            if (!section.modules) {
 | 
					            ignoreSection: section => !CoreCourseHelper.canUserViewSection(section),
 | 
				
			||||||
 | 
					            ignoreModule: module => !CoreCourseHelper.canUserViewModule(module) || !CoreCourse.moduleHasView(module),
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        modules.forEach((mod) => {
 | 
				
			||||||
 | 
					            if (archetypes[mod.modname] !== undefined) {
 | 
				
			||||||
                return;
 | 
					                return;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            section.modules.forEach((mod) => {
 | 
					            // Get the archetype of the module type.
 | 
				
			||||||
                if (archetypes[mod.modname] !== undefined ||
 | 
					            archetypes[mod.modname] = CoreCourseModuleDelegate.supportsFeature<number>(
 | 
				
			||||||
                    !CoreCourseHelper.canUserViewModule(mod, section) ||
 | 
					                mod.modname,
 | 
				
			||||||
                    !CoreCourse.moduleHasView(mod)) {
 | 
					                CoreConstants.FEATURE_MOD_ARCHETYPE,
 | 
				
			||||||
                    // Ignore this module.
 | 
					                CoreConstants.MOD_ARCHETYPE_OTHER,
 | 
				
			||||||
                    return;
 | 
					            );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Get the full name of the module type.
 | 
				
			||||||
 | 
					            if (archetypes[mod.modname] === CoreConstants.MOD_ARCHETYPE_RESOURCE) {
 | 
				
			||||||
 | 
					                // All resources are gathered in a single "Resources" option.
 | 
				
			||||||
 | 
					                if (!modFullNames['resources']) {
 | 
				
			||||||
 | 
					                    modFullNames['resources'] = Translate.instant('core.resources');
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                modFullNames[mod.modname] = mod.modplural;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                // Get the archetype of the module type.
 | 
					            brandedIcons[mod.modname] = mod.branded;
 | 
				
			||||||
                archetypes[mod.modname] = CoreCourseModuleDelegate.supportsFeature<number>(
 | 
					 | 
				
			||||||
                    mod.modname,
 | 
					 | 
				
			||||||
                    CoreConstants.FEATURE_MOD_ARCHETYPE,
 | 
					 | 
				
			||||||
                    CoreConstants.MOD_ARCHETYPE_OTHER,
 | 
					 | 
				
			||||||
                );
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                // Get the full name of the module type.
 | 
					            // If this is not a theme image, leave it undefined to avoid having specific activity icons.
 | 
				
			||||||
                if (archetypes[mod.modname] === CoreConstants.MOD_ARCHETYPE_RESOURCE) {
 | 
					            if (CoreUrl.isThemeImageUrl(mod.modicon)) {
 | 
				
			||||||
                    // All resources are gathered in a single "Resources" option.
 | 
					                modIcons[mod.modname] = mod.modicon;
 | 
				
			||||||
                    if (!modFullNames['resources']) {
 | 
					            }
 | 
				
			||||||
                        modFullNames['resources'] = Translate.instant('core.resources');
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                    modFullNames[mod.modname] = mod.modplural;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                brandedIcons[mod.modname] = mod.branded;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                // If this is not a theme image, leave it undefined to avoid having specific activity icons.
 | 
					 | 
				
			||||||
                if (CoreUrl.isThemeImageUrl(mod.modicon)) {
 | 
					 | 
				
			||||||
                    modIcons[mod.modname] = mod.modicon;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Sort the modnames alphabetically.
 | 
					        // Sort the modnames alphabetically.
 | 
				
			||||||
        modFullNames = CoreUtils.sortValues(modFullNames);
 | 
					        modFullNames = CoreUtils.sortValues(modFullNames);
 | 
				
			||||||
        for (const modName in modFullNames) {
 | 
					        for (const modName in modFullNames) {
 | 
				
			||||||
 | 
				
			|||||||
@ -12,6 +12,8 @@
 | 
				
			|||||||
            </ion-label>
 | 
					            </ion-label>
 | 
				
			||||||
        </ion-item>
 | 
					        </ion-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <core-course-module *ngFor="let module of mainMenuBlock.modules" [module]="module" [section]="mainMenuBlock" />
 | 
					        <ng-container *ngFor="let modOrSubsection of mainMenuBlock.contents">
 | 
				
			||||||
 | 
					            <core-course-module *ngIf="isModule(modOrSubsection)" [module]="modOrSubsection" [section]="mainMenuBlock" />
 | 
				
			||||||
 | 
					        </ng-container>
 | 
				
			||||||
    </ion-list>
 | 
					    </ion-list>
 | 
				
			||||||
</core-loading>
 | 
					</core-loading>
 | 
				
			||||||
 | 
				
			|||||||
@ -14,7 +14,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import { Component, OnInit } from '@angular/core';
 | 
					import { Component, OnInit } from '@angular/core';
 | 
				
			||||||
import { CoreSites } from '@services/sites';
 | 
					import { CoreSites } from '@services/sites';
 | 
				
			||||||
import { CoreCourse } from '@features/course/services/course';
 | 
					import { CoreCourse, sectionContentIsModule } from '@features/course/services/course';
 | 
				
			||||||
import { CoreCourseHelper, CoreCourseSection } from '@features/course/services/course-helper';
 | 
					import { CoreCourseHelper, CoreCourseSection } from '@features/course/services/course-helper';
 | 
				
			||||||
import { CoreSiteHome, FrontPageItemNames } from '@features/sitehome/services/sitehome';
 | 
					import { CoreSiteHome, FrontPageItemNames } from '@features/sitehome/services/sitehome';
 | 
				
			||||||
import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate';
 | 
					import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate';
 | 
				
			||||||
@ -39,6 +39,7 @@ export class AddonBlockSiteMainMenuComponent extends CoreBlockBaseComponent impl
 | 
				
			|||||||
    component = 'AddonBlockSiteMainMenu';
 | 
					    component = 'AddonBlockSiteMainMenu';
 | 
				
			||||||
    mainMenuBlock?: CoreCourseSection;
 | 
					    mainMenuBlock?: CoreCourseSection;
 | 
				
			||||||
    siteHomeId = 1;
 | 
					    siteHomeId = 1;
 | 
				
			||||||
 | 
					    isModule = sectionContentIsModule;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    protected fetchContentDefaultError = 'Error getting main menu data.';
 | 
					    protected fetchContentDefaultError = 'Error getting main menu data.';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -66,9 +67,12 @@ export class AddonBlockSiteMainMenuComponent extends CoreBlockBaseComponent impl
 | 
				
			|||||||
        promises.push(CoreCourse.invalidateSections(this.siteHomeId));
 | 
					        promises.push(CoreCourse.invalidateSections(this.siteHomeId));
 | 
				
			||||||
        promises.push(CoreSiteHome.invalidateNewsForum(this.siteHomeId));
 | 
					        promises.push(CoreSiteHome.invalidateNewsForum(this.siteHomeId));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (this.mainMenuBlock && this.mainMenuBlock.modules) {
 | 
					        if (this.mainMenuBlock?.contents.length) {
 | 
				
			||||||
            // Invalidate modules prefetch data.
 | 
					            // Invalidate modules prefetch data.
 | 
				
			||||||
            promises.push(CoreCourseModulePrefetchDelegate.invalidateModules(this.mainMenuBlock.modules, this.siteHomeId));
 | 
					            promises.push(CoreCourseModulePrefetchDelegate.invalidateModules(
 | 
				
			||||||
 | 
					                CoreCourse.getSectionsModules([this.mainMenuBlock]),
 | 
				
			||||||
 | 
					                this.siteHomeId,
 | 
				
			||||||
 | 
					            ));
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        await Promise.all(promises);
 | 
					        await Promise.all(promises);
 | 
				
			||||||
@ -114,11 +118,11 @@ export class AddonBlockSiteMainMenuComponent extends CoreBlockBaseComponent impl
 | 
				
			|||||||
        try {
 | 
					        try {
 | 
				
			||||||
            const forum = await CoreSiteHome.getNewsForum(this.siteHomeId);
 | 
					            const forum = await CoreSiteHome.getNewsForum(this.siteHomeId);
 | 
				
			||||||
            // Search the module that belongs to site news.
 | 
					            // Search the module that belongs to site news.
 | 
				
			||||||
            const forumIndex =
 | 
					            const forumIndex = this.mainMenuBlock.contents.findIndex((mod) =>
 | 
				
			||||||
                this.mainMenuBlock.modules.findIndex((mod) => mod.modname == 'forum' && mod.instance == forum.id);
 | 
					                sectionContentIsModule(mod) && mod.modname == 'forum' && mod.instance == forum.id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (forumIndex >= 0) {
 | 
					            if (forumIndex >= 0) {
 | 
				
			||||||
                this.mainMenuBlock.modules.splice(forumIndex, 1);
 | 
					                this.mainMenuBlock.contents.splice(forumIndex, 1);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        } catch {
 | 
					        } catch {
 | 
				
			||||||
            // Ignore errors.
 | 
					            // Ignore errors.
 | 
				
			||||||
 | 
				
			|||||||
@ -19,7 +19,8 @@ import { CoreCourse } from '@features/course/services/course';
 | 
				
			|||||||
import { CoreLoadings } from '@services/loadings';
 | 
					import { CoreLoadings } from '@services/loadings';
 | 
				
			||||||
import { CoreDomUtils } from '@services/utils/dom';
 | 
					import { CoreDomUtils } from '@services/utils/dom';
 | 
				
			||||||
import { makeSingleton } from '@singletons';
 | 
					import { makeSingleton } from '@singletons';
 | 
				
			||||||
import { AddonModSubsection } from '../subsection';
 | 
					import { CoreSites } from '@services/sites';
 | 
				
			||||||
 | 
					import { CoreCourseHelper } from '@features/course/services/course-helper';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Handler to treat links to subsection.
 | 
					 * Handler to treat links to subsection.
 | 
				
			||||||
@ -33,6 +34,29 @@ export class AddonModSubsectionIndexLinkHandlerService extends CoreContentLinksM
 | 
				
			|||||||
        super('AddonModSubsection', 'subsection', 'id');
 | 
					        super('AddonModSubsection', 'subsection', 'id');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Open a subsection.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param sectionId Section ID.
 | 
				
			||||||
 | 
					     * @param courseId Course ID.
 | 
				
			||||||
 | 
					     * @param siteId Site ID. If not defined, current site.
 | 
				
			||||||
 | 
					     * @returns Promise resolved when done.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async openSubsection(sectionId: number, courseId: number, siteId?: string): Promise<void> {
 | 
				
			||||||
 | 
					        const pageParams = {
 | 
				
			||||||
 | 
					            sectionId,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (
 | 
				
			||||||
 | 
					            (!siteId || siteId === CoreSites.getCurrentSiteId()) &&
 | 
				
			||||||
 | 
					            CoreCourse.currentViewIsCourse(courseId)
 | 
				
			||||||
 | 
					        ) {
 | 
				
			||||||
 | 
					            CoreCourse.selectCourseTab('', pageParams);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            await CoreCourseHelper.getAndOpenCourse(courseId, pageParams, siteId);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * @inheritdoc
 | 
					     * @inheritdoc
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
@ -51,7 +75,7 @@ export class AddonModSubsectionIndexLinkHandlerService extends CoreContentLinksM
 | 
				
			|||||||
                    // Get the module.
 | 
					                    // Get the module.
 | 
				
			||||||
                    const module = await CoreCourse.getModule(moduleId, courseId, undefined, true, false, siteId);
 | 
					                    const module = await CoreCourse.getModule(moduleId, courseId, undefined, true, false, siteId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    await AddonModSubsection.openSubsection(module.section, module.course, siteId);
 | 
					                    await this.openSubsection(module.section, module.course, siteId);
 | 
				
			||||||
                } catch (error) {
 | 
					                } catch (error) {
 | 
				
			||||||
                    CoreDomUtils.showErrorModalDefault(error, 'Error opening link.');
 | 
					                    CoreDomUtils.showErrorModalDefault(error, 'Error opening link.');
 | 
				
			||||||
                } finally {
 | 
					                } finally {
 | 
				
			||||||
 | 
				
			|||||||
@ -1,88 +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 { CoreConstants, ModPurpose } from '@/core/constants';
 | 
					 | 
				
			||||||
import { Injectable } from '@angular/core';
 | 
					 | 
				
			||||||
import { CoreModuleHandlerBase } from '@features/course/classes/module-base-handler';
 | 
					 | 
				
			||||||
import { CoreCourseModuleData } from '@features/course/services/course-helper';
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
    CoreCourseModuleDelegate,
 | 
					 | 
				
			||||||
    CoreCourseModuleHandler,
 | 
					 | 
				
			||||||
    CoreCourseModuleHandlerData,
 | 
					 | 
				
			||||||
} from '@features/course/services/module-delegate';
 | 
					 | 
				
			||||||
import { CoreDomUtils } from '@services/utils/dom';
 | 
					 | 
				
			||||||
import { makeSingleton } from '@singletons';
 | 
					 | 
				
			||||||
import { AddonModSubsection } from '../subsection';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * Handler to support subsection modules.
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * This is merely to disable the siteplugin.
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
@Injectable({ providedIn: 'root' })
 | 
					 | 
				
			||||||
export class AddonModSubsectionModuleHandlerService extends CoreModuleHandlerBase implements CoreCourseModuleHandler {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    name = 'AddonModSubsection';
 | 
					 | 
				
			||||||
    modName = 'subsection';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    supportedFeatures = {
 | 
					 | 
				
			||||||
        [CoreConstants.FEATURE_MOD_ARCHETYPE]: CoreConstants.MOD_ARCHETYPE_RESOURCE,
 | 
					 | 
				
			||||||
        [CoreConstants.FEATURE_GROUPS]: false,
 | 
					 | 
				
			||||||
        [CoreConstants.FEATURE_GROUPINGS]: false,
 | 
					 | 
				
			||||||
        [CoreConstants.FEATURE_MOD_INTRO]: false,
 | 
					 | 
				
			||||||
        [CoreConstants.FEATURE_COMPLETION_TRACKS_VIEWS]: true,
 | 
					 | 
				
			||||||
        [CoreConstants.FEATURE_GRADE_HAS_GRADE]: false,
 | 
					 | 
				
			||||||
        [CoreConstants.FEATURE_GRADE_OUTCOMES]: false,
 | 
					 | 
				
			||||||
        [CoreConstants.FEATURE_BACKUP_MOODLE2]: true,
 | 
					 | 
				
			||||||
        [CoreConstants.FEATURE_SHOW_DESCRIPTION]: false,
 | 
					 | 
				
			||||||
        [CoreConstants.FEATURE_MOD_PURPOSE]: ModPurpose.MOD_PURPOSE_CONTENT,
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * @inheritdoc
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    getData(module: CoreCourseModuleData): CoreCourseModuleHandlerData {
 | 
					 | 
				
			||||||
        return {
 | 
					 | 
				
			||||||
            icon: CoreCourseModuleDelegate.getModuleIconSrc(module.modname, module.modicon),
 | 
					 | 
				
			||||||
            title: module.name,
 | 
					 | 
				
			||||||
            a11yTitle: '',
 | 
					 | 
				
			||||||
            class: 'addon-mod-subsection-handler',
 | 
					 | 
				
			||||||
            hasCustomCmListItem: true,
 | 
					 | 
				
			||||||
            action: async(event, module) => {
 | 
					 | 
				
			||||||
                try {
 | 
					 | 
				
			||||||
                    await AddonModSubsection.openSubsection(module.section, module.course);
 | 
					 | 
				
			||||||
                } catch (error) {
 | 
					 | 
				
			||||||
                    CoreDomUtils.showErrorModalDefault(error, 'Error opening subsection.');
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * @inheritdoc
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    async getMainComponent(): Promise<undefined> {
 | 
					 | 
				
			||||||
        // There's no need to implement this because subsection cannot be used in singleactivity course format.
 | 
					 | 
				
			||||||
        return;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * @inheritdoc
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    getIconSrc(): string {
 | 
					 | 
				
			||||||
        return '';
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
export const AddonModSubsectionModuleHandler = makeSingleton(AddonModSubsectionModuleHandlerService);
 | 
					 | 
				
			||||||
@ -1,51 +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 { CoreCourse } from '@features/course/services/course';
 | 
					 | 
				
			||||||
import { CoreCourseHelper } from '@features/course/services/course-helper';
 | 
					 | 
				
			||||||
import { CoreSites } from '@services/sites';
 | 
					 | 
				
			||||||
import { makeSingleton } from '@singletons';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * Service that provides some features for subsections.
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
@Injectable({ providedIn: 'root' })
 | 
					 | 
				
			||||||
export class AddonModSubsectionProvider {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * Open a subsection.
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param sectionId Section ID.
 | 
					 | 
				
			||||||
     * @param courseId Course ID.
 | 
					 | 
				
			||||||
     * @param siteId Site ID. If not defined, current site.
 | 
					 | 
				
			||||||
     * @returns Promise resolved when done.
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    async openSubsection(sectionId: number, courseId: number, siteId?: string): Promise<void> {
 | 
					 | 
				
			||||||
        const pageParams = {
 | 
					 | 
				
			||||||
            sectionId,
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (
 | 
					 | 
				
			||||||
            (!siteId || siteId === CoreSites.getCurrentSiteId()) &&
 | 
					 | 
				
			||||||
            CoreCourse.currentViewIsCourse(courseId)
 | 
					 | 
				
			||||||
        ) {
 | 
					 | 
				
			||||||
            CoreCourse.selectCourseTab('', pageParams);
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            await CoreCourseHelper.getAndOpenCourse(courseId, pageParams, siteId);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
export const AddonModSubsection = makeSingleton(AddonModSubsectionProvider);
 | 
					 | 
				
			||||||
@ -14,9 +14,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import { APP_INITIALIZER, NgModule } from '@angular/core';
 | 
					import { APP_INITIALIZER, NgModule } from '@angular/core';
 | 
				
			||||||
import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate';
 | 
					import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate';
 | 
				
			||||||
import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
 | 
					 | 
				
			||||||
import { AddonModSubsectionIndexLinkHandler } from './services/handlers/index-link';
 | 
					import { AddonModSubsectionIndexLinkHandler } from './services/handlers/index-link';
 | 
				
			||||||
import { AddonModSubsectionModuleHandler } from './services/handlers/module';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
@NgModule({
 | 
					@NgModule({
 | 
				
			||||||
    providers: [
 | 
					    providers: [
 | 
				
			||||||
@ -24,7 +22,6 @@ import { AddonModSubsectionModuleHandler } from './services/handlers/module';
 | 
				
			|||||||
            provide: APP_INITIALIZER,
 | 
					            provide: APP_INITIALIZER,
 | 
				
			||||||
            multi: true,
 | 
					            multi: true,
 | 
				
			||||||
            useValue: () => {
 | 
					            useValue: () => {
 | 
				
			||||||
                CoreCourseModuleDelegate.registerHandler(AddonModSubsectionModuleHandler.instance);
 | 
					 | 
				
			||||||
                CoreContentLinksDelegate.registerHandler(AddonModSubsectionIndexLinkHandler.instance);
 | 
					                CoreContentLinksDelegate.registerHandler(AddonModSubsectionIndexLinkHandler.instance);
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
				
			|||||||
@ -240,7 +240,7 @@ export class AddonStorageManagerCoursesStoragePage implements OnInit, OnDestroy
 | 
				
			|||||||
     */
 | 
					     */
 | 
				
			||||||
    private async calculateDownloadedCourseSize(courseId: number): Promise<number> {
 | 
					    private async calculateDownloadedCourseSize(courseId: number): Promise<number> {
 | 
				
			||||||
        const sections = await CoreCourse.getSections(courseId);
 | 
					        const sections = await CoreCourse.getSections(courseId);
 | 
				
			||||||
        const modules = sections.map((section) => section.modules).flat();
 | 
					        const modules = CoreCourseHelper.getSectionsModules(sections);
 | 
				
			||||||
        const promisedModuleSizes = modules.map(async (module) => {
 | 
					        const promisedModuleSizes = modules.map(async (module) => {
 | 
				
			||||||
            const size = await CoreCourseModulePrefetchDelegate.getModuleStoredSize(module, courseId);
 | 
					            const size = await CoreCourseModulePrefetchDelegate.getModuleStoredSize(module, courseId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -12,9 +12,9 @@
 | 
				
			|||||||
            <core-dynamic-component [component]="singleSectionComponent" [data]="data">
 | 
					            <core-dynamic-component [component]="singleSectionComponent" [data]="data">
 | 
				
			||||||
                <ion-accordion-group [multiple]="true" (ionChange)="accordionMultipleChange($event.detail)"
 | 
					                <ion-accordion-group [multiple]="true" (ionChange)="accordionMultipleChange($event.detail)"
 | 
				
			||||||
                    [value]="accordionMultipleValue">
 | 
					                    [value]="accordionMultipleValue">
 | 
				
			||||||
                    <core-course-section *ngIf="!selectedSection.hiddenbynumsections && selectedSection.id !== stealthModulesSectionId &&
 | 
					                    <core-course-section *ngIf="!selectedSection.hiddenbynumsections && selectedSection.id !== stealthModulesSectionId"
 | 
				
			||||||
                        !selectedSection.component" [course]="course" [section]="selectedSection" [lastModuleViewed]="lastModuleViewed"
 | 
					                        [course]="course" [section]="selectedSection" [lastModuleViewed]="lastModuleViewed" [viewedModules]="viewedModules"
 | 
				
			||||||
                        [viewedModules]="viewedModules" [collapsible]="false" [subSections]="subSections" />
 | 
					                        [collapsible]="false" />
 | 
				
			||||||
                </ion-accordion-group>
 | 
					                </ion-accordion-group>
 | 
				
			||||||
                <core-empty-box *ngIf="!selectedSection.hasContent" icon="fas-table-cells-large"
 | 
					                <core-empty-box *ngIf="!selectedSection.hasContent" icon="fas-table-cells-large"
 | 
				
			||||||
                    [message]="'core.course.nocontentavailable' | translate" />
 | 
					                    [message]="'core.course.nocontentavailable' | translate" />
 | 
				
			||||||
@ -27,12 +27,11 @@
 | 
				
			|||||||
                <ion-accordion-group [multiple]="true" (ionChange)="accordionMultipleChange($event.detail)"
 | 
					                <ion-accordion-group [multiple]="true" (ionChange)="accordionMultipleChange($event.detail)"
 | 
				
			||||||
                    [value]="accordionMultipleValue">
 | 
					                    [value]="accordionMultipleValue">
 | 
				
			||||||
                    @for (section of sections; track section.id) {
 | 
					                    @for (section of sections; track section.id) {
 | 
				
			||||||
                        @if ($index <= lastShownSectionIndex && !section.hiddenbynumsections && section.id !== allSectionsId &&
 | 
					                    @if ($index
 | 
				
			||||||
                            section.id !== stealthModulesSectionId && !section.component) {
 | 
					                    <= lastShownSectionIndex && !section.hiddenbynumsections && section.id !==allSectionsId && section.id
 | 
				
			||||||
                            <core-course-section
 | 
					                        !==stealthModulesSectionId) { <core-course-section [course]="course" [section]="section"
 | 
				
			||||||
                                [course]="course" [section]="section" [lastModuleViewed]="lastModuleViewed" [viewedModules]="viewedModules"
 | 
					                        [lastModuleViewed]="lastModuleViewed" [viewedModules]="viewedModules" [collapsible]="true" />
 | 
				
			||||||
                                [collapsible]="true" [subSections]="subSections" />
 | 
					                    }
 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                </ion-accordion-group>
 | 
					                </ion-accordion-group>
 | 
				
			||||||
            </core-dynamic-component>
 | 
					            </core-dynamic-component>
 | 
				
			||||||
 | 
				
			|||||||
@ -31,9 +31,11 @@ import { CoreCourseAnyCourseData } from '@features/courses/services/courses';
 | 
				
			|||||||
import {
 | 
					import {
 | 
				
			||||||
    CoreCourse,
 | 
					    CoreCourse,
 | 
				
			||||||
    CoreCourseProvider,
 | 
					    CoreCourseProvider,
 | 
				
			||||||
 | 
					    sectionContentIsModule,
 | 
				
			||||||
} from '@features/course/services/course';
 | 
					} from '@features/course/services/course';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
    CoreCourseHelper,
 | 
					    CoreCourseHelper,
 | 
				
			||||||
 | 
					    CoreCourseModuleData,
 | 
				
			||||||
    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';
 | 
				
			||||||
@ -124,7 +126,6 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
 | 
				
			|||||||
    displayCourseIndex = false;
 | 
					    displayCourseIndex = false;
 | 
				
			||||||
    displayBlocks = false;
 | 
					    displayBlocks = false;
 | 
				
			||||||
    hasBlocks = false;
 | 
					    hasBlocks = false;
 | 
				
			||||||
    subSections: CoreCourseSectionToDisplay[] = []; // List of course subsections.
 | 
					 | 
				
			||||||
    selectedSection?: CoreCourseSectionToDisplay;
 | 
					    selectedSection?: CoreCourseSectionToDisplay;
 | 
				
			||||||
    previousSection?: CoreCourseSectionToDisplay;
 | 
					    previousSection?: CoreCourseSectionToDisplay;
 | 
				
			||||||
    nextSection?: CoreCourseSectionToDisplay;
 | 
					    nextSection?: CoreCourseSectionToDisplay;
 | 
				
			||||||
@ -226,9 +227,6 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (changes.sections && this.sections) {
 | 
					        if (changes.sections && this.sections) {
 | 
				
			||||||
            this.subSections = this.sections.filter((section) => section.component === 'mod_subsection');
 | 
					 | 
				
			||||||
            this.sections = this.sections.filter((section) => section.component !== 'mod_subsection');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            this.treatSections(this.sections);
 | 
					            this.treatSections(this.sections);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        this.changeDetectorRef.markForCheck();
 | 
					        this.changeDetectorRef.markForCheck();
 | 
				
			||||||
@ -326,35 +324,25 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
 | 
				
			|||||||
            this.loaded = true;
 | 
					            this.loaded = true;
 | 
				
			||||||
            this.sectionChanged(sections[0]);
 | 
					            this.sectionChanged(sections[0]);
 | 
				
			||||||
        } else if (this.initialSectionId || this.initialSectionNumber !== undefined) {
 | 
					        } else if (this.initialSectionId || this.initialSectionNumber !== undefined) {
 | 
				
			||||||
            const subSection = this.subSections.find((section) => section.id === this.initialSectionId ||
 | 
					            // We have an input indicating the section ID to load. Search the section.
 | 
				
			||||||
                (section.section !== undefined && section.section === this.initialSectionNumber));
 | 
					            const { section, parents } = CoreCourseHelper.findSection(this.sections, {
 | 
				
			||||||
            if (subSection) {
 | 
					                id: this.initialSectionId,
 | 
				
			||||||
                // The section is a subsection, load the parent section.
 | 
					                num: this.initialSectionNumber,
 | 
				
			||||||
                this.sections.some((section) => {
 | 
					            });
 | 
				
			||||||
                    const module = section.modules.find((module) =>
 | 
					 | 
				
			||||||
                        subSection.itemid === module.instance && module.modname === 'subsection');
 | 
					 | 
				
			||||||
                    if (module) {
 | 
					 | 
				
			||||||
                        this.initialSectionId = module.section;
 | 
					 | 
				
			||||||
                        this.initialSectionNumber = undefined;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                        return true;
 | 
					            if (parents.length) {
 | 
				
			||||||
                    }
 | 
					                // The section is a subsection, load the root section.
 | 
				
			||||||
 | 
					                this.initialSectionId = parents[0].id;
 | 
				
			||||||
                    return false;
 | 
					                this.initialSectionNumber = undefined;
 | 
				
			||||||
                });
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                this.setInputData();
 | 
					                this.setInputData();
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // We have an input indicating the section ID to load. Search the section.
 | 
					 | 
				
			||||||
            const section = sections.find((section) =>
 | 
					 | 
				
			||||||
                section.id === this.initialSectionId ||
 | 
					 | 
				
			||||||
                    (section.section !== undefined && section.section === this.initialSectionNumber));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            // Don't load the section if it cannot be viewed by the user.
 | 
					            // Don't load the section if it cannot be viewed by the user.
 | 
				
			||||||
            if (section && this.canViewSection(section)) {
 | 
					            const sectionToLoad = parents[0] ?? section;
 | 
				
			||||||
 | 
					            if (sectionToLoad && this.canViewSection(sectionToLoad)) {
 | 
				
			||||||
                this.loaded = true;
 | 
					                this.loaded = true;
 | 
				
			||||||
                this.sectionChanged(section);
 | 
					                this.sectionChanged(sectionToLoad);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        } else if (this.initialBlockInstanceId && this.displayBlocks && this.hasBlocks) {
 | 
					        } else if (this.initialBlockInstanceId && this.displayBlocks && this.hasBlocks) {
 | 
				
			||||||
            const { CoreBlockSideBlocksComponent } = await import('@features/block/components/side-blocks/side-blocks');
 | 
					            const { CoreBlockSideBlocksComponent } = await import('@features/block/components/side-blocks/side-blocks');
 | 
				
			||||||
@ -386,9 +374,12 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
                    section = lastModuleSection || section;
 | 
					                    section = lastModuleSection || section;
 | 
				
			||||||
                    moduleId = lastModuleSection ? lastModuleViewed?.cmId : undefined;
 | 
					                    moduleId = lastModuleSection ? lastModuleViewed?.cmId : undefined;
 | 
				
			||||||
                } else if (currentSectionData.section.modules.some(module => module.id === lastModuleViewed.cmId)) {
 | 
					                } else {
 | 
				
			||||||
                    // Last module viewed is inside the highlighted section.
 | 
					                    const modules = CoreCourseHelper.getSectionsModules([currentSectionData.section]);
 | 
				
			||||||
                    moduleId = lastModuleViewed.cmId;
 | 
					                    if (modules.some(module => module.id === lastModuleViewed.cmId)) {
 | 
				
			||||||
 | 
					                        // Last module viewed is inside the highlighted section.
 | 
				
			||||||
 | 
					                        moduleId = lastModuleViewed.cmId;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -422,7 +413,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Get the section of a viewed module.
 | 
					     * Get the section of a viewed module. If the module is in a subsection, returns the root section.
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * @param sections List of sections.
 | 
					     * @param sections List of sections.
 | 
				
			||||||
     * @param viewedModule Viewed module.
 | 
					     * @param viewedModule Viewed module.
 | 
				
			||||||
@ -432,16 +423,11 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
 | 
				
			|||||||
        sections: CoreCourseSection[],
 | 
					        sections: CoreCourseSection[],
 | 
				
			||||||
        viewedModule: CoreCourseViewedModulesDBRecord,
 | 
					        viewedModule: CoreCourseViewedModulesDBRecord,
 | 
				
			||||||
    ): CoreCourseSection | undefined {
 | 
					    ): CoreCourseSection | undefined {
 | 
				
			||||||
        let lastModuleSection: CoreCourseSection | undefined;
 | 
					        const { section, parents } = CoreCourseHelper.findSection(sections, {
 | 
				
			||||||
 | 
					            id: viewedModule.sectionId,
 | 
				
			||||||
        if (viewedModule.sectionId) {
 | 
					            moduleId: viewedModule.cmId,
 | 
				
			||||||
            lastModuleSection = sections.find(section => section.id === viewedModule.sectionId);
 | 
					        });
 | 
				
			||||||
        }
 | 
					        const lastModuleSection: CoreCourseSection | undefined = parents[0] ?? section;
 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (!lastModuleSection) {
 | 
					 | 
				
			||||||
            // No sectionId or section not found. Search the module.
 | 
					 | 
				
			||||||
            lastModuleSection = sections.find(section => section.modules.some(module => module.id === viewedModule.cmId));
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return lastModuleSection && lastModuleSection.id !== this.stealthModulesSectionId ? lastModuleSection : undefined;
 | 
					        return lastModuleSection && lastModuleSection.id !== this.stealthModulesSectionId ? lastModuleSection : undefined;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -488,7 +474,6 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
 | 
				
			|||||||
            componentProps: {
 | 
					            componentProps: {
 | 
				
			||||||
                course: this.course,
 | 
					                course: this.course,
 | 
				
			||||||
                sections: this.sections,
 | 
					                sections: this.sections,
 | 
				
			||||||
                subSections: this.subSections,
 | 
					 | 
				
			||||||
                selectedId: selectedId,
 | 
					                selectedId: selectedId,
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
@ -496,29 +481,32 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
 | 
				
			|||||||
        if (!data) {
 | 
					        if (!data) {
 | 
				
			||||||
            return;
 | 
					            return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        let section = this.sections.find((section) => section.id === data.sectionId);
 | 
					 | 
				
			||||||
        if (!section) {
 | 
					 | 
				
			||||||
            return;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        this.sectionChanged(section);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (data.subSectionId) {
 | 
					        const { section, parents } = CoreCourseHelper.findSection(this.sections, {
 | 
				
			||||||
            section = this.subSections.find((section) => section.id === data.subSectionId);
 | 
					            moduleId: data.moduleId,
 | 
				
			||||||
            if (!section) {
 | 
					            id: data.moduleId === undefined ? data.sectionId : undefined,
 | 
				
			||||||
                return;
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Select the root section.
 | 
				
			||||||
 | 
					        this.sectionChanged(parents[0] ?? section);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (parents.length && section) {
 | 
				
			||||||
 | 
					            // It's a subsection. Expand all the parents and the subsection.
 | 
				
			||||||
 | 
					            for (let i = 1; i < parents.length; i++) {
 | 
				
			||||||
 | 
					                this.setSectionExpanded(parents[i]);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // Use this section to find the module.
 | 
					 | 
				
			||||||
            this.setSectionExpanded(section);
 | 
					            this.setSectionExpanded(section);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // Scroll to the subsection (later it may be scrolled to the module).
 | 
					            // Scroll to the subsection (later it may be scrolled to the module).
 | 
				
			||||||
            this.scrollInCourse(section.id, true);
 | 
					            this.scrollInCourse(section.id, true);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (!data.moduleId) {
 | 
					        if (!data.moduleId || !section) {
 | 
				
			||||||
            return;
 | 
					            return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        const module = section.modules.find((module) => module.id === data.moduleId);
 | 
					        const module = <CoreCourseModuleData | undefined>
 | 
				
			||||||
 | 
					            section.contents.find((module) => sectionContentIsModule(module) && module.id === data.moduleId);
 | 
				
			||||||
        if (!module) {
 | 
					        if (!module) {
 | 
				
			||||||
            return;
 | 
					            return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@ -676,12 +664,14 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            // Skip sections without content, with stealth modules or collapsed.
 | 
					            // Skip sections without content, with stealth modules or collapsed.
 | 
				
			||||||
            if (!this.sections[this.lastShownSectionIndex].hasContent ||
 | 
					            if (!this.sections[this.lastShownSectionIndex].hasContent ||
 | 
				
			||||||
                !this.sections[this.lastShownSectionIndex].modules ||
 | 
					                !this.sections[this.lastShownSectionIndex].contents ||
 | 
				
			||||||
                !this.sections[this.lastShownSectionIndex].expanded) {
 | 
					                !this.sections[this.lastShownSectionIndex].expanded) {
 | 
				
			||||||
                continue;
 | 
					                continue;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            modulesLoaded += this.sections[this.lastShownSectionIndex].modules.reduce((total, module) =>
 | 
					            const sectionModules = CoreCourseHelper.getSectionsModules([this.sections[this.lastShownSectionIndex]]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            modulesLoaded += sectionModules.reduce((total, module) =>
 | 
				
			||||||
                !CoreCourseHelper.isModuleStealth(module, this.sections[this.lastShownSectionIndex]) ? total + 1 : total, 0);
 | 
					                !CoreCourseHelper.isModuleStealth(module, this.sections[this.lastShownSectionIndex]) ? total + 1 : total, 0);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -782,9 +772,8 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
 | 
				
			|||||||
     * Save expanded sections for the course.
 | 
					     * Save expanded sections for the course.
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    protected async saveExpandedSections(): Promise<void> {
 | 
					    protected async saveExpandedSections(): Promise<void> {
 | 
				
			||||||
        let expandedSections = this.sections.filter((section) => section.expanded && section.id > 0).map((section) => section.id);
 | 
					        const expandedSections = CoreCourseHelper.flattenSections(this.sections)
 | 
				
			||||||
        expandedSections =
 | 
					            .filter((section) => section.expanded && section.id > 0).map((section) => section.id);
 | 
				
			||||||
            expandedSections.concat(this.subSections.filter((section) => section.expanded).map((section) => section.id));
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        await this.currentSite?.setLocalSiteConfig(
 | 
					        await this.currentSite?.setLocalSiteConfig(
 | 
				
			||||||
            `${COURSE_EXPANDED_SECTIONS_PREFIX}${this.course.id}`,
 | 
					            `${COURSE_EXPANDED_SECTIONS_PREFIX}${this.course.id}`,
 | 
				
			||||||
@ -802,12 +791,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        // Expand all sections if not defined.
 | 
					        // Expand all sections if not defined.
 | 
				
			||||||
        if (expandedSections === undefined) {
 | 
					        if (expandedSections === undefined) {
 | 
				
			||||||
            this.sections.forEach((section) => {
 | 
					            CoreCourseHelper.flattenSections(this.sections).forEach((section) => {
 | 
				
			||||||
                section.expanded = true;
 | 
					 | 
				
			||||||
                this.accordionMultipleValue.push(section.id.toString());
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            this.subSections.forEach((section) => {
 | 
					 | 
				
			||||||
                section.expanded = true;
 | 
					                section.expanded = true;
 | 
				
			||||||
                this.accordionMultipleValue.push(section.id.toString());
 | 
					                this.accordionMultipleValue.push(section.id.toString());
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
@ -817,11 +801,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        this.accordionMultipleValue = expandedSections.split(',');
 | 
					        this.accordionMultipleValue = expandedSections.split(',');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.sections.forEach((section) => {
 | 
					        CoreCourseHelper.flattenSections(this.sections).forEach((section) => {
 | 
				
			||||||
            section.expanded = this.accordionMultipleValue.includes(section.id.toString());
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        this.subSections.forEach((section) => {
 | 
					 | 
				
			||||||
            section.expanded = this.accordionMultipleValue.includes(section.id.toString());
 | 
					            section.expanded = this.accordionMultipleValue.includes(section.id.toString());
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -833,20 +813,14 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
 | 
				
			|||||||
     */
 | 
					     */
 | 
				
			||||||
    accordionMultipleChange(ev: AccordionGroupChangeEventDetail): void {
 | 
					    accordionMultipleChange(ev: AccordionGroupChangeEventDetail): void {
 | 
				
			||||||
        const sectionIds = ev.value as string[] | undefined;
 | 
					        const sectionIds = ev.value as string[] | undefined;
 | 
				
			||||||
        this.sections.forEach((section) => {
 | 
					        const allSections = CoreCourseHelper.flattenSections(this.sections);
 | 
				
			||||||
            section.expanded = false;
 | 
					        allSections.forEach((section) => {
 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        this.subSections.forEach((section) => {
 | 
					 | 
				
			||||||
            section.expanded = false;
 | 
					            section.expanded = false;
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        sectionIds?.forEach((sectionId) => {
 | 
					        sectionIds?.forEach((sectionId) => {
 | 
				
			||||||
            const sId = Number(sectionId);
 | 
					            const sId = Number(sectionId);
 | 
				
			||||||
            let section = this.sections.find((section) => section.id === sId);
 | 
					            const section = allSections.find((section) => section.id === sId);
 | 
				
			||||||
            if (!section) {
 | 
					 | 
				
			||||||
                section = this.subSections.find((section) => section.id === sId);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (section) {
 | 
					            if (section) {
 | 
				
			||||||
                section.expanded = true;
 | 
					                section.expanded = true;
 | 
				
			||||||
 | 
				
			|||||||
@ -56,30 +56,31 @@
 | 
				
			|||||||
    </ion-item>
 | 
					    </ion-item>
 | 
				
			||||||
    <div id="core-course-index-section-{{section.id}}" class="core-course-index-section-content">
 | 
					    <div id="core-course-index-section-{{section.id}}" class="core-course-index-section-content">
 | 
				
			||||||
        <ng-container *ngIf="section.expanded">
 | 
					        <ng-container *ngIf="section.expanded">
 | 
				
			||||||
            <ng-container *ngFor="let module of section.modules">
 | 
					            <ng-container *ngFor="let modOrSubsection of section.contents">
 | 
				
			||||||
                @if (module.subSection) {
 | 
					                @if (!isModule(modOrSubsection)) {
 | 
				
			||||||
                <div class="core-course-index-subsection">
 | 
					                <div class="core-course-index-subsection">
 | 
				
			||||||
                    <ng-container *ngTemplateOutlet="sectionTemplate; context: {section: module.subSection}" />
 | 
					                    <ng-container *ngTemplateOutlet="sectionTemplate; context: {section: modOrSubsection}" />
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
                } @else {
 | 
					                } @else {
 | 
				
			||||||
                <ion-item class="module" [class.item-dimmed]="!module.visible" [class.indented]="module.indented"
 | 
					                <ion-item class="module" [class.item-dimmed]="!modOrSubsection.visible" [class.indented]="modOrSubsection.indented"
 | 
				
			||||||
                    [class.item-hightlighted]="section.highlighted" (click)="selectSectionOrModule($event, section.id, module.id)" button>
 | 
					                    [class.item-hightlighted]="section.highlighted" (click)="selectSectionOrModule($event, section.id, modOrSubsection.id)"
 | 
				
			||||||
                    <ion-icon class="completioninfo completion_none" name="" *ngIf="module.completionStatus === undefined" slot="start"
 | 
					                    button>
 | 
				
			||||||
                        aria-hidden="true" />
 | 
					                    <ion-icon class="completioninfo completion_none" name="" *ngIf="modOrSubsection.completionStatus === undefined"
 | 
				
			||||||
                    <ion-icon class="completioninfo completion_incomplete" name="far-circle" *ngIf="module.completionStatus === 0"
 | 
					                        slot="start" aria-hidden="true" />
 | 
				
			||||||
 | 
					                    <ion-icon class="completioninfo completion_incomplete" name="far-circle" *ngIf="modOrSubsection.completionStatus === 0"
 | 
				
			||||||
                        slot="start" [attr.aria-label]="'core.course.todo' | translate" />
 | 
					                        slot="start" [attr.aria-label]="'core.course.todo' | translate" />
 | 
				
			||||||
                    <ion-icon class="completioninfo completion_complete" name="fas-circle"
 | 
					                    <ion-icon class="completioninfo completion_complete" name="fas-circle"
 | 
				
			||||||
                        *ngIf="module.completionStatus === 1 || module.completionStatus === 2" color="success" slot="start"
 | 
					                        *ngIf="modOrSubsection.completionStatus === 1 || modOrSubsection.completionStatus === 2" color="success"
 | 
				
			||||||
                        [attr.aria-label]="'core.course.done' | translate" />
 | 
					                        slot="start" [attr.aria-label]="'core.course.done' | translate" />
 | 
				
			||||||
                    <ion-icon class="completioninfo completion_fail" name="fas-xmark" *ngIf="module.completionStatus === 3" color="danger"
 | 
					                    <ion-icon class="completioninfo completion_fail" name="fas-xmark" *ngIf="modOrSubsection.completionStatus === 3"
 | 
				
			||||||
                        slot="start" [attr.aria-label]="'core.course.failed' | translate" />
 | 
					                        color="danger" slot="start" [attr.aria-label]="'core.course.failed' | translate" />
 | 
				
			||||||
                    <ion-label>
 | 
					                    <ion-label>
 | 
				
			||||||
                        <p class="item-heading">
 | 
					                        <p class="item-heading">
 | 
				
			||||||
                            <core-format-text [text]="module.name" contextLevel="module" [contextInstanceId]="module.id"
 | 
					                            <core-format-text [text]="modOrSubsection.name" contextLevel="module" [contextInstanceId]="modOrSubsection.id"
 | 
				
			||||||
                                [courseId]="module.course" />
 | 
					                                [courseId]="modOrSubsection.course" />
 | 
				
			||||||
                        </p>
 | 
					                        </p>
 | 
				
			||||||
                    </ion-label>
 | 
					                    </ion-label>
 | 
				
			||||||
                    <ion-icon name="fas-lock" *ngIf="!module.uservisible" slot="end" class="restricted"
 | 
					                    <ion-icon name="fas-lock" *ngIf="!modOrSubsection.uservisible" slot="end" class="restricted"
 | 
				
			||||||
                        [attr.aria-label]="'core.restricted' | translate" />
 | 
					                        [attr.aria-label]="'core.restricted' | translate" />
 | 
				
			||||||
                </ion-item>
 | 
					                </ion-item>
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
				
			|||||||
@ -18,6 +18,7 @@ import {
 | 
				
			|||||||
    CoreCourse,
 | 
					    CoreCourse,
 | 
				
			||||||
    CoreCourseModuleCompletionStatus,
 | 
					    CoreCourseModuleCompletionStatus,
 | 
				
			||||||
    CoreCourseProvider,
 | 
					    CoreCourseProvider,
 | 
				
			||||||
 | 
					    sectionContentIsModule,
 | 
				
			||||||
} from '@features/course/services/course';
 | 
					} from '@features/course/services/course';
 | 
				
			||||||
import { CoreCourseHelper, CoreCourseModuleData, CoreCourseSection } from '@features/course/services/course-helper';
 | 
					import { CoreCourseHelper, CoreCourseModuleData, CoreCourseSection } from '@features/course/services/course-helper';
 | 
				
			||||||
import { CoreCourseFormatCurrentSectionData, CoreCourseFormatDelegate } from '@features/course/services/format-delegate';
 | 
					import { CoreCourseFormatCurrentSectionData, CoreCourseFormatDelegate } from '@features/course/services/format-delegate';
 | 
				
			||||||
@ -43,7 +44,6 @@ import { CoreDom } from '@singletons/dom';
 | 
				
			|||||||
export class CoreCourseCourseIndexComponent implements OnInit {
 | 
					export class CoreCourseCourseIndexComponent implements OnInit {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @Input() sections: CoreCourseSection[] = [];
 | 
					    @Input() sections: CoreCourseSection[] = [];
 | 
				
			||||||
    @Input() subSections: CoreCourseSection[] = [];
 | 
					 | 
				
			||||||
    @Input() selectedId?: number;
 | 
					    @Input() selectedId?: number;
 | 
				
			||||||
    @Input() course?: CoreCourseAnyCourseData;
 | 
					    @Input() course?: CoreCourseAnyCourseData;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -51,6 +51,7 @@ export class CoreCourseCourseIndexComponent implements OnInit {
 | 
				
			|||||||
    highlighted?: string;
 | 
					    highlighted?: string;
 | 
				
			||||||
    sectionsToRender: CourseIndexSection[] = [];
 | 
					    sectionsToRender: CourseIndexSection[] = [];
 | 
				
			||||||
    loaded = false;
 | 
					    loaded = false;
 | 
				
			||||||
 | 
					    isModule = sectionContentIsModule;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    constructor(
 | 
					    constructor(
 | 
				
			||||||
        protected elementRef: ElementRef,
 | 
					        protected elementRef: ElementRef,
 | 
				
			||||||
@ -88,7 +89,7 @@ export class CoreCourseCourseIndexComponent implements OnInit {
 | 
				
			|||||||
        const enableIndentation = await CoreCourse.isCourseIndentationEnabled(site, this.course.id);
 | 
					        const enableIndentation = await CoreCourse.isCourseIndentationEnabled(site, this.course.id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.sectionsToRender = this.sections
 | 
					        this.sectionsToRender = this.sections
 | 
				
			||||||
            .filter((section) => section.component !== 'mod_subsection' && !CoreCourseHelper.isSectionStealth(section))
 | 
					            .filter((section) => !CoreCourseHelper.isSectionStealth(section))
 | 
				
			||||||
            .map((section) => this.mapSectionToRender(section, completionEnabled, enableIndentation, currentSectionData));
 | 
					            .map((section) => this.mapSectionToRender(section, completionEnabled, enableIndentation, currentSectionData));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.highlighted = CoreCourseFormatDelegate.getSectionHightlightedName(this.course);
 | 
					        this.highlighted = CoreCourseFormatDelegate.getSectionHightlightedName(this.course);
 | 
				
			||||||
@ -134,26 +135,7 @@ export class CoreCourseCourseIndexComponent implements OnInit {
 | 
				
			|||||||
     * @param moduleId Selected module id, if any.
 | 
					     * @param moduleId Selected module id, if any.
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    selectSectionOrModule(event: Event, sectionId: number, moduleId?: number): void {
 | 
					    selectSectionOrModule(event: Event, sectionId: number, moduleId?: number): void {
 | 
				
			||||||
        let subSectionId: number | undefined;
 | 
					        ModalController.dismiss(<CoreCourseIndexSectionWithModule> { event, sectionId, moduleId });
 | 
				
			||||||
        this.sectionsToRender.some((section) => {
 | 
					 | 
				
			||||||
            if (section.id === sectionId) {
 | 
					 | 
				
			||||||
                return true;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            return section.modules.some((module) => {
 | 
					 | 
				
			||||||
                if (module.subSection?.id === sectionId) {
 | 
					 | 
				
			||||||
                    // Always use the parent section.
 | 
					 | 
				
			||||||
                    subSectionId = sectionId;
 | 
					 | 
				
			||||||
                    sectionId = section.id;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    return true;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                return false;
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        ModalController.dismiss({ event, sectionId, subSectionId, moduleId });
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
@ -192,37 +174,26 @@ export class CoreCourseCourseIndexComponent implements OnInit {
 | 
				
			|||||||
        enableIndentation: boolean,
 | 
					        enableIndentation: boolean,
 | 
				
			||||||
        currentSectionData?: CoreCourseFormatCurrentSectionData<CoreCourseSection>,
 | 
					        currentSectionData?: CoreCourseFormatCurrentSectionData<CoreCourseSection>,
 | 
				
			||||||
    ): CourseIndexSection {
 | 
					    ): CourseIndexSection {
 | 
				
			||||||
        const modules = section.modules
 | 
					        const contents = section.contents
 | 
				
			||||||
            .filter((module) => module.modname === 'subsection' || this.renderModule(section, module))
 | 
					            .filter((modOrSubsection) =>
 | 
				
			||||||
            .map((module) => {
 | 
					                !sectionContentIsModule(modOrSubsection) || this.renderModule(section, modOrSubsection))
 | 
				
			||||||
                if (module.modname === 'subsection') {
 | 
					            .map((modOrSubsection) => {
 | 
				
			||||||
                    const subSectionFound = this.subSections.find((subSection) => subSection.itemid === module.instance);
 | 
					                if (!sectionContentIsModule(modOrSubsection)) {
 | 
				
			||||||
                    const subSection = subSectionFound
 | 
					                    return this.mapSectionToRender(modOrSubsection, completionEnabled, enableIndentation);
 | 
				
			||||||
                        ? this.mapSectionToRender(subSectionFound, completionEnabled, enableIndentation)
 | 
					 | 
				
			||||||
                        : undefined;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    return {
 | 
					 | 
				
			||||||
                        id: module.id,
 | 
					 | 
				
			||||||
                        name: module.name,
 | 
					 | 
				
			||||||
                        course: module.course,
 | 
					 | 
				
			||||||
                        visible: !!module.visible,
 | 
					 | 
				
			||||||
                        uservisible: CoreCourseHelper.canUserViewModule(module, section),
 | 
					 | 
				
			||||||
                        indented: true,
 | 
					 | 
				
			||||||
                        subSection,
 | 
					 | 
				
			||||||
                    };
 | 
					 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                const completionStatus = completionEnabled
 | 
					                const completionStatus = completionEnabled
 | 
				
			||||||
                    ? CoreCourseHelper.getCompletionStatus(module.completiondata)
 | 
					                    ? CoreCourseHelper.getCompletionStatus(modOrSubsection.completiondata)
 | 
				
			||||||
                    : undefined;
 | 
					                    : undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                return {
 | 
					                return {
 | 
				
			||||||
                    id: module.id,
 | 
					                    id: modOrSubsection.id,
 | 
				
			||||||
                    name: module.name,
 | 
					                    name: modOrSubsection.name,
 | 
				
			||||||
                    course: module.course,
 | 
					                    modname: modOrSubsection.modname,
 | 
				
			||||||
                    visible: !!module.visible,
 | 
					                    course: modOrSubsection.course,
 | 
				
			||||||
                    uservisible: CoreCourseHelper.canUserViewModule(module, section),
 | 
					                    visible: !!modOrSubsection.visible,
 | 
				
			||||||
                    indented: enableIndentation && module.indent > 0,
 | 
					                    uservisible: CoreCourseHelper.canUserViewModule(modOrSubsection, section),
 | 
				
			||||||
 | 
					                    indented: enableIndentation && modOrSubsection.indent > 0,
 | 
				
			||||||
                    completionStatus,
 | 
					                    completionStatus,
 | 
				
			||||||
                };
 | 
					                };
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
@ -235,8 +206,8 @@ export class CoreCourseCourseIndexComponent implements OnInit {
 | 
				
			|||||||
            uservisible: CoreCourseHelper.canUserViewSection(section),
 | 
					            uservisible: CoreCourseHelper.canUserViewSection(section),
 | 
				
			||||||
            expanded: section.id === this.selectedId,
 | 
					            expanded: section.id === this.selectedId,
 | 
				
			||||||
            highlighted: currentSectionData?.section.id === section.id,
 | 
					            highlighted: currentSectionData?.section.id === section.id,
 | 
				
			||||||
            hasVisibleModules: modules.length > 0,
 | 
					            hasVisibleModules: contents.length > 0,
 | 
				
			||||||
            modules,
 | 
					            contents,
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -251,20 +222,21 @@ type CourseIndexSection = {
 | 
				
			|||||||
    availabilityinfo: boolean;
 | 
					    availabilityinfo: boolean;
 | 
				
			||||||
    visible: boolean;
 | 
					    visible: boolean;
 | 
				
			||||||
    uservisible: boolean;
 | 
					    uservisible: boolean;
 | 
				
			||||||
    modules: {
 | 
					    contents: (CourseIndexSection | CourseIndexModule)[];
 | 
				
			||||||
        id: number;
 | 
					};
 | 
				
			||||||
        course: number;
 | 
					
 | 
				
			||||||
        visible: boolean;
 | 
					type CourseIndexModule = {
 | 
				
			||||||
        indented: boolean;
 | 
					    id: number;
 | 
				
			||||||
        uservisible: boolean;
 | 
					    modname: string;
 | 
				
			||||||
        completionStatus?: CoreCourseModuleCompletionStatus;
 | 
					    course: number;
 | 
				
			||||||
        subSection?: CourseIndexSection;
 | 
					    visible: boolean;
 | 
				
			||||||
    }[];
 | 
					    indented: boolean;
 | 
				
			||||||
 | 
					    uservisible: boolean;
 | 
				
			||||||
 | 
					    completionStatus?: CoreCourseModuleCompletionStatus;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type CoreCourseIndexSectionWithModule = {
 | 
					export type CoreCourseIndexSectionWithModule = {
 | 
				
			||||||
    event: Event;
 | 
					    event: Event;
 | 
				
			||||||
    sectionId: number;
 | 
					    sectionId: number;
 | 
				
			||||||
    subSectionId?: number;
 | 
					 | 
				
			||||||
    moduleId?: number;
 | 
					    moduleId?: number;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -53,16 +53,16 @@
 | 
				
			|||||||
        </ion-label>
 | 
					        </ion-label>
 | 
				
			||||||
    </ion-item>
 | 
					    </ion-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <ng-container *ngFor="let module of modules">
 | 
					    <ng-container *ngFor="let modOrSubsection of section.contents">
 | 
				
			||||||
        @if (module.subSection) {
 | 
					        @if (!isModule(modOrSubsection)) {
 | 
				
			||||||
        <core-course-section [course]="course" [section]="module.subSection" [lastModuleViewed]="lastModuleViewed"
 | 
					        <core-course-section [course]="course" [section]="modOrSubsection" [lastModuleViewed]="lastModuleViewed"
 | 
				
			||||||
            [viewedModules]="viewedModules" [collapsible]="true" />
 | 
					            [viewedModules]="viewedModules" [collapsible]="true" />
 | 
				
			||||||
        } @else {
 | 
					        } @else {
 | 
				
			||||||
        <core-course-module *ngIf="module.visibleoncoursepage !== 0" [module]="module" [section]="section"
 | 
					        <core-course-module *ngIf="modOrSubsection.visibleoncoursepage !== 0" [module]="modOrSubsection" [section]="section"
 | 
				
			||||||
            [showActivityDates]="course.showactivitydates" [showCompletionConditions]="course.showcompletionconditions"
 | 
					            [showActivityDates]="course.showactivitydates" [showCompletionConditions]="course.showcompletionconditions"
 | 
				
			||||||
            [isLastViewed]="lastModuleViewed && lastModuleViewed.cmId === module.id" [class.core-course-module-not-viewed]="
 | 
					            [isLastViewed]="lastModuleViewed && lastModuleViewed.cmId === modOrSubsection.id" [class.core-course-module-not-viewed]="
 | 
				
			||||||
                    !viewedModules[module.id] &&
 | 
					                    !viewedModules[modOrSubsection.id] &&
 | 
				
			||||||
                    (!module.completiondata || module.completiondata.state === completionStatusIncomplete)" />
 | 
					                    (!modOrSubsection.completiondata || modOrSubsection.completiondata.state === completionStatusIncomplete)" />
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    </ng-container>
 | 
					    </ng-container>
 | 
				
			||||||
</ng-template>
 | 
					</ng-template>
 | 
				
			||||||
 | 
				
			|||||||
@ -15,12 +15,9 @@ import {
 | 
				
			|||||||
    Component,
 | 
					    Component,
 | 
				
			||||||
    HostBinding,
 | 
					    HostBinding,
 | 
				
			||||||
    Input,
 | 
					    Input,
 | 
				
			||||||
    OnChanges,
 | 
					 | 
				
			||||||
    OnInit,
 | 
					    OnInit,
 | 
				
			||||||
    SimpleChange,
 | 
					 | 
				
			||||||
} from '@angular/core';
 | 
					} from '@angular/core';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
    CoreCourseModuleData,
 | 
					 | 
				
			||||||
    CoreCourseSection,
 | 
					    CoreCourseSection,
 | 
				
			||||||
} from '@features/course/services/course-helper';
 | 
					} from '@features/course/services/course-helper';
 | 
				
			||||||
import { CoreSharedModule } from '@/core/shared.module';
 | 
					import { CoreSharedModule } from '@/core/shared.module';
 | 
				
			||||||
@ -28,7 +25,7 @@ import { CoreCourseComponentsModule } from '../components.module';
 | 
				
			|||||||
import { toBoolean } from '@/core/transforms/boolean';
 | 
					import { toBoolean } from '@/core/transforms/boolean';
 | 
				
			||||||
import { CoreCourseAnyCourseData } from '@features/courses/services/courses';
 | 
					import { CoreCourseAnyCourseData } from '@features/courses/services/courses';
 | 
				
			||||||
import { CoreCourseViewedModulesDBRecord } from '@features/course/services/database/course';
 | 
					import { CoreCourseViewedModulesDBRecord } from '@features/course/services/database/course';
 | 
				
			||||||
import { CoreCourseModuleCompletionStatus } from '@features/course/services/course';
 | 
					import { CoreCourseModuleCompletionStatus, sectionContentIsModule } from '@features/course/services/course';
 | 
				
			||||||
import { CoreCourseFormatDelegate } from '@features/course/services/format-delegate';
 | 
					import { CoreCourseFormatDelegate } from '@features/course/services/format-delegate';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
@ -44,11 +41,10 @@ import { CoreCourseFormatDelegate } from '@features/course/services/format-deleg
 | 
				
			|||||||
        CoreCourseComponentsModule,
 | 
					        CoreCourseComponentsModule,
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
export class CoreCourseSectionComponent implements OnInit, OnChanges {
 | 
					export class CoreCourseSectionComponent implements OnInit {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @Input({ required: true }) course!: CoreCourseAnyCourseData; // The course to render.
 | 
					    @Input({ required: true }) course!: CoreCourseAnyCourseData; // The course to render.
 | 
				
			||||||
    @Input({ required: true }) section!: CoreCourseSectionToDisplay;
 | 
					    @Input({ required: true }) section!: CoreCourseSectionToDisplay;
 | 
				
			||||||
    @Input() subSections: CoreCourseSectionToDisplay[] = []; // List of subsections in the course.
 | 
					 | 
				
			||||||
    @Input({ transform: toBoolean }) collapsible = true; // Whether the section can be collapsed.
 | 
					    @Input({ transform: toBoolean }) collapsible = true; // Whether the section can be collapsed.
 | 
				
			||||||
    @Input() lastModuleViewed?: CoreCourseViewedModulesDBRecord;
 | 
					    @Input() lastModuleViewed?: CoreCourseViewedModulesDBRecord;
 | 
				
			||||||
    @Input() viewedModules: Record<number, boolean> = {};
 | 
					    @Input() viewedModules: Record<number, boolean> = {};
 | 
				
			||||||
@ -58,9 +54,9 @@ export class CoreCourseSectionComponent implements OnInit, OnChanges {
 | 
				
			|||||||
            return this.collapsible ? 'collapsible' : 'non-collapsible';
 | 
					            return this.collapsible ? 'collapsible' : 'non-collapsible';
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    modules: CoreCourseModuleToDisplay[] = [];
 | 
					 | 
				
			||||||
    completionStatusIncomplete = CoreCourseModuleCompletionStatus.COMPLETION_INCOMPLETE;
 | 
					    completionStatusIncomplete = CoreCourseModuleCompletionStatus.COMPLETION_INCOMPLETE;
 | 
				
			||||||
    highlightedName?: string; // Name to highlight.
 | 
					    highlightedName?: string; // Name to highlight.
 | 
				
			||||||
 | 
					    isModule = sectionContentIsModule;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * @inheritdoc
 | 
					     * @inheritdoc
 | 
				
			||||||
@ -71,28 +67,8 @@ export class CoreCourseSectionComponent implements OnInit, OnChanges {
 | 
				
			|||||||
            : undefined;
 | 
					            : undefined;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * @inheritdoc
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    ngOnChanges(changes: { [name: string]: SimpleChange }): void {
 | 
					 | 
				
			||||||
        if (changes.section && this.section) {
 | 
					 | 
				
			||||||
            this.modules = this.section.modules;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            this.modules.forEach((module) => {
 | 
					 | 
				
			||||||
                if (module.modname === 'subsection') {
 | 
					 | 
				
			||||||
                    module.subSection = this.subSections.find((section) =>
 | 
					 | 
				
			||||||
                        section.component === 'mod_subsection' && section.itemid === module.instance);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type CoreCourseModuleToDisplay = CoreCourseModuleData & {
 | 
					 | 
				
			||||||
    subSection?: CoreCourseSectionToDisplay;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export type CoreCourseSectionToDisplay = CoreCourseSection & {
 | 
					export type CoreCourseSectionToDisplay = CoreCourseSection & {
 | 
				
			||||||
    highlighted?: boolean;
 | 
					    highlighted?: boolean;
 | 
				
			||||||
    expanded?: boolean; // The aim of this property is to avoid DOM overloading.
 | 
					    expanded?: boolean; // The aim of this property is to avoid DOM overloading.
 | 
				
			||||||
 | 
				
			|||||||
@ -42,8 +42,6 @@ export class CoreCourseModuleNavigationComponent implements OnInit, OnDestroy {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    nextModule?: CoreCourseModuleData;
 | 
					    nextModule?: CoreCourseModuleData;
 | 
				
			||||||
    previousModule?: CoreCourseModuleData;
 | 
					    previousModule?: CoreCourseModuleData;
 | 
				
			||||||
    nextModuleSection?: CoreCourseWSSection;
 | 
					 | 
				
			||||||
    previousModuleSection?: CoreCourseWSSection;
 | 
					 | 
				
			||||||
    loaded = false;
 | 
					    loaded = false;
 | 
				
			||||||
    element: HTMLElement;
 | 
					    element: HTMLElement;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -104,46 +102,23 @@ export class CoreCourseModuleNavigationComponent implements OnInit, OnDestroy {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        const sections = await CoreCourse.getSections(this.courseId, false, true, preSets);
 | 
					        const sections = await CoreCourse.getSections(this.courseId, false, true, preSets);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Search the next module.
 | 
					        const modules = await CoreCourseHelper.getSectionsModules(sections, {
 | 
				
			||||||
        let currentModuleIndex = -1;
 | 
					            ignoreSection: (section) => !this.isSectionAvailable(section),
 | 
				
			||||||
 | 
					 | 
				
			||||||
        const currentSectionIndex = sections.findIndex((section) => {
 | 
					 | 
				
			||||||
            if (!this.isSectionAvailable(section)) {
 | 
					 | 
				
			||||||
                // User cannot view the section, skip it.
 | 
					 | 
				
			||||||
                return false;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            currentModuleIndex = section.modules.findIndex((module: CoreCourseModuleData) => module.id == this.currentModuleId);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            return currentModuleIndex >= 0;
 | 
					 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (currentSectionIndex < 0) {
 | 
					        const currentModuleIndex = modules.findIndex((module) => module.id === this.currentModuleId);
 | 
				
			||||||
            // Nothing found. Return.
 | 
					        if (currentModuleIndex < 0) {
 | 
				
			||||||
 | 
					            // Current module found. Return.
 | 
				
			||||||
            return;
 | 
					            return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (checkNext) {
 | 
					        if (checkNext) {
 | 
				
			||||||
            // Find next Module.
 | 
					            // Find next Module.
 | 
				
			||||||
            this.nextModule = undefined;
 | 
					            this.nextModule = undefined;
 | 
				
			||||||
            for (let i = currentSectionIndex; i < sections.length && this.nextModule == undefined; i++) {
 | 
					            for (let i = currentModuleIndex + 1; i < modules.length && this.nextModule == undefined; i++) {
 | 
				
			||||||
                const section = sections[i];
 | 
					                const module = modules[i];
 | 
				
			||||||
 | 
					                if (this.isModuleAvailable(module)) {
 | 
				
			||||||
                if (!this.isSectionAvailable(section)) {
 | 
					                    this.nextModule = module;
 | 
				
			||||||
                    // User cannot view the section, skip it.
 | 
					 | 
				
			||||||
                    continue;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                const startModule = i == currentSectionIndex ? currentModuleIndex + 1 : 0;
 | 
					 | 
				
			||||||
                for (let j = startModule; j < section.modules.length && this.nextModule == undefined; j++) {
 | 
					 | 
				
			||||||
                    const module = section.modules[j];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    const found = await this.isModuleAvailable(module);
 | 
					 | 
				
			||||||
                    if (found) {
 | 
					 | 
				
			||||||
                        this.nextModule = module;
 | 
					 | 
				
			||||||
                        this.nextModuleSection = section;
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@ -151,23 +126,10 @@ export class CoreCourseModuleNavigationComponent implements OnInit, OnDestroy {
 | 
				
			|||||||
        if (checkPrevious) {
 | 
					        if (checkPrevious) {
 | 
				
			||||||
            // Find previous Module.
 | 
					            // Find previous Module.
 | 
				
			||||||
            this.previousModule = undefined;
 | 
					            this.previousModule = undefined;
 | 
				
			||||||
            for (let i = currentSectionIndex; i >= 0 && this.previousModule == undefined; i--) {
 | 
					            for (let i = currentModuleIndex - 1; i >= 0 && this.previousModule == undefined; i--) {
 | 
				
			||||||
                const section = sections[i];
 | 
					                const module = modules[i];
 | 
				
			||||||
 | 
					                if (this.isModuleAvailable(module)) {
 | 
				
			||||||
                if (!this.isSectionAvailable(section)) {
 | 
					                    this.previousModule = module;
 | 
				
			||||||
                    // User cannot view the section, skip it.
 | 
					 | 
				
			||||||
                    continue;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                const startModule = i == currentSectionIndex ? currentModuleIndex - 1 : section.modules.length - 1;
 | 
					 | 
				
			||||||
                for (let j = startModule; j >= 0 && this.previousModule == undefined; j--) {
 | 
					 | 
				
			||||||
                    const module = section.modules[j];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    const found = await this.isModuleAvailable(module);
 | 
					 | 
				
			||||||
                    if (found) {
 | 
					 | 
				
			||||||
                        this.previousModule = module;
 | 
					 | 
				
			||||||
                        this.previousModuleSection = section;
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@ -181,8 +143,8 @@ export class CoreCourseModuleNavigationComponent implements OnInit, OnDestroy {
 | 
				
			|||||||
     * @param module Module to check.
 | 
					     * @param module Module to check.
 | 
				
			||||||
     * @returns Wether the module is available to the user or not.
 | 
					     * @returns Wether the module is available to the user or not.
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    protected async isModuleAvailable(module: CoreCourseModuleData): Promise<boolean> {
 | 
					    protected isModuleAvailable(module: CoreCourseModuleData): boolean {
 | 
				
			||||||
        return !CoreCourseHelper.isModuleStealth(module) && CoreCourse.instance.moduleHasView(module);
 | 
					        return !CoreCourseHelper.isModuleStealth(module) && CoreCourse.moduleHasView(module);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
@ -229,10 +191,8 @@ export class CoreCourseModuleNavigationComponent implements OnInit, OnDestroy {
 | 
				
			|||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (!CoreCourseHelper.canUserViewModule(module)) {
 | 
					        if (!CoreCourseHelper.canUserViewModule(module)) {
 | 
				
			||||||
            const section = next ? this.nextModuleSection : this.previousModuleSection;
 | 
					 | 
				
			||||||
            options.params = {
 | 
					            options.params = {
 | 
				
			||||||
                module,
 | 
					                module,
 | 
				
			||||||
                section,
 | 
					 | 
				
			||||||
            };
 | 
					            };
 | 
				
			||||||
            CoreNavigator.navigateToSitePath('course/' + this.courseId + '/' + module.id +'/module-preview', options);
 | 
					            CoreNavigator.navigateToSitePath('course/' + this.courseId + '/' + module.id +'/module-preview', options);
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
 | 
				
			|||||||
@ -18,7 +18,7 @@ import { CoreCourseModuleDelegate } from '@features/course/services/module-deleg
 | 
				
			|||||||
import { CoreCourseUnsupportedModuleComponent } from '@features/course/components/unsupported-module/unsupported-module';
 | 
					import { CoreCourseUnsupportedModuleComponent } from '@features/course/components/unsupported-module/unsupported-module';
 | 
				
			||||||
import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-component';
 | 
					import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-component';
 | 
				
			||||||
import { CoreCourseAnyCourseData } from '@features/courses/services/courses';
 | 
					import { CoreCourseAnyCourseData } from '@features/courses/services/courses';
 | 
				
			||||||
import { CoreCourseModuleCompletionData, CoreCourseSection } from '@features/course/services/course-helper';
 | 
					import { CoreCourseModuleCompletionData, CoreCourseModuleData, CoreCourseSection } from '@features/course/services/course-helper';
 | 
				
			||||||
import { CoreCourse } from '@features/course/services/course';
 | 
					import { CoreCourse } from '@features/course/services/course';
 | 
				
			||||||
import type { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
 | 
					import type { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -59,7 +59,7 @@ export class CoreCourseFormatSingleActivityComponent implements OnChanges {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // In single activity the module should only have 1 section and 1 module. Get the module.
 | 
					        // In single activity the module should only have 1 section and 1 module. Get the module.
 | 
				
			||||||
        const module = this.sections?.[0].modules?.[0];
 | 
					        const module = this.sections?.[0].contents?.[0] as (CoreCourseModuleData | undefined);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.data.courseId = this.course.id;
 | 
					        this.data.courseId = this.course.id;
 | 
				
			||||||
        this.data.module = module;
 | 
					        this.data.module = module;
 | 
				
			||||||
 | 
				
			|||||||
@ -55,8 +55,8 @@ export class CoreCourseFormatSingleActivityHandlerService implements CoreCourseF
 | 
				
			|||||||
     * @inheritdoc
 | 
					     * @inheritdoc
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    getCourseTitle(course: CoreCourseAnyCourseData, sections?: CoreCourseWSSection[]): string {
 | 
					    getCourseTitle(course: CoreCourseAnyCourseData, sections?: CoreCourseWSSection[]): string {
 | 
				
			||||||
        if (sections?.[0]?.modules?.[0]) {
 | 
					        if (sections?.[0]?.contents?.[0]) {
 | 
				
			||||||
            return sections[0].modules[0].name;
 | 
					            return sections[0].contents[0].name;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return course.fullname || '';
 | 
					        return course.fullname || '';
 | 
				
			||||||
@ -73,8 +73,8 @@ export class CoreCourseFormatSingleActivityHandlerService implements CoreCourseF
 | 
				
			|||||||
     * @inheritdoc
 | 
					     * @inheritdoc
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    displayRefresher(course: CoreCourseAnyCourseData, sections: CoreCourseWSSection[]): boolean {
 | 
					    displayRefresher(course: CoreCourseAnyCourseData, sections: CoreCourseWSSection[]): boolean {
 | 
				
			||||||
        if (sections?.[0]?.modules?.[0]) {
 | 
					        if (sections?.[0]?.contents?.[0] && 'modname' in sections[0].contents[0]) {
 | 
				
			||||||
            return CoreCourseModuleDelegate.displayRefresherInSingleActivity(sections[0].modules[0].modname);
 | 
					            return CoreCourseModuleDelegate.displayRefresherInSingleActivity(sections[0].contents[0].modname);
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            return true;
 | 
					            return true;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
				
			|||||||
@ -27,7 +27,6 @@ import {
 | 
				
			|||||||
import {
 | 
					import {
 | 
				
			||||||
    CoreCourseHelper,
 | 
					    CoreCourseHelper,
 | 
				
			||||||
    CoreCourseModuleCompletionData,
 | 
					    CoreCourseModuleCompletionData,
 | 
				
			||||||
    CoreCourseModuleData,
 | 
					 | 
				
			||||||
    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';
 | 
				
			||||||
@ -219,7 +218,7 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy, CoreRefreshCon
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        if (refresh) {
 | 
					        if (refresh) {
 | 
				
			||||||
            // Invalidate the recently downloaded module list. To ensure info can be prefetched.
 | 
					            // Invalidate the recently downloaded module list. To ensure info can be prefetched.
 | 
				
			||||||
            const modules = CoreCourse.getSectionsModules(sections);
 | 
					            const modules = CoreCourseHelper.getSectionsModules(sections);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            await CoreCourseModulePrefetchDelegate.invalidateModules(modules, this.course.id);
 | 
					            await CoreCourseModulePrefetchDelegate.invalidateModules(modules, this.course.id);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@ -228,9 +227,9 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy, CoreRefreshCon
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        // Get the completion status.
 | 
					        // Get the completion status.
 | 
				
			||||||
        if (CoreCoursesHelper.isCompletionEnabledInCourse(this.course)) {
 | 
					        if (CoreCoursesHelper.isCompletionEnabledInCourse(this.course)) {
 | 
				
			||||||
            const sectionWithModules = sections.find((section) => section.modules.length > 0);
 | 
					            const modules = CoreCourseHelper.getSectionsModules(sections);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (sectionWithModules && sectionWithModules.modules[0].completion !== undefined) {
 | 
					            if (modules[0]?.completion !== undefined) {
 | 
				
			||||||
                // The module already has completion (3.6 onwards). Load the offline completion.
 | 
					                // The module already has completion (3.6 onwards). Load the offline completion.
 | 
				
			||||||
                this.modulesHaveCompletion = true;
 | 
					                this.modulesHaveCompletion = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -335,8 +334,7 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy, CoreRefreshCon
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            if (this.sections) {
 | 
					            if (this.sections) {
 | 
				
			||||||
                // If the completion value is not used, the page won't be reloaded, so update the progress bar.
 | 
					                // If the completion value is not used, the page won't be reloaded, so update the progress bar.
 | 
				
			||||||
                const completionModules = (<CoreCourseModuleData[]> [])
 | 
					                const completionModules = CoreCourseHelper.getSectionsModules(this.sections)
 | 
				
			||||||
                    .concat(...this.sections.map((section) => section.modules))
 | 
					 | 
				
			||||||
                    .map((module) => module.completion && module.completion > 0 ? 1 : module.completion)
 | 
					                    .map((module) => module.completion && module.completion > 0 ? 1 : module.completion)
 | 
				
			||||||
                    .reduce((accumulator, currentValue) => (accumulator || 0) + (currentValue || 0), 0);
 | 
					                    .reduce((accumulator, currentValue) => (accumulator || 0) + (currentValue || 0), 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -18,21 +18,31 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        <ion-list class="core-course-module-list-wrapper">
 | 
					        <ion-list class="core-course-module-list-wrapper">
 | 
				
			||||||
            <ng-container *ngFor="let section of sections; index as i">
 | 
					            <ng-container *ngFor="let section of sections; index as i">
 | 
				
			||||||
                <ion-card *ngIf="i <= lastShownSectionIndex">
 | 
					                <ng-container *ngIf="i <= lastShownSectionIndex">
 | 
				
			||||||
                    <ion-item-divider class="course-section ion-text-wrap" *ngIf="section.name">
 | 
					                    <ng-container *ngTemplateOutlet="sectionTemplate; context: {section: section}" />
 | 
				
			||||||
                        <ion-label>
 | 
					                </ng-container>
 | 
				
			||||||
                            <h2>
 | 
					 | 
				
			||||||
                                <core-format-text [text]="section.name" contextLevel="course" [contextInstanceId]="courseId" />
 | 
					 | 
				
			||||||
                            </h2>
 | 
					 | 
				
			||||||
                        </ion-label>
 | 
					 | 
				
			||||||
                    </ion-item-divider>
 | 
					 | 
				
			||||||
                    <ng-container *ngFor="let module of section.modules">
 | 
					 | 
				
			||||||
                        <core-course-module [module]="module" [section]="section" [showActivityDates]="false" [showAvailability]="false"
 | 
					 | 
				
			||||||
                            [showExtra]="false" [showDownloadStatus]="false" [showCompletion]="false" [showIndentation]="false" />
 | 
					 | 
				
			||||||
                    </ng-container>
 | 
					 | 
				
			||||||
                </ion-card>
 | 
					 | 
				
			||||||
            </ng-container>
 | 
					            </ng-container>
 | 
				
			||||||
        </ion-list>
 | 
					        </ion-list>
 | 
				
			||||||
        <core-infinite-loading [enabled]="canLoadMore" (action)="showMoreActivities($event)" />
 | 
					        <core-infinite-loading [enabled]="canLoadMore" (action)="showMoreActivities($event)" />
 | 
				
			||||||
    </core-loading>
 | 
					    </core-loading>
 | 
				
			||||||
</ion-content>
 | 
					</ion-content>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<ng-template #sectionTemplate let-section="section">
 | 
				
			||||||
 | 
					    <ion-card>
 | 
				
			||||||
 | 
					        <ion-item-divider class="course-section ion-text-wrap" *ngIf="section.name">
 | 
				
			||||||
 | 
					            <ion-label>
 | 
				
			||||||
 | 
					                <h2>
 | 
				
			||||||
 | 
					                    <core-format-text [text]="section.name" contextLevel="course" [contextInstanceId]="courseId" />
 | 
				
			||||||
 | 
					                </h2>
 | 
				
			||||||
 | 
					            </ion-label>
 | 
				
			||||||
 | 
					        </ion-item-divider>
 | 
				
			||||||
 | 
					        <ng-container *ngFor="let modOrSubsection of section.contents">
 | 
				
			||||||
 | 
					            @if (isModule(modOrSubsection)) {
 | 
				
			||||||
 | 
					            <core-course-module [module]="modOrSubsection" [section]="section" [showActivityDates]="false" [showAvailability]="false"
 | 
				
			||||||
 | 
					                [showExtra]="false" [showDownloadStatus]="false" [showCompletion]="false" [showIndentation]="false" />
 | 
				
			||||||
 | 
					            } @else {
 | 
				
			||||||
 | 
					            <ng-container *ngTemplateOutlet="sectionTemplate; context: {section: modOrSubsection}" />
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        </ng-container>
 | 
				
			||||||
 | 
					    </ion-card>
 | 
				
			||||||
 | 
					</ng-template>
 | 
				
			||||||
 | 
				
			|||||||
@ -15,7 +15,7 @@
 | 
				
			|||||||
import { Component, OnInit } from '@angular/core';
 | 
					import { Component, OnInit } from '@angular/core';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { CoreDomUtils } from '@services/utils/dom';
 | 
					import { CoreDomUtils } from '@services/utils/dom';
 | 
				
			||||||
import { CoreCourse } from '@features/course/services/course';
 | 
					import { CoreCourse, CoreCourseWSSection, sectionContentIsModule } from '@features/course/services/course';
 | 
				
			||||||
import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
 | 
					import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
 | 
				
			||||||
import { CoreCourseHelper, CoreCourseSection } from '@features/course/services/course-helper';
 | 
					import { CoreCourseHelper, CoreCourseSection } from '@features/course/services/course-helper';
 | 
				
			||||||
import { CoreNavigator } from '@services/navigator';
 | 
					import { CoreNavigator } from '@services/navigator';
 | 
				
			||||||
@ -30,6 +30,10 @@ import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics';
 | 
				
			|||||||
@Component({
 | 
					@Component({
 | 
				
			||||||
    selector: 'page-core-course-list-mod-type',
 | 
					    selector: 'page-core-course-list-mod-type',
 | 
				
			||||||
    templateUrl: 'list-mod-type.html',
 | 
					    templateUrl: 'list-mod-type.html',
 | 
				
			||||||
 | 
					    styles: `core-course-module:last-child {
 | 
				
			||||||
 | 
					        --activity-border: 0px;
 | 
				
			||||||
 | 
					        --card-padding-bottom: 0px;
 | 
				
			||||||
 | 
					    }`,
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
export class CoreCourseListModTypePage implements OnInit {
 | 
					export class CoreCourseListModTypePage implements OnInit {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -41,6 +45,7 @@ export class CoreCourseListModTypePage implements OnInit {
 | 
				
			|||||||
    courseId = 0;
 | 
					    courseId = 0;
 | 
				
			||||||
    canLoadMore = false;
 | 
					    canLoadMore = false;
 | 
				
			||||||
    lastShownSectionIndex = -1;
 | 
					    lastShownSectionIndex = -1;
 | 
				
			||||||
 | 
					    isModule = sectionContentIsModule;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    protected modName?: string;
 | 
					    protected modName?: string;
 | 
				
			||||||
    protected archetypes: Record<string, number> = {}; // To speed up the check of modules.
 | 
					    protected archetypes: Record<string, number> = {}; // To speed up the check of modules.
 | 
				
			||||||
@ -97,40 +102,7 @@ export class CoreCourseListModTypePage implements OnInit {
 | 
				
			|||||||
            // Get all the modules in the course.
 | 
					            // Get all the modules in the course.
 | 
				
			||||||
            let sections = await CoreCourse.getSections(this.courseId, false, true);
 | 
					            let sections = await CoreCourse.getSections(this.courseId, false, true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            sections = sections.filter((section) => {
 | 
					            sections = this.filterSectionsAndContents(sections);
 | 
				
			||||||
                if (!section.modules.length || section.hiddenbynumsections) {
 | 
					 | 
				
			||||||
                    return false;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                section.modules = section.modules.filter((mod) => {
 | 
					 | 
				
			||||||
                    if (!CoreCourseHelper.canUserViewModule(mod, section) ||
 | 
					 | 
				
			||||||
                        !CoreCourse.moduleHasView(mod) ||
 | 
					 | 
				
			||||||
                        mod.visibleoncoursepage === 0) {
 | 
					 | 
				
			||||||
                        // Ignore this module.
 | 
					 | 
				
			||||||
                        return false;
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    if (this.modName === 'resources') {
 | 
					 | 
				
			||||||
                        // Check that the module is a resource.
 | 
					 | 
				
			||||||
                        if (this.archetypes[mod.modname] === undefined) {
 | 
					 | 
				
			||||||
                            this.archetypes[mod.modname] = CoreCourseModuleDelegate.supportsFeature<number>(
 | 
					 | 
				
			||||||
                                mod.modname,
 | 
					 | 
				
			||||||
                                CoreConstants.FEATURE_MOD_ARCHETYPE,
 | 
					 | 
				
			||||||
                                CoreConstants.MOD_ARCHETYPE_OTHER,
 | 
					 | 
				
			||||||
                            );
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        if (this.archetypes[mod.modname] === CoreConstants.MOD_ARCHETYPE_RESOURCE) {
 | 
					 | 
				
			||||||
                            return true;
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    } else if (mod.modname === this.modName) {
 | 
					 | 
				
			||||||
                        return true;
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                return section.modules.length > 0;
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            const result = await CoreCourseHelper.addHandlerDataForModules(sections, this.courseId);
 | 
					            const result = await CoreCourseHelper.addHandlerDataForModules(sections, this.courseId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -143,6 +115,56 @@ export class CoreCourseListModTypePage implements OnInit {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Given a list of sections, return only those with contents to display. Also filter the contents to only include
 | 
				
			||||||
 | 
					     * the ones that should be displayed.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param sections Sections.
 | 
				
			||||||
 | 
					     * @returns Filtered sections.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    protected filterSectionsAndContents(sections: CoreCourseWSSection[]): CoreCourseWSSection[] {
 | 
				
			||||||
 | 
					        return sections.filter((section) => {
 | 
				
			||||||
 | 
					            if (!section.contents.length || section.hiddenbynumsections) {
 | 
				
			||||||
 | 
					                return false;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            section.contents = section.contents.filter((modOrSubsection) => {
 | 
				
			||||||
 | 
					                if (!sectionContentIsModule(modOrSubsection)) {
 | 
				
			||||||
 | 
					                    const formattedSections = this.filterSectionsAndContents([modOrSubsection]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    return !!formattedSections.length;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (!CoreCourseHelper.canUserViewModule(modOrSubsection, section) ||
 | 
				
			||||||
 | 
					                    !CoreCourse.moduleHasView(modOrSubsection) ||
 | 
				
			||||||
 | 
					                    modOrSubsection.visibleoncoursepage === 0) {
 | 
				
			||||||
 | 
					                    // Ignore this module.
 | 
				
			||||||
 | 
					                    return false;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (this.modName === 'resources') {
 | 
				
			||||||
 | 
					                    // Check that the module is a resource.
 | 
				
			||||||
 | 
					                    if (this.archetypes[modOrSubsection.modname] === undefined) {
 | 
				
			||||||
 | 
					                        this.archetypes[modOrSubsection.modname] = CoreCourseModuleDelegate.supportsFeature<number>(
 | 
				
			||||||
 | 
					                            modOrSubsection.modname,
 | 
				
			||||||
 | 
					                            CoreConstants.FEATURE_MOD_ARCHETYPE,
 | 
				
			||||||
 | 
					                            CoreConstants.MOD_ARCHETYPE_OTHER,
 | 
				
			||||||
 | 
					                        );
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    if (this.archetypes[modOrSubsection.modname] === CoreConstants.MOD_ARCHETYPE_RESOURCE) {
 | 
				
			||||||
 | 
					                        return true;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                } else if (modOrSubsection.modname === this.modName) {
 | 
				
			||||||
 | 
					                    return true;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return section.contents.length > 0;
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Show more activities.
 | 
					     * Show more activities.
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
@ -153,7 +175,8 @@ export class CoreCourseListModTypePage implements OnInit {
 | 
				
			|||||||
        while (this.lastShownSectionIndex < this.sections.length - 1 && modulesLoaded < CoreCourseListModTypePage.PAGE_LENGTH) {
 | 
					        while (this.lastShownSectionIndex < this.sections.length - 1 && modulesLoaded < CoreCourseListModTypePage.PAGE_LENGTH) {
 | 
				
			||||||
            this.lastShownSectionIndex++;
 | 
					            this.lastShownSectionIndex++;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            modulesLoaded += this.sections[this.lastShownSectionIndex].modules.length;
 | 
					            const sectionModules = CoreCourseHelper.getSectionsModules([this.sections[this.lastShownSectionIndex]]);
 | 
				
			||||||
 | 
					            modulesLoaded += sectionModules.length;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.canLoadMore = this.lastShownSectionIndex < this.sections.length - 1;
 | 
					        this.canLoadMore = this.lastShownSectionIndex < this.sections.length - 1;
 | 
				
			||||||
 | 
				
			|||||||
@ -15,7 +15,7 @@
 | 
				
			|||||||
import { Component, OnInit } from '@angular/core';
 | 
					import { Component, OnInit } from '@angular/core';
 | 
				
			||||||
import { CoreCourseModuleSummaryResult } from '@features/course/components/module-summary/module-summary';
 | 
					import { CoreCourseModuleSummaryResult } from '@features/course/components/module-summary/module-summary';
 | 
				
			||||||
import { CoreCourse } from '@features/course/services/course';
 | 
					import { CoreCourse } from '@features/course/services/course';
 | 
				
			||||||
import { CoreCourseHelper, CoreCourseModuleData, CoreCourseSection } from '@features/course/services/course-helper';
 | 
					import { CoreCourseHelper, CoreCourseModuleData } from '@features/course/services/course-helper';
 | 
				
			||||||
import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
 | 
					import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
 | 
				
			||||||
import { CoreModals } from '@services/modals';
 | 
					import { CoreModals } from '@services/modals';
 | 
				
			||||||
import { CoreNavigator } from '@services/navigator';
 | 
					import { CoreNavigator } from '@services/navigator';
 | 
				
			||||||
@ -35,7 +35,6 @@ export class CoreCourseModulePreviewPage implements OnInit {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    title!: string;
 | 
					    title!: string;
 | 
				
			||||||
    module!: CoreCourseModuleData;
 | 
					    module!: CoreCourseModuleData;
 | 
				
			||||||
    section?: CoreCourseSection; // The section the module belongs to.
 | 
					 | 
				
			||||||
    courseId!: number;
 | 
					    courseId!: number;
 | 
				
			||||||
    loaded = false;
 | 
					    loaded = false;
 | 
				
			||||||
    unsupported = false;
 | 
					    unsupported = false;
 | 
				
			||||||
@ -52,7 +51,6 @@ export class CoreCourseModulePreviewPage implements OnInit {
 | 
				
			|||||||
        try {
 | 
					        try {
 | 
				
			||||||
            this.module = CoreNavigator.getRequiredRouteParam<CoreCourseModuleData>('module');
 | 
					            this.module = CoreNavigator.getRequiredRouteParam<CoreCourseModuleData>('module');
 | 
				
			||||||
            this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
 | 
					            this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
 | 
				
			||||||
            this.section = CoreNavigator.getRouteParam<CoreCourseSection>('section');
 | 
					 | 
				
			||||||
        } catch (error) {
 | 
					        } catch (error) {
 | 
				
			||||||
            CoreDomUtils.showErrorModal(error);
 | 
					            CoreDomUtils.showErrorModal(error);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -27,6 +27,7 @@ import {
 | 
				
			|||||||
    CoreCourseModuleCompletionTracking,
 | 
					    CoreCourseModuleCompletionTracking,
 | 
				
			||||||
    CoreCourseModuleCompletionStatus,
 | 
					    CoreCourseModuleCompletionStatus,
 | 
				
			||||||
    CoreCourseGetContentsWSModule,
 | 
					    CoreCourseGetContentsWSModule,
 | 
				
			||||||
 | 
					    sectionContentIsModule,
 | 
				
			||||||
} from './course';
 | 
					} from './course';
 | 
				
			||||||
import { CoreConstants, DownloadStatus, ContextLevel } from '@/core/constants';
 | 
					import { CoreConstants, DownloadStatus, ContextLevel } from '@/core/constants';
 | 
				
			||||||
import { CoreLogger } from '@singletons/logger';
 | 
					import { CoreLogger } from '@singletons/logger';
 | 
				
			||||||
@ -76,6 +77,7 @@ import { CoreEnrolAction, CoreEnrolDelegate } from '@features/enrol/services/enr
 | 
				
			|||||||
import { LazyRoutesModule } from '@/app/app-routing.module';
 | 
					import { LazyRoutesModule } from '@/app/app-routing.module';
 | 
				
			||||||
import { CoreModals } from '@services/modals';
 | 
					import { CoreModals } from '@services/modals';
 | 
				
			||||||
import { CoreLoadings } from '@services/loadings';
 | 
					import { CoreLoadings } from '@services/loadings';
 | 
				
			||||||
 | 
					import { ArrayElement } from '@/core/utils/types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Prefetch info of a module.
 | 
					 * Prefetch info of a module.
 | 
				
			||||||
@ -166,50 +168,56 @@ export class CoreCourseHelperProvider {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        let hasContent = false;
 | 
					        let hasContent = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const formattedSections = await Promise.all(
 | 
					        const treatSection = async (sectionToTreat: CoreCourseWSSection): Promise<CoreCourseSection> => {
 | 
				
			||||||
            sections.map<Promise<CoreCourseSection>>(async (courseSection) => {
 | 
					            const section = {
 | 
				
			||||||
                const section = {
 | 
					                ...sectionToTreat,
 | 
				
			||||||
                    ...courseSection,
 | 
					                hasContent: this.sectionHasContent(sectionToTreat),
 | 
				
			||||||
                    hasContent: this.sectionHasContent(courseSection),
 | 
					            };
 | 
				
			||||||
                };
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if (!section.hasContent) {
 | 
					            if (!section.hasContent) {
 | 
				
			||||||
                    return section;
 | 
					                return section;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            hasContent = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            section.contents = await Promise.all(section.contents.map(async (module) => {
 | 
				
			||||||
 | 
					                if (!sectionContentIsModule(module)) {
 | 
				
			||||||
 | 
					                    return await treatSection(module);
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                hasContent = true;
 | 
					                module.handlerData = await CoreCourseModuleDelegate.getModuleDataFor(
 | 
				
			||||||
 | 
					                    module.modname,
 | 
				
			||||||
 | 
					                    module,
 | 
				
			||||||
 | 
					                    courseId,
 | 
				
			||||||
 | 
					                    section.id,
 | 
				
			||||||
 | 
					                    forCoursePage,
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                await Promise.all(section.modules.map(async (module) => {
 | 
					                if (!module.completiondata && completionStatus && completionStatus[module.id] !== undefined) {
 | 
				
			||||||
                    module.handlerData = await CoreCourseModuleDelegate.getModuleDataFor(
 | 
					                    // Should not happen on > 3.6. Check if activity has completions and if it's marked.
 | 
				
			||||||
                        module.modname,
 | 
					                    const activityStatus = completionStatus[module.id];
 | 
				
			||||||
                        module,
 | 
					
 | 
				
			||||||
 | 
					                    module.completiondata = {
 | 
				
			||||||
 | 
					                        state: activityStatus.state,
 | 
				
			||||||
 | 
					                        timecompleted: activityStatus.timecompleted,
 | 
				
			||||||
 | 
					                        overrideby: activityStatus.overrideby || 0,
 | 
				
			||||||
 | 
					                        valueused: activityStatus.valueused,
 | 
				
			||||||
 | 
					                        tracking: activityStatus.tracking,
 | 
				
			||||||
                        courseId,
 | 
					                        courseId,
 | 
				
			||||||
                        section.id,
 | 
					                        cmid: module.id,
 | 
				
			||||||
                        forCoursePage,
 | 
					                    };
 | 
				
			||||||
                    );
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    if (!module.completiondata && completionStatus && completionStatus[module.id] !== undefined) {
 | 
					                // Check if the module is stealth.
 | 
				
			||||||
                        // Should not happen on > 3.6. Check if activity has completions and if it's marked.
 | 
					                module.isStealth = CoreCourseHelper.isModuleStealth(module, section);
 | 
				
			||||||
                        const activityStatus = completionStatus[module.id];
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                        module.completiondata = {
 | 
					                return module;
 | 
				
			||||||
                            state: activityStatus.state,
 | 
					            }));
 | 
				
			||||||
                            timecompleted: activityStatus.timecompleted,
 | 
					 | 
				
			||||||
                            overrideby: activityStatus.overrideby || 0,
 | 
					 | 
				
			||||||
                            valueused: activityStatus.valueused,
 | 
					 | 
				
			||||||
                            tracking: activityStatus.tracking,
 | 
					 | 
				
			||||||
                            courseId,
 | 
					 | 
				
			||||||
                            cmid: module.id,
 | 
					 | 
				
			||||||
                        };
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    // Check if the module is stealth.
 | 
					            return section;
 | 
				
			||||||
                    module.isStealth = CoreCourseHelper.isModuleStealth(module, section);
 | 
					        };
 | 
				
			||||||
                }));
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                return section;
 | 
					        const formattedSections = await Promise.all(sections.map((courseSection) => treatSection(courseSection)));
 | 
				
			||||||
            }),
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return { hasContent, sections: formattedSections };
 | 
					        return { hasContent, sections: formattedSections };
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -218,7 +226,8 @@ export class CoreCourseHelperProvider {
 | 
				
			|||||||
     * Module is stealth.
 | 
					     * Module is stealth.
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * @param module Module to check.
 | 
					     * @param module Module to check.
 | 
				
			||||||
     * @param section Section to check.
 | 
					     * @param section Section to check. If the module belongs to a subsection, you can pass either the subsection or the parent
 | 
				
			||||||
 | 
					     *               section. Subsections inherit the visibility from their parent section.
 | 
				
			||||||
     * @returns Wether the module is stealth.
 | 
					     * @returns Wether the module is stealth.
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    isModuleStealth(module: CoreCourseModuleData, section?: CoreCourseWSSection): boolean {
 | 
					    isModuleStealth(module: CoreCourseModuleData, section?: CoreCourseWSSection): boolean {
 | 
				
			||||||
@ -230,7 +239,8 @@ export class CoreCourseHelperProvider {
 | 
				
			|||||||
     * Module is visible by the user.
 | 
					     * Module is visible by the user.
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * @param module Module to check.
 | 
					     * @param module Module to check.
 | 
				
			||||||
     * @param section Section to check. Omitted if not defined.
 | 
					     * @param section Section to check. Omitted if not defined. If the module belongs to a subsection, you can pass either the
 | 
				
			||||||
 | 
					     *                subsection or the parent section. Subsections inherit the visibility from their parent section.
 | 
				
			||||||
     * @returns Wether the section is visible by the user.
 | 
					     * @returns Wether the section is visible by the user.
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    canUserViewModule(module: CoreCourseModuleData, section?: CoreCourseWSSection): boolean {
 | 
					    canUserViewModule(module: CoreCourseModuleData, section?: CoreCourseWSSection): boolean {
 | 
				
			||||||
@ -281,7 +291,7 @@ export class CoreCourseHelperProvider {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        // Get the status of this section.
 | 
					        // Get the status of this section.
 | 
				
			||||||
        const result = await CoreCourseModulePrefetchDelegate.getModulesStatus(
 | 
					        const result = await CoreCourseModulePrefetchDelegate.getModulesStatus(
 | 
				
			||||||
            section.modules,
 | 
					            section.contents,
 | 
				
			||||||
            courseId,
 | 
					            courseId,
 | 
				
			||||||
            section.id,
 | 
					            section.id,
 | 
				
			||||||
            refresh,
 | 
					            refresh,
 | 
				
			||||||
@ -517,7 +527,7 @@ export class CoreCourseHelperProvider {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        // Calculate the size of the download.
 | 
					        // Calculate the size of the download.
 | 
				
			||||||
        if (section && section.id != CoreCourseProvider.ALL_SECTIONS_ID) {
 | 
					        if (section && section.id != CoreCourseProvider.ALL_SECTIONS_ID) {
 | 
				
			||||||
            sizeSum = await CoreCourseModulePrefetchDelegate.getDownloadSize(section.modules, courseId);
 | 
					            sizeSum = await CoreCourseModulePrefetchDelegate.getDownloadSize(section.contents, courseId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // Check if the section has embedded files in the description.
 | 
					            // Check if the section has embedded files in the description.
 | 
				
			||||||
            hasEmbeddedFiles = CoreFilepool.extractDownloadableFilesFromHtml(section.summary).length > 0;
 | 
					            hasEmbeddedFiles = CoreFilepool.extractDownloadableFilesFromHtml(section.summary).length > 0;
 | 
				
			||||||
@ -527,7 +537,7 @@ export class CoreCourseHelperProvider {
 | 
				
			|||||||
                    return;
 | 
					                    return;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                const sectionSize = await CoreCourseModulePrefetchDelegate.getDownloadSize(section.modules, courseId);
 | 
					                const sectionSize = await CoreCourseModulePrefetchDelegate.getDownloadSize(section.contents, courseId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                sizeSum.total = sizeSum.total && sectionSize.total;
 | 
					                sizeSum.total = sizeSum.total && sectionSize.total;
 | 
				
			||||||
                sizeSum.size += sectionSize.size;
 | 
					                sizeSum.size += sectionSize.size;
 | 
				
			||||||
@ -624,6 +634,7 @@ export class CoreCourseHelperProvider {
 | 
				
			|||||||
            summary: '',
 | 
					            summary: '',
 | 
				
			||||||
            summaryformat: 1,
 | 
					            summaryformat: 1,
 | 
				
			||||||
            modules: [],
 | 
					            modules: [],
 | 
				
			||||||
 | 
					            contents: [],
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -1121,31 +1132,36 @@ export class CoreCourseHelperProvider {
 | 
				
			|||||||
        const totalOffline = offlineCompletions.length;
 | 
					        const totalOffline = offlineCompletions.length;
 | 
				
			||||||
        let loaded = 0;
 | 
					        let loaded = 0;
 | 
				
			||||||
        const offlineCompletionsMap = CoreUtils.arrayToObject(offlineCompletions, 'cmid');
 | 
					        const offlineCompletionsMap = CoreUtils.arrayToObject(offlineCompletions, 'cmid');
 | 
				
			||||||
        // Load the offline data in the modules.
 | 
					
 | 
				
			||||||
        for (let i = 0; i < sections.length; i++) {
 | 
					        const loadSectionOfflineCompletion = (section: CoreCourseWSSection): void => {
 | 
				
			||||||
            const section = sections[i];
 | 
					            if (!section.contents || !section.contents.length) {
 | 
				
			||||||
            if (!section.modules || !section.modules.length) {
 | 
					                return;
 | 
				
			||||||
                // Section has no modules, ignore it.
 | 
					 | 
				
			||||||
                continue;
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            for (let j = 0; j < section.modules.length; j++) {
 | 
					            for (let j = 0; j < section.contents.length && loaded < totalOffline; j++) {
 | 
				
			||||||
                const module = section.modules[j];
 | 
					                const modOrSubsection = section.contents[j];
 | 
				
			||||||
                const offlineCompletion = offlineCompletionsMap[module.id];
 | 
					                if (!sectionContentIsModule(modOrSubsection)) {
 | 
				
			||||||
 | 
					                    loadSectionOfflineCompletion(modOrSubsection);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if (offlineCompletion && module.completiondata !== undefined &&
 | 
					                    continue;
 | 
				
			||||||
                    offlineCompletion.timecompleted >= module.completiondata.timecompleted * 1000) {
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                const offlineCompletion = offlineCompletionsMap[modOrSubsection.id];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (offlineCompletion && modOrSubsection.completiondata !== undefined &&
 | 
				
			||||||
 | 
					                    offlineCompletion.timecompleted >= modOrSubsection.completiondata.timecompleted * 1000) {
 | 
				
			||||||
                    // The module has offline completion. Load it.
 | 
					                    // The module has offline completion. Load it.
 | 
				
			||||||
                    module.completiondata.state = offlineCompletion.completed;
 | 
					                    modOrSubsection.completiondata.state = offlineCompletion.completed;
 | 
				
			||||||
                    module.completiondata.offline = true;
 | 
					                    modOrSubsection.completiondata.offline = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    // If all completions have been loaded, stop.
 | 
					 | 
				
			||||||
                    loaded++;
 | 
					                    loaded++;
 | 
				
			||||||
                    if (loaded == totalOffline) {
 | 
					 | 
				
			||||||
                        break;
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Load the offline data in the modules.
 | 
				
			||||||
 | 
					        for (let i = 0; i < sections.length && loaded < totalOffline; i++) {
 | 
				
			||||||
 | 
					            loadSectionOfflineCompletion(sections[i]);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -1631,8 +1647,8 @@ export class CoreCourseHelperProvider {
 | 
				
			|||||||
            // Prefetch other data needed to render the course.
 | 
					            // Prefetch other data needed to render the course.
 | 
				
			||||||
            promises.push(CoreCourses.getCoursesByField('id', course.id));
 | 
					            promises.push(CoreCourses.getCoursesByField('id', course.id));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            const sectionWithModules = sections.find((section) => section.modules && section.modules.length > 0);
 | 
					            const modules = this.getSectionsModules(sections);
 | 
				
			||||||
            if (!sectionWithModules || sectionWithModules.modules[0].completion === undefined) {
 | 
					            if (!modules.length || modules[0].completion === undefined) {
 | 
				
			||||||
                promises.push(CoreCourse.getActivitiesCompletionStatus(course.id));
 | 
					                promises.push(CoreCourse.getActivitiesCompletionStatus(course.id));
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -1787,10 +1803,10 @@ export class CoreCourseHelperProvider {
 | 
				
			|||||||
     */
 | 
					     */
 | 
				
			||||||
    protected async syncModulesAndPrefetchSection(section: CoreCourseSectionWithStatus, courseId: number): Promise<void> {
 | 
					    protected async syncModulesAndPrefetchSection(section: CoreCourseSectionWithStatus, courseId: number): Promise<void> {
 | 
				
			||||||
        // Sync the modules first.
 | 
					        // Sync the modules first.
 | 
				
			||||||
        await CoreCourseModulePrefetchDelegate.syncModules(section.modules, courseId);
 | 
					        await CoreCourseModulePrefetchDelegate.syncModules(section.contents, courseId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Validate the section needs to be downloaded and calculate amount of modules that need to be downloaded.
 | 
					        // Validate the section needs to be downloaded and calculate amount of modules that need to be downloaded.
 | 
				
			||||||
        const result = await CoreCourseModulePrefetchDelegate.getModulesStatus(section.modules, courseId, section.id);
 | 
					        const result = await CoreCourseModulePrefetchDelegate.getModulesStatus(section.contents, courseId, section.id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (result.status === DownloadStatus.DOWNLOADED || result.status === DownloadStatus.NOT_DOWNLOADABLE) {
 | 
					        if (result.status === DownloadStatus.DOWNLOADED || result.status === DownloadStatus.NOT_DOWNLOADABLE) {
 | 
				
			||||||
            // Section is downloaded or not downloadable, nothing to do.
 | 
					            // Section is downloaded or not downloadable, nothing to do.
 | 
				
			||||||
@ -1844,16 +1860,12 @@ export class CoreCourseHelperProvider {
 | 
				
			|||||||
     * @returns Whether the section has content.
 | 
					     * @returns Whether the section has content.
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    sectionHasContent(section: CoreCourseWSSection): boolean {
 | 
					    sectionHasContent(section: CoreCourseWSSection): boolean {
 | 
				
			||||||
        if (!section.modules) {
 | 
					 | 
				
			||||||
            return false;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (section.hiddenbynumsections) {
 | 
					        if (section.hiddenbynumsections) {
 | 
				
			||||||
            return false;
 | 
					            return false;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return (section.availabilityinfo !== undefined && section.availabilityinfo != '') ||
 | 
					        return (section.availabilityinfo !== undefined && section.availabilityinfo != '') ||
 | 
				
			||||||
            section.summary != '' || (section.modules && section.modules.length > 0);
 | 
					            section.summary != '' || section.contents.length > 0;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
@ -1914,7 +1926,7 @@ export class CoreCourseHelperProvider {
 | 
				
			|||||||
    async deleteCourseFiles(courseId: number): Promise<void> {
 | 
					    async deleteCourseFiles(courseId: number): Promise<void> {
 | 
				
			||||||
        const siteId = CoreSites.getCurrentSiteId();
 | 
					        const siteId = CoreSites.getCurrentSiteId();
 | 
				
			||||||
        const sections = await CoreCourse.getSections(courseId);
 | 
					        const sections = await CoreCourse.getSections(courseId);
 | 
				
			||||||
        const modules = sections.map((section) => section.modules).flat();
 | 
					        const modules = this.getSectionsModules(sections);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        await Promise.all([
 | 
					        await Promise.all([
 | 
				
			||||||
            ...modules.map((module) => this.removeModuleStoredData(module, courseId)),
 | 
					            ...modules.map((module) => this.removeModuleStoredData(module, courseId)),
 | 
				
			||||||
@ -2112,6 +2124,127 @@ export class CoreCourseHelperProvider {
 | 
				
			|||||||
        return completion.state;
 | 
					        return completion.state;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Find a section by id.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param sections List of sections, with subsections included in the contents.
 | 
				
			||||||
 | 
					     * @param searchValue Value to search. If moduleId, returns the section that contains the module.
 | 
				
			||||||
 | 
					     * @returns Section object, list of parents (if any) from top to bottom.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    findSection<T extends CoreCourseWSSection>(
 | 
				
			||||||
 | 
					        sections: T[],
 | 
				
			||||||
 | 
					        searchValue: { id?: number; num?: number; moduleId?: number},
 | 
				
			||||||
 | 
					    ): {section: T | undefined; parents: T[]} {
 | 
				
			||||||
 | 
					        if (searchValue.id === undefined && searchValue.num === undefined && searchValue.moduleId === undefined) {
 | 
				
			||||||
 | 
					            return { section: undefined, parents: [] };
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let foundSection: T | undefined;
 | 
				
			||||||
 | 
					        const parents: T[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const findInSection = (section: T): T | undefined => {
 | 
				
			||||||
 | 
					            if (section.id === searchValue.id || (section.section !== undefined && section.section === searchValue.num)) {
 | 
				
			||||||
 | 
					                return section;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            let foundSection: T | undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            section.contents.some(modOrSubsection => {
 | 
				
			||||||
 | 
					                if (sectionContentIsModule(modOrSubsection)) {
 | 
				
			||||||
 | 
					                    if (searchValue.moduleId !== undefined && modOrSubsection.id === searchValue.moduleId) {
 | 
				
			||||||
 | 
					                        foundSection = section;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        return true;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    return false;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                foundSection = findInSection(modOrSubsection as T);
 | 
				
			||||||
 | 
					                if (!foundSection) {
 | 
				
			||||||
 | 
					                    return false;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                parents.push(section);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                return true;
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return foundSection;
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        sections.some(section => {
 | 
				
			||||||
 | 
					            foundSection = findInSection(section);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return !!foundSection;
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        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.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param sections Sections.
 | 
				
			||||||
 | 
					     * @returns All sections, including subsections.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    flattenSections<T extends CoreCourseWSSection>(sections: T[]): T[] {
 | 
				
			||||||
 | 
					        const subsections: T[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const getSubsections = (section: T): void => {
 | 
				
			||||||
 | 
					            section.contents.forEach((modOrSubsection) => {
 | 
				
			||||||
 | 
					                if (!sectionContentIsModule(modOrSubsection)) {
 | 
				
			||||||
 | 
					                    subsections.push(modOrSubsection as T);
 | 
				
			||||||
 | 
					                    getSubsections(modOrSubsection as T);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        sections.forEach((section) => {
 | 
				
			||||||
 | 
					            getSubsections(section);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return sections.concat(subsections);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const CoreCourseHelper = makeSingleton(CoreCourseHelperProvider);
 | 
					export const CoreCourseHelper = makeSingleton(CoreCourseHelperProvider);
 | 
				
			||||||
@ -2119,8 +2252,9 @@ export const CoreCourseHelper = makeSingleton(CoreCourseHelperProvider);
 | 
				
			|||||||
/**
 | 
					/**
 | 
				
			||||||
 * Section with calculated data.
 | 
					 * Section with calculated data.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export type CoreCourseSection = CoreCourseWSSection & {
 | 
					export type CoreCourseSection = Omit<CoreCourseWSSection, 'contents'> & {
 | 
				
			||||||
    hasContent?: boolean;
 | 
					    hasContent?: boolean;
 | 
				
			||||||
 | 
					    contents: (CoreCourseModuleData | CoreCourseSection)[];
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
@ -2216,3 +2350,11 @@ export type CoreCourseGuestAccessInfo = {
 | 
				
			|||||||
     */
 | 
					     */
 | 
				
			||||||
    passwordRequired?: boolean;
 | 
					    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.
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -62,6 +62,8 @@ import { firstValueFrom } from 'rxjs';
 | 
				
			|||||||
import { map } from 'rxjs/operators';
 | 
					import { map } from 'rxjs/operators';
 | 
				
			||||||
import { CoreSiteWSPreSets, WSObservable } from '@classes/sites/authenticated-site';
 | 
					import { CoreSiteWSPreSets, WSObservable } from '@classes/sites/authenticated-site';
 | 
				
			||||||
import { CoreLoadings } from '@services/loadings';
 | 
					import { CoreLoadings } from '@services/loadings';
 | 
				
			||||||
 | 
					import { CoreArray } from '@singletons/array';
 | 
				
			||||||
 | 
					import { CoreText } from '@singletons/text';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const ROOT_CACHE_KEY = 'mmCourse:';
 | 
					const ROOT_CACHE_KEY = 'mmCourse:';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -639,9 +641,9 @@ export class CoreCourseProvider {
 | 
				
			|||||||
            sectionId = module.section;
 | 
					            sectionId = module.section;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const site = await CoreSites.getSite(siteId);
 | 
				
			||||||
        let sections: CoreCourseGetContentsWSSection[];
 | 
					        let sections: CoreCourseGetContentsWSSection[];
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
            const site = await CoreSites.getSite(siteId);
 | 
					 | 
				
			||||||
            // We have courseId, we can use core_course_get_contents for compatibility.
 | 
					            // We have courseId, we can use core_course_get_contents for compatibility.
 | 
				
			||||||
            this.logger.debug(`Getting module ${moduleId} in course ${courseId}`);
 | 
					            this.logger.debug(`Getting module ${moduleId} in course ${courseId}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -657,7 +659,11 @@ export class CoreCourseProvider {
 | 
				
			|||||||
                preSets.emergencyCache = false;
 | 
					                preSets.emergencyCache = false;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            sections = await this.getSections(courseId, false, false, preSets, siteId);
 | 
					            sections = await firstValueFrom(this.callGetSectionsWS(site, courseId, {
 | 
				
			||||||
 | 
					                excludeModules: false,
 | 
				
			||||||
 | 
					                excludeContents: false,
 | 
				
			||||||
 | 
					                preSets,
 | 
				
			||||||
 | 
					            }));
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let foundModule: CoreCourseGetContentsWSModule | undefined;
 | 
					        let foundModule: CoreCourseGetContentsWSModule | undefined;
 | 
				
			||||||
@ -953,40 +959,10 @@ export class CoreCourseProvider {
 | 
				
			|||||||
        courseId: number,
 | 
					        courseId: number,
 | 
				
			||||||
        options: CoreCourseGetSectionsOptions = {},
 | 
					        options: CoreCourseGetSectionsOptions = {},
 | 
				
			||||||
    ): WSObservable<CoreCourseWSSection[]> {
 | 
					    ): WSObservable<CoreCourseWSSection[]> {
 | 
				
			||||||
        options.includeStealthModules = options.includeStealthModules ?? true;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return asyncObservable(async () => {
 | 
					        return asyncObservable(async () => {
 | 
				
			||||||
            const site = await CoreSites.getSite(options.siteId);
 | 
					            const site = await CoreSites.getSite(options.siteId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            const preSets: CoreSiteWSPreSets = {
 | 
					            return this.callGetSectionsWS(site, courseId, options).pipe(
 | 
				
			||||||
                ...options.preSets,
 | 
					 | 
				
			||||||
                cacheKey: this.getSectionsCacheKey(courseId),
 | 
					 | 
				
			||||||
                updateFrequency: CoreSite.FREQUENCY_RARELY,
 | 
					 | 
				
			||||||
                ...CoreSites.getReadingStrategyPreSets(options.readingStrategy),
 | 
					 | 
				
			||||||
            };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            const params: CoreCourseGetContentsParams = {
 | 
					 | 
				
			||||||
                courseid: courseId,
 | 
					 | 
				
			||||||
            };
 | 
					 | 
				
			||||||
            params.options = [
 | 
					 | 
				
			||||||
                {
 | 
					 | 
				
			||||||
                    name: 'excludemodules',
 | 
					 | 
				
			||||||
                    value: !!options.excludeModules,
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
                {
 | 
					 | 
				
			||||||
                    name: 'excludecontents',
 | 
					 | 
				
			||||||
                    value: !!options.excludeContents,
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
            ];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (this.canRequestStealthModules(site)) {
 | 
					 | 
				
			||||||
                params.options.push({
 | 
					 | 
				
			||||||
                    name: 'includestealthmodules',
 | 
					 | 
				
			||||||
                    value: !!options.includeStealthModules,
 | 
					 | 
				
			||||||
                });
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            return site.readObservable<CoreCourseGetContentsWSSection[]>('core_course_get_contents', params, preSets).pipe(
 | 
					 | 
				
			||||||
                map(sections => {
 | 
					                map(sections => {
 | 
				
			||||||
                    const siteHomeId = site.getSiteHomeId();
 | 
					                    const siteHomeId = site.getSiteHomeId();
 | 
				
			||||||
                    let showSections = true;
 | 
					                    let showSections = true;
 | 
				
			||||||
@ -1000,17 +976,92 @@ export class CoreCourseProvider {
 | 
				
			|||||||
                        sections.pop();
 | 
					                        sections.pop();
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    // Add course to all modules.
 | 
					                    // First format all the sections and their modules.
 | 
				
			||||||
                    return sections.map((section) => ({
 | 
					                    const formattedSections: CoreCourseWSSection[] = sections.map((section) => ({
 | 
				
			||||||
                        ...section,
 | 
					                        ...section,
 | 
				
			||||||
                        availabilityinfo: this.treatAvailablityInfo(section.availabilityinfo),
 | 
					                        availabilityinfo: this.treatAvailablityInfo(section.availabilityinfo),
 | 
				
			||||||
                        modules: section.modules.map((module) => this.addAdditionalModuleData(module, courseId, section.id)),
 | 
					                        modules: section.modules.map((module) => this.addAdditionalModuleData(module, courseId, section.id)),
 | 
				
			||||||
 | 
					                        contents: [],
 | 
				
			||||||
                    }));
 | 
					                    }));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    // Only return the root sections, subsections are included in section contents.
 | 
				
			||||||
 | 
					                    return this.addSectionsContents(formattedSections).filter((section) => !section.component);
 | 
				
			||||||
                }),
 | 
					                }),
 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Call the WS to get the course sections.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param site Site.
 | 
				
			||||||
 | 
					     * @param courseId The course ID.
 | 
				
			||||||
 | 
					     * @param options Options.
 | 
				
			||||||
 | 
					     * @returns Observable that returns the sections.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    protected callGetSectionsWS(
 | 
				
			||||||
 | 
					        site: CoreSite,
 | 
				
			||||||
 | 
					        courseId: number,
 | 
				
			||||||
 | 
					        options: CoreCourseGetSectionsOptions = {},
 | 
				
			||||||
 | 
					    ): WSObservable<CoreCourseGetContentsWSSection[]> {
 | 
				
			||||||
 | 
					        const preSets: CoreSiteWSPreSets = {
 | 
				
			||||||
 | 
					            ...options.preSets,
 | 
				
			||||||
 | 
					            cacheKey: this.getSectionsCacheKey(courseId),
 | 
				
			||||||
 | 
					            updateFrequency: CoreSite.FREQUENCY_RARELY,
 | 
				
			||||||
 | 
					            ...CoreSites.getReadingStrategyPreSets(options.readingStrategy),
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const params: CoreCourseGetContentsParams = {
 | 
				
			||||||
 | 
					            courseid: courseId,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        params.options = [
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                name: 'excludemodules',
 | 
				
			||||||
 | 
					                value: !!options.excludeModules,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                name: 'excludecontents',
 | 
				
			||||||
 | 
					                value: !!options.excludeContents,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (this.canRequestStealthModules(site)) {
 | 
				
			||||||
 | 
					            params.options.push({
 | 
				
			||||||
 | 
					                name: 'includestealthmodules',
 | 
				
			||||||
 | 
					                value: !!(options.includeStealthModules ?? true),
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return site.readObservable<CoreCourseGetContentsWSSection[]>('core_course_get_contents', params, preSets);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Calculate and add the section contents. Section contents include modules and subsections.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param sections Sections to calculate.
 | 
				
			||||||
 | 
					     * @returns Sections with contents.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    protected addSectionsContents(sections: CoreCourseWSSection[]): CoreCourseWSSection[] {
 | 
				
			||||||
 | 
					        const subsections = sections.filter((section) => !!section.component);
 | 
				
			||||||
 | 
					        const subsectionsComponents = CoreArray.unique(subsections.map(section => (section.component ?? '').replace('mod_', '')));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        sections.forEach(section => {
 | 
				
			||||||
 | 
					            // eslint-disable-next-line deprecation/deprecation
 | 
				
			||||||
 | 
					            section.contents = section.modules.map(module => {
 | 
				
			||||||
 | 
					                if (!subsectionsComponents.includes(module.modname)) {
 | 
				
			||||||
 | 
					                    return module;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // Replace the module with the subsection. If subsection not found, the module will be removed from the list.
 | 
				
			||||||
 | 
					                const customData = CoreText.parseJSON<{ sectionid?: string | number }>(module.customdata ?? '{}', {});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                return subsections.find(subsection => subsection.id === Number(customData.sectionid));
 | 
				
			||||||
 | 
					            }).filter((content): content is (CoreCourseWSSection | CoreCourseModuleData) => content !== undefined);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return sections;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Get cache key for section WS call.
 | 
					     * Get cache key for section WS call.
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
@ -1026,13 +1077,10 @@ export class CoreCourseProvider {
 | 
				
			|||||||
     *
 | 
					     *
 | 
				
			||||||
     * @param sections Sections.
 | 
					     * @param sections Sections.
 | 
				
			||||||
     * @returns Modules.
 | 
					     * @returns Modules.
 | 
				
			||||||
 | 
					     * @deprecated since 4.5. Use CoreCourseHelper.getSectionsModules instead.
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    getSectionsModules(sections: CoreCourseWSSection[]): CoreCourseModuleData[] {
 | 
					    getSectionsModules(sections: CoreCourseWSSection[]): CoreCourseModuleData[] {
 | 
				
			||||||
        if (!sections || !sections.length) {
 | 
					        return CoreCourseHelper.getSectionsModules(sections);
 | 
				
			||||||
            return [];
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return sections.reduce((previous: CoreCourseModuleData[], section) => previous.concat(section.modules || []), []);
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
@ -1591,6 +1639,18 @@ export class CoreCourseProvider {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export const CoreCourse = makeSingleton(CoreCourseProvider);
 | 
					export const CoreCourse = makeSingleton(CoreCourseProvider);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Type guard to detect if a section content (module or subsection) is a module.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @param content Section module or subsection.
 | 
				
			||||||
 | 
					 * @returns Whether section content is a module.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export function sectionContentIsModule<Section extends CoreCourseWSSection, Module extends CoreCourseModuleData>(
 | 
				
			||||||
 | 
					    content: Module | Section,
 | 
				
			||||||
 | 
					): content is Module {
 | 
				
			||||||
 | 
					    return 'modname' in content;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Common options used by modules when calling a WS through CoreSite.
 | 
					 * Common options used by modules when calling a WS through CoreSite.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
@ -1821,9 +1881,21 @@ export type CoreCourseGetContentsWSModule = {
 | 
				
			|||||||
 * Data returned by core_course_get_contents WS.
 | 
					 * Data returned by core_course_get_contents WS.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export type CoreCourseWSSection = Omit<CoreCourseGetContentsWSSection, 'modules'> & {
 | 
					export type CoreCourseWSSection = Omit<CoreCourseGetContentsWSSection, 'modules'> & {
 | 
				
			||||||
    modules: CoreCourseModuleData[]; // List of module.
 | 
					    contents: CoreCourseModuleOrSection[]; // List of modules and subsections.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * List of modules
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @deprecated since 4.5. Use contents instead.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    modules: CoreCourseModuleData[];
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Module or subsection.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export type CoreCourseModuleOrSection = CoreCourseModuleData | CoreCourseWSSection;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Params of core_course_get_course_module WS.
 | 
					 * Params of core_course_get_course_module WS.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
				
			|||||||
@ -26,7 +26,7 @@ import {
 | 
				
			|||||||
    CoreFilterStateValue,
 | 
					    CoreFilterStateValue,
 | 
				
			||||||
    CoreFilterAllStates,
 | 
					    CoreFilterAllStates,
 | 
				
			||||||
} from './filter';
 | 
					} from './filter';
 | 
				
			||||||
import { CoreCourse } from '@features/course/services/course';
 | 
					import { CoreCourse, sectionContentIsModule } from '@features/course/services/course';
 | 
				
			||||||
import { CoreCourses } from '@features/courses/services/courses';
 | 
					import { CoreCourses } from '@features/courses/services/courses';
 | 
				
			||||||
import { makeSingleton } from '@singletons';
 | 
					import { makeSingleton } from '@singletons';
 | 
				
			||||||
import { CoreEvents, CoreEventSiteData } from '@singletons/events';
 | 
					import { CoreEvents, CoreEventSiteData } from '@singletons/events';
 | 
				
			||||||
@ -180,16 +180,18 @@ export class CoreFilterHelperProvider {
 | 
				
			|||||||
        const contexts: CoreFiltersGetAvailableInContextWSParamContext[] = [];
 | 
					        const contexts: CoreFiltersGetAvailableInContextWSParamContext[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        sections.forEach((section) => {
 | 
					        sections.forEach((section) => {
 | 
				
			||||||
            if (section.modules) {
 | 
					            section.contents.forEach((modOrSubsection) => {
 | 
				
			||||||
                section.modules.forEach((module) => {
 | 
					                if (!sectionContentIsModule(modOrSubsection)) {
 | 
				
			||||||
                    if (CoreCourseHelper.canUserViewModule(module, section)) {
 | 
					                    return;
 | 
				
			||||||
                        contexts.push({
 | 
					                }
 | 
				
			||||||
                            contextlevel: ContextLevel.MODULE,
 | 
					
 | 
				
			||||||
                            instanceid: module.id,
 | 
					                if (CoreCourseHelper.canUserViewModule(modOrSubsection, section)) {
 | 
				
			||||||
                        });
 | 
					                    contexts.push({
 | 
				
			||||||
                    }
 | 
					                        contextlevel: ContextLevel.MODULE,
 | 
				
			||||||
                });
 | 
					                        instanceid: modOrSubsection.id,
 | 
				
			||||||
            }
 | 
					                    });
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return contexts;
 | 
					        return contexts;
 | 
				
			||||||
 | 
				
			|||||||
@ -45,7 +45,9 @@
 | 
				
			|||||||
                    </ion-label>
 | 
					                    </ion-label>
 | 
				
			||||||
                </ion-item>
 | 
					                </ion-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                <core-course-module *ngFor="let module of section.modules" [module]="module" [section]="section" />
 | 
					                <ng-container *ngFor="let modOrSubsection of section.contents">
 | 
				
			||||||
 | 
					                    <core-course-module *ngIf="isModule(modOrSubsection)" [module]="modOrSubsection" [section]="section" />
 | 
				
			||||||
 | 
					                </ng-container>
 | 
				
			||||||
            </section>
 | 
					            </section>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <!-- Site home items: news, categories, courses, etc. -->
 | 
					            <!-- Site home items: news, categories, courses, etc. -->
 | 
				
			||||||
 | 
				
			|||||||
@ -16,7 +16,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core';
 | 
				
			|||||||
import { ActivatedRoute } from '@angular/router';
 | 
					import { ActivatedRoute } from '@angular/router';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { CoreSite, CoreSiteConfig } from '@classes/sites/site';
 | 
					import { CoreSite, CoreSiteConfig } from '@classes/sites/site';
 | 
				
			||||||
import { CoreCourse, CoreCourseWSSection } from '@features/course/services/course';
 | 
					import { CoreCourse, CoreCourseWSSection, sectionContentIsModule } from '@features/course/services/course';
 | 
				
			||||||
import { CoreDomUtils } from '@services/utils/dom';
 | 
					import { CoreDomUtils } from '@services/utils/dom';
 | 
				
			||||||
import { CoreSites } from '@services/sites';
 | 
					import { CoreSites } from '@services/sites';
 | 
				
			||||||
import { CoreSiteHome } from '@features/sitehome/services/sitehome';
 | 
					import { CoreSiteHome } from '@features/sitehome/services/sitehome';
 | 
				
			||||||
@ -55,6 +55,7 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy {
 | 
				
			|||||||
    currentSite!: CoreSite;
 | 
					    currentSite!: CoreSite;
 | 
				
			||||||
    searchEnabled = false;
 | 
					    searchEnabled = false;
 | 
				
			||||||
    newsForumModule?: CoreCourseModuleData;
 | 
					    newsForumModule?: CoreCourseModuleData;
 | 
				
			||||||
 | 
					    isModule = sectionContentIsModule;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    protected updateSiteObserver: CoreEventObserver;
 | 
					    protected updateSiteObserver: CoreEventObserver;
 | 
				
			||||||
    protected logView: () => void;
 | 
					    protected logView: () => void;
 | 
				
			||||||
@ -177,9 +178,12 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        promises.push(CoreCourse.invalidateCourseBlocks(this.siteHomeId));
 | 
					        promises.push(CoreCourse.invalidateCourseBlocks(this.siteHomeId));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (this.section && this.section.modules) {
 | 
					        if (this.section?.contents.length) {
 | 
				
			||||||
            // Invalidate modules prefetch data.
 | 
					            // Invalidate modules prefetch data.
 | 
				
			||||||
            promises.push(CoreCourseModulePrefetchDelegate.invalidateModules(this.section.modules, this.siteHomeId));
 | 
					            promises.push(CoreCourseModulePrefetchDelegate.invalidateModules(
 | 
				
			||||||
 | 
					                CoreCourse.getSectionsModules([this.section]),
 | 
				
			||||||
 | 
					                this.siteHomeId,
 | 
				
			||||||
 | 
					            ));
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Promise.all(promises).finally(async () => {
 | 
					        Promise.all(promises).finally(async () => {
 | 
				
			||||||
 | 
				
			|||||||
@ -99,7 +99,7 @@ export class CoreSiteHomeProvider {
 | 
				
			|||||||
                    throw Error('No sections found');
 | 
					                    throw Error('No sections found');
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                const hasContent = sections.some((section) => section.summary || (section.modules && section.modules.length));
 | 
					                const hasContent = sections.some((section) => section.summary || section.contents.length);
 | 
				
			||||||
                const hasCourseBlocks = await CoreBlockHelper.hasCourseBlocks(siteHomeId);
 | 
					                const hasCourseBlocks = await CoreBlockHelper.hasCourseBlocks(siteHomeId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if (hasContent || hasCourseBlocks) {
 | 
					                if (hasContent || hasCourseBlocks) {
 | 
				
			||||||
 | 
				
			|||||||
@ -148,3 +148,15 @@ export function safeNumber(value?: unknown): SafeNumber | undefined {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    return value;
 | 
					    return value;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Helper type to extract the type of each item of an array.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @example
 | 
				
			||||||
 | 
					 * ```
 | 
				
			||||||
 | 
					 *  type Result = ArrayElement<(A|B)[]>;
 | 
				
			||||||
 | 
					 *  //      ^? type Result = A|B;
 | 
				
			||||||
 | 
					 * ```
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export type ArrayElement<ArrayType extends readonly unknown[]> = ArrayType extends readonly (infer ElementType)[] ?
 | 
				
			||||||
 | 
					    ElementType : never;
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user