MOBILE-4660 course: Refactor how subsections are handled

This commit doesn't refactor course downloads yet, it will be done in another commit.
main
Dani Palou 2024-10-03 17:13:45 +02:00
parent b5b44a8a1d
commit a169d9301a
28 changed files with 652 additions and 622 deletions

View File

@ -70,44 +70,41 @@ export class AddonBlockActivityModulesComponent extends CoreBlockBaseComponent i
let modFullNames: Record<string, string> = {};
const brandedIcons: Record<string, boolean|undefined> = {};
sections.forEach((section) => {
if (!section.modules) {
const modules = CoreCourseHelper.getSectionsModules(sections, {
ignoreSection: section => !CoreCourseHelper.canUserViewSection(section),
ignoreModule: module => !CoreCourseHelper.canUserViewModule(module) || !CoreCourse.moduleHasView(module),
});
modules.forEach((mod) => {
if (archetypes[mod.modname] !== undefined) {
return;
}
section.modules.forEach((mod) => {
if (archetypes[mod.modname] !== undefined ||
!CoreCourseHelper.canUserViewModule(mod, section) ||
!CoreCourse.moduleHasView(mod)) {
// Ignore this module.
return;
// Get the archetype of the module type.
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 (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.
archetypes[mod.modname] = CoreCourseModuleDelegate.supportsFeature<number>(
mod.modname,
CoreConstants.FEATURE_MOD_ARCHETYPE,
CoreConstants.MOD_ARCHETYPE_OTHER,
);
brandedIcons[mod.modname] = mod.branded;
// 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;
}
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;
}
});
// 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.
modFullNames = CoreUtils.sortValues(modFullNames);
for (const modName in modFullNames) {

View File

@ -12,6 +12,8 @@
</ion-label>
</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>
</core-loading>

View File

@ -14,7 +14,7 @@
import { Component, OnInit } from '@angular/core';
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 { CoreSiteHome, FrontPageItemNames } from '@features/sitehome/services/sitehome';
import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate';
@ -39,6 +39,7 @@ export class AddonBlockSiteMainMenuComponent extends CoreBlockBaseComponent impl
component = 'AddonBlockSiteMainMenu';
mainMenuBlock?: CoreCourseSection;
siteHomeId = 1;
isModule = sectionContentIsModule;
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(CoreSiteHome.invalidateNewsForum(this.siteHomeId));
if (this.mainMenuBlock && this.mainMenuBlock.modules) {
if (this.mainMenuBlock?.contents.length) {
// 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);
@ -114,11 +118,11 @@ export class AddonBlockSiteMainMenuComponent extends CoreBlockBaseComponent impl
try {
const forum = await CoreSiteHome.getNewsForum(this.siteHomeId);
// Search the module that belongs to site news.
const forumIndex =
this.mainMenuBlock.modules.findIndex((mod) => mod.modname == 'forum' && mod.instance == forum.id);
const forumIndex = this.mainMenuBlock.contents.findIndex((mod) =>
sectionContentIsModule(mod) && mod.modname == 'forum' && mod.instance == forum.id);
if (forumIndex >= 0) {
this.mainMenuBlock.modules.splice(forumIndex, 1);
this.mainMenuBlock.contents.splice(forumIndex, 1);
}
} catch {
// Ignore errors.

View File

@ -19,7 +19,8 @@ import { CoreCourse } from '@features/course/services/course';
import { CoreLoadings } from '@services/loadings';
import { CoreDomUtils } from '@services/utils/dom';
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.
@ -33,6 +34,29 @@ export class AddonModSubsectionIndexLinkHandlerService extends CoreContentLinksM
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
*/
@ -51,7 +75,7 @@ export class AddonModSubsectionIndexLinkHandlerService extends CoreContentLinksM
// Get the module.
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) {
CoreDomUtils.showErrorModalDefault(error, 'Error opening link.');
} finally {

View File

@ -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);

View File

@ -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);

View File

@ -14,9 +14,7 @@
import { APP_INITIALIZER, NgModule } from '@angular/core';
import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate';
import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
import { AddonModSubsectionIndexLinkHandler } from './services/handlers/index-link';
import { AddonModSubsectionModuleHandler } from './services/handlers/module';
@NgModule({
providers: [
@ -24,7 +22,6 @@ import { AddonModSubsectionModuleHandler } from './services/handlers/module';
provide: APP_INITIALIZER,
multi: true,
useValue: () => {
CoreCourseModuleDelegate.registerHandler(AddonModSubsectionModuleHandler.instance);
CoreContentLinksDelegate.registerHandler(AddonModSubsectionIndexLinkHandler.instance);
},
},

View File

@ -240,7 +240,7 @@ export class AddonStorageManagerCoursesStoragePage implements OnInit, OnDestroy
*/
private async calculateDownloadedCourseSize(courseId: number): Promise<number> {
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 size = await CoreCourseModulePrefetchDelegate.getModuleStoredSize(module, courseId);

View File

@ -12,9 +12,9 @@
<core-dynamic-component [component]="singleSectionComponent" [data]="data">
<ion-accordion-group [multiple]="true" (ionChange)="accordionMultipleChange($event.detail)"
[value]="accordionMultipleValue">
<core-course-section *ngIf="!selectedSection.hiddenbynumsections && selectedSection.id !== stealthModulesSectionId &&
!selectedSection.component" [course]="course" [section]="selectedSection" [lastModuleViewed]="lastModuleViewed"
[viewedModules]="viewedModules" [collapsible]="false" [subSections]="subSections" />
<core-course-section *ngIf="!selectedSection.hiddenbynumsections && selectedSection.id !== stealthModulesSectionId"
[course]="course" [section]="selectedSection" [lastModuleViewed]="lastModuleViewed" [viewedModules]="viewedModules"
[collapsible]="false" />
</ion-accordion-group>
<core-empty-box *ngIf="!selectedSection.hasContent" icon="fas-table-cells-large"
[message]="'core.course.nocontentavailable' | translate" />
@ -27,12 +27,11 @@
<ion-accordion-group [multiple]="true" (ionChange)="accordionMultipleChange($event.detail)"
[value]="accordionMultipleValue">
@for (section of sections; track section.id) {
@if ($index <= lastShownSectionIndex && !section.hiddenbynumsections && section.id !== allSectionsId &&
section.id !== stealthModulesSectionId && !section.component) {
<core-course-section
[course]="course" [section]="section" [lastModuleViewed]="lastModuleViewed" [viewedModules]="viewedModules"
[collapsible]="true" [subSections]="subSections" />
}
@if ($index
<= lastShownSectionIndex && !section.hiddenbynumsections && section.id !==allSectionsId && section.id
!==stealthModulesSectionId) { <core-course-section [course]="course" [section]="section"
[lastModuleViewed]="lastModuleViewed" [viewedModules]="viewedModules" [collapsible]="true" />
}
}
</ion-accordion-group>
</core-dynamic-component>

View File

@ -31,9 +31,11 @@ import { CoreCourseAnyCourseData } from '@features/courses/services/courses';
import {
CoreCourse,
CoreCourseProvider,
sectionContentIsModule,
} from '@features/course/services/course';
import {
CoreCourseHelper,
CoreCourseModuleData,
CoreCourseSection,
} from '@features/course/services/course-helper';
import { CoreCourseFormatDelegate } from '@features/course/services/format-delegate';
@ -124,7 +126,6 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
displayCourseIndex = false;
displayBlocks = false;
hasBlocks = false;
subSections: CoreCourseSectionToDisplay[] = []; // List of course subsections.
selectedSection?: CoreCourseSectionToDisplay;
previousSection?: CoreCourseSectionToDisplay;
nextSection?: CoreCourseSectionToDisplay;
@ -226,9 +227,6 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
}
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.changeDetectorRef.markForCheck();
@ -326,35 +324,25 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
this.loaded = true;
this.sectionChanged(sections[0]);
} else if (this.initialSectionId || this.initialSectionNumber !== undefined) {
const subSection = this.subSections.find((section) => section.id === this.initialSectionId ||
(section.section !== undefined && section.section === this.initialSectionNumber));
if (subSection) {
// The section is a subsection, load the parent section.
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;
// We have an input indicating the section ID to load. Search the section.
const { section, parents } = CoreCourseHelper.findSection(this.sections, {
id: this.initialSectionId,
num: this.initialSectionNumber,
});
return true;
}
return false;
});
if (parents.length) {
// The section is a subsection, load the root section.
this.initialSectionId = parents[0].id;
this.initialSectionNumber = undefined;
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.
if (section && this.canViewSection(section)) {
const sectionToLoad = parents[0] ?? section;
if (sectionToLoad && this.canViewSection(sectionToLoad)) {
this.loaded = true;
this.sectionChanged(section);
this.sectionChanged(sectionToLoad);
}
} else if (this.initialBlockInstanceId && this.displayBlocks && this.hasBlocks) {
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;
moduleId = lastModuleSection ? lastModuleViewed?.cmId : undefined;
} else if (currentSectionData.section.modules.some(module => module.id === lastModuleViewed.cmId)) {
// Last module viewed is inside the highlighted section.
moduleId = lastModuleViewed.cmId;
} else {
const modules = CoreCourseHelper.getSectionsModules([currentSectionData.section]);
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 viewedModule Viewed module.
@ -432,16 +423,11 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
sections: CoreCourseSection[],
viewedModule: CoreCourseViewedModulesDBRecord,
): CoreCourseSection | undefined {
let lastModuleSection: CoreCourseSection | undefined;
if (viewedModule.sectionId) {
lastModuleSection = sections.find(section => section.id === viewedModule.sectionId);
}
if (!lastModuleSection) {
// No sectionId or section not found. Search the module.
lastModuleSection = sections.find(section => section.modules.some(module => module.id === viewedModule.cmId));
}
const { section, parents } = CoreCourseHelper.findSection(sections, {
id: viewedModule.sectionId,
moduleId: viewedModule.cmId,
});
const lastModuleSection: CoreCourseSection | undefined = parents[0] ?? section;
return lastModuleSection && lastModuleSection.id !== this.stealthModulesSectionId ? lastModuleSection : undefined;
}
@ -488,7 +474,6 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
componentProps: {
course: this.course,
sections: this.sections,
subSections: this.subSections,
selectedId: selectedId,
},
});
@ -496,29 +481,32 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
if (!data) {
return;
}
let section = this.sections.find((section) => section.id === data.sectionId);
if (!section) {
return;
}
this.sectionChanged(section);
if (data.subSectionId) {
section = this.subSections.find((section) => section.id === data.subSectionId);
if (!section) {
return;
const { section, parents } = CoreCourseHelper.findSection(this.sections, {
moduleId: data.moduleId,
id: data.moduleId === undefined ? data.sectionId : undefined,
});
// 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);
// Scroll to the subsection (later it may be scrolled to the module).
this.scrollInCourse(section.id, true);
}
if (!data.moduleId) {
if (!data.moduleId || !section) {
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) {
return;
}
@ -676,12 +664,14 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
// Skip sections without content, with stealth modules or collapsed.
if (!this.sections[this.lastShownSectionIndex].hasContent ||
!this.sections[this.lastShownSectionIndex].modules ||
!this.sections[this.lastShownSectionIndex].contents ||
!this.sections[this.lastShownSectionIndex].expanded) {
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);
}
@ -782,9 +772,8 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
* Save expanded sections for the course.
*/
protected async saveExpandedSections(): Promise<void> {
let expandedSections = this.sections.filter((section) => section.expanded && section.id > 0).map((section) => section.id);
expandedSections =
expandedSections.concat(this.subSections.filter((section) => section.expanded).map((section) => section.id));
const expandedSections = CoreCourseHelper.flattenSections(this.sections)
.filter((section) => section.expanded && section.id > 0).map((section) => section.id);
await this.currentSite?.setLocalSiteConfig(
`${COURSE_EXPANDED_SECTIONS_PREFIX}${this.course.id}`,
@ -802,12 +791,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
// Expand all sections if not defined.
if (expandedSections === undefined) {
this.sections.forEach((section) => {
section.expanded = true;
this.accordionMultipleValue.push(section.id.toString());
});
this.subSections.forEach((section) => {
CoreCourseHelper.flattenSections(this.sections).forEach((section) => {
section.expanded = true;
this.accordionMultipleValue.push(section.id.toString());
});
@ -817,11 +801,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
this.accordionMultipleValue = expandedSections.split(',');
this.sections.forEach((section) => {
section.expanded = this.accordionMultipleValue.includes(section.id.toString());
});
this.subSections.forEach((section) => {
CoreCourseHelper.flattenSections(this.sections).forEach((section) => {
section.expanded = this.accordionMultipleValue.includes(section.id.toString());
});
}
@ -833,20 +813,14 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
*/
accordionMultipleChange(ev: AccordionGroupChangeEventDetail): void {
const sectionIds = ev.value as string[] | undefined;
this.sections.forEach((section) => {
section.expanded = false;
});
this.subSections.forEach((section) => {
const allSections = CoreCourseHelper.flattenSections(this.sections);
allSections.forEach((section) => {
section.expanded = false;
});
sectionIds?.forEach((sectionId) => {
const sId = Number(sectionId);
let section = this.sections.find((section) => section.id === sId);
if (!section) {
section = this.subSections.find((section) => section.id === sId);
}
const section = allSections.find((section) => section.id === sId);
if (section) {
section.expanded = true;

View File

@ -56,30 +56,31 @@
</ion-item>
<div id="core-course-index-section-{{section.id}}" class="core-course-index-section-content">
<ng-container *ngIf="section.expanded">
<ng-container *ngFor="let module of section.modules">
@if (module.subSection) {
<ng-container *ngFor="let modOrSubsection of section.contents">
@if (!isModule(modOrSubsection)) {
<div class="core-course-index-subsection">
<ng-container *ngTemplateOutlet="sectionTemplate; context: {section: module.subSection}" />
<ng-container *ngTemplateOutlet="sectionTemplate; context: {section: modOrSubsection}" />
</div>
} @else {
<ion-item class="module" [class.item-dimmed]="!module.visible" [class.indented]="module.indented"
[class.item-hightlighted]="section.highlighted" (click)="selectSectionOrModule($event, section.id, module.id)" button>
<ion-icon class="completioninfo completion_none" name="" *ngIf="module.completionStatus === undefined" slot="start"
aria-hidden="true" />
<ion-icon class="completioninfo completion_incomplete" name="far-circle" *ngIf="module.completionStatus === 0"
<ion-item class="module" [class.item-dimmed]="!modOrSubsection.visible" [class.indented]="modOrSubsection.indented"
[class.item-hightlighted]="section.highlighted" (click)="selectSectionOrModule($event, section.id, modOrSubsection.id)"
button>
<ion-icon class="completioninfo completion_none" name="" *ngIf="modOrSubsection.completionStatus === undefined"
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" />
<ion-icon class="completioninfo completion_complete" name="fas-circle"
*ngIf="module.completionStatus === 1 || module.completionStatus === 2" color="success" slot="start"
[attr.aria-label]="'core.course.done' | translate" />
<ion-icon class="completioninfo completion_fail" name="fas-xmark" *ngIf="module.completionStatus === 3" color="danger"
slot="start" [attr.aria-label]="'core.course.failed' | translate" />
*ngIf="modOrSubsection.completionStatus === 1 || modOrSubsection.completionStatus === 2" color="success"
slot="start" [attr.aria-label]="'core.course.done' | translate" />
<ion-icon class="completioninfo completion_fail" name="fas-xmark" *ngIf="modOrSubsection.completionStatus === 3"
color="danger" slot="start" [attr.aria-label]="'core.course.failed' | translate" />
<ion-label>
<p class="item-heading">
<core-format-text [text]="module.name" contextLevel="module" [contextInstanceId]="module.id"
[courseId]="module.course" />
<core-format-text [text]="modOrSubsection.name" contextLevel="module" [contextInstanceId]="modOrSubsection.id"
[courseId]="modOrSubsection.course" />
</p>
</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" />
</ion-item>
}

View File

@ -18,6 +18,7 @@ import {
CoreCourse,
CoreCourseModuleCompletionStatus,
CoreCourseProvider,
sectionContentIsModule,
} from '@features/course/services/course';
import { CoreCourseHelper, CoreCourseModuleData, CoreCourseSection } from '@features/course/services/course-helper';
import { CoreCourseFormatCurrentSectionData, CoreCourseFormatDelegate } from '@features/course/services/format-delegate';
@ -43,7 +44,6 @@ import { CoreDom } from '@singletons/dom';
export class CoreCourseCourseIndexComponent implements OnInit {
@Input() sections: CoreCourseSection[] = [];
@Input() subSections: CoreCourseSection[] = [];
@Input() selectedId?: number;
@Input() course?: CoreCourseAnyCourseData;
@ -51,6 +51,7 @@ export class CoreCourseCourseIndexComponent implements OnInit {
highlighted?: string;
sectionsToRender: CourseIndexSection[] = [];
loaded = false;
isModule = sectionContentIsModule;
constructor(
protected elementRef: ElementRef,
@ -88,7 +89,7 @@ export class CoreCourseCourseIndexComponent implements OnInit {
const enableIndentation = await CoreCourse.isCourseIndentationEnabled(site, this.course.id);
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));
this.highlighted = CoreCourseFormatDelegate.getSectionHightlightedName(this.course);
@ -134,26 +135,7 @@ export class CoreCourseCourseIndexComponent implements OnInit {
* @param moduleId Selected module id, if any.
*/
selectSectionOrModule(event: Event, sectionId: number, moduleId?: number): void {
let subSectionId: number | undefined;
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 });
ModalController.dismiss(<CoreCourseIndexSectionWithModule> { event, sectionId, moduleId });
}
/**
@ -192,37 +174,26 @@ export class CoreCourseCourseIndexComponent implements OnInit {
enableIndentation: boolean,
currentSectionData?: CoreCourseFormatCurrentSectionData<CoreCourseSection>,
): CourseIndexSection {
const modules = section.modules
.filter((module) => module.modname === 'subsection' || this.renderModule(section, module))
.map((module) => {
if (module.modname === 'subsection') {
const subSectionFound = this.subSections.find((subSection) => subSection.itemid === module.instance);
const subSection = subSectionFound
? 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 contents = section.contents
.filter((modOrSubsection) =>
!sectionContentIsModule(modOrSubsection) || this.renderModule(section, modOrSubsection))
.map((modOrSubsection) => {
if (!sectionContentIsModule(modOrSubsection)) {
return this.mapSectionToRender(modOrSubsection, completionEnabled, enableIndentation);
}
const completionStatus = completionEnabled
? CoreCourseHelper.getCompletionStatus(module.completiondata)
? CoreCourseHelper.getCompletionStatus(modOrSubsection.completiondata)
: undefined;
return {
id: module.id,
name: module.name,
course: module.course,
visible: !!module.visible,
uservisible: CoreCourseHelper.canUserViewModule(module, section),
indented: enableIndentation && module.indent > 0,
id: modOrSubsection.id,
name: modOrSubsection.name,
modname: modOrSubsection.modname,
course: modOrSubsection.course,
visible: !!modOrSubsection.visible,
uservisible: CoreCourseHelper.canUserViewModule(modOrSubsection, section),
indented: enableIndentation && modOrSubsection.indent > 0,
completionStatus,
};
});
@ -235,8 +206,8 @@ export class CoreCourseCourseIndexComponent implements OnInit {
uservisible: CoreCourseHelper.canUserViewSection(section),
expanded: section.id === this.selectedId,
highlighted: currentSectionData?.section.id === section.id,
hasVisibleModules: modules.length > 0,
modules,
hasVisibleModules: contents.length > 0,
contents,
};
}
@ -251,20 +222,21 @@ type CourseIndexSection = {
availabilityinfo: boolean;
visible: boolean;
uservisible: boolean;
modules: {
id: number;
course: number;
visible: boolean;
indented: boolean;
uservisible: boolean;
completionStatus?: CoreCourseModuleCompletionStatus;
subSection?: CourseIndexSection;
}[];
contents: (CourseIndexSection | CourseIndexModule)[];
};
type CourseIndexModule = {
id: number;
modname: string;
course: number;
visible: boolean;
indented: boolean;
uservisible: boolean;
completionStatus?: CoreCourseModuleCompletionStatus;
};
export type CoreCourseIndexSectionWithModule = {
event: Event;
sectionId: number;
subSectionId?: number;
moduleId?: number;
};

View File

@ -53,16 +53,16 @@
</ion-label>
</ion-item>
<ng-container *ngFor="let module of modules">
@if (module.subSection) {
<core-course-section [course]="course" [section]="module.subSection" [lastModuleViewed]="lastModuleViewed"
<ng-container *ngFor="let modOrSubsection of section.contents">
@if (!isModule(modOrSubsection)) {
<core-course-section [course]="course" [section]="modOrSubsection" [lastModuleViewed]="lastModuleViewed"
[viewedModules]="viewedModules" [collapsible]="true" />
} @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"
[isLastViewed]="lastModuleViewed && lastModuleViewed.cmId === module.id" [class.core-course-module-not-viewed]="
!viewedModules[module.id] &&
(!module.completiondata || module.completiondata.state === completionStatusIncomplete)" />
[isLastViewed]="lastModuleViewed && lastModuleViewed.cmId === modOrSubsection.id" [class.core-course-module-not-viewed]="
!viewedModules[modOrSubsection.id] &&
(!modOrSubsection.completiondata || modOrSubsection.completiondata.state === completionStatusIncomplete)" />
}
</ng-container>
</ng-template>

View File

@ -15,12 +15,9 @@ import {
Component,
HostBinding,
Input,
OnChanges,
OnInit,
SimpleChange,
} from '@angular/core';
import {
CoreCourseModuleData,
CoreCourseSection,
} from '@features/course/services/course-helper';
import { CoreSharedModule } from '@/core/shared.module';
@ -28,7 +25,7 @@ import { CoreCourseComponentsModule } from '../components.module';
import { toBoolean } from '@/core/transforms/boolean';
import { CoreCourseAnyCourseData } from '@features/courses/services/courses';
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';
/**
@ -44,11 +41,10 @@ import { CoreCourseFormatDelegate } from '@features/course/services/format-deleg
CoreCourseComponentsModule,
],
})
export class CoreCourseSectionComponent implements OnInit, OnChanges {
export class CoreCourseSectionComponent implements OnInit {
@Input({ required: true }) course!: CoreCourseAnyCourseData; // The course to render.
@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() lastModuleViewed?: CoreCourseViewedModulesDBRecord;
@Input() viewedModules: Record<number, boolean> = {};
@ -58,9 +54,9 @@ export class CoreCourseSectionComponent implements OnInit, OnChanges {
return this.collapsible ? 'collapsible' : 'non-collapsible';
}
modules: CoreCourseModuleToDisplay[] = [];
completionStatusIncomplete = CoreCourseModuleCompletionStatus.COMPLETION_INCOMPLETE;
highlightedName?: string; // Name to highlight.
isModule = sectionContentIsModule;
/**
* @inheritdoc
@ -71,28 +67,8 @@ export class CoreCourseSectionComponent implements OnInit, OnChanges {
: 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 & {
highlighted?: boolean;
expanded?: boolean; // The aim of this property is to avoid DOM overloading.

View File

@ -42,8 +42,6 @@ export class CoreCourseModuleNavigationComponent implements OnInit, OnDestroy {
nextModule?: CoreCourseModuleData;
previousModule?: CoreCourseModuleData;
nextModuleSection?: CoreCourseWSSection;
previousModuleSection?: CoreCourseWSSection;
loaded = false;
element: HTMLElement;
@ -104,46 +102,23 @@ export class CoreCourseModuleNavigationComponent implements OnInit, OnDestroy {
const sections = await CoreCourse.getSections(this.courseId, false, true, preSets);
// Search the next module.
let currentModuleIndex = -1;
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;
const modules = await CoreCourseHelper.getSectionsModules(sections, {
ignoreSection: (section) => !this.isSectionAvailable(section),
});
if (currentSectionIndex < 0) {
// Nothing found. Return.
const currentModuleIndex = modules.findIndex((module) => module.id === this.currentModuleId);
if (currentModuleIndex < 0) {
// Current module found. Return.
return;
}
if (checkNext) {
// Find next Module.
this.nextModule = undefined;
for (let i = currentSectionIndex; i < sections.length && this.nextModule == undefined; i++) {
const section = sections[i];
if (!this.isSectionAvailable(section)) {
// 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;
}
for (let i = currentModuleIndex + 1; i < modules.length && this.nextModule == undefined; i++) {
const module = modules[i];
if (this.isModuleAvailable(module)) {
this.nextModule = module;
}
}
}
@ -151,23 +126,10 @@ export class CoreCourseModuleNavigationComponent implements OnInit, OnDestroy {
if (checkPrevious) {
// Find previous Module.
this.previousModule = undefined;
for (let i = currentSectionIndex; i >= 0 && this.previousModule == undefined; i--) {
const section = sections[i];
if (!this.isSectionAvailable(section)) {
// 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;
}
for (let i = currentModuleIndex - 1; i >= 0 && this.previousModule == undefined; i--) {
const module = modules[i];
if (this.isModuleAvailable(module)) {
this.previousModule = module;
}
}
}
@ -181,8 +143,8 @@ export class CoreCourseModuleNavigationComponent implements OnInit, OnDestroy {
* @param module Module to check.
* @returns Wether the module is available to the user or not.
*/
protected async isModuleAvailable(module: CoreCourseModuleData): Promise<boolean> {
return !CoreCourseHelper.isModuleStealth(module) && CoreCourse.instance.moduleHasView(module);
protected isModuleAvailable(module: CoreCourseModuleData): boolean {
return !CoreCourseHelper.isModuleStealth(module) && CoreCourse.moduleHasView(module);
}
/**
@ -229,10 +191,8 @@ export class CoreCourseModuleNavigationComponent implements OnInit, OnDestroy {
};
if (!CoreCourseHelper.canUserViewModule(module)) {
const section = next ? this.nextModuleSection : this.previousModuleSection;
options.params = {
module,
section,
};
CoreNavigator.navigateToSitePath('course/' + this.courseId + '/' + module.id +'/module-preview', options);
} else {

View File

@ -18,7 +18,7 @@ import { CoreCourseModuleDelegate } from '@features/course/services/module-deleg
import { CoreCourseUnsupportedModuleComponent } from '@features/course/components/unsupported-module/unsupported-module';
import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-component';
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 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.
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.module = module;

View File

@ -55,8 +55,8 @@ export class CoreCourseFormatSingleActivityHandlerService implements CoreCourseF
* @inheritdoc
*/
getCourseTitle(course: CoreCourseAnyCourseData, sections?: CoreCourseWSSection[]): string {
if (sections?.[0]?.modules?.[0]) {
return sections[0].modules[0].name;
if (sections?.[0]?.contents?.[0]) {
return sections[0].contents[0].name;
}
return course.fullname || '';
@ -73,8 +73,8 @@ export class CoreCourseFormatSingleActivityHandlerService implements CoreCourseF
* @inheritdoc
*/
displayRefresher(course: CoreCourseAnyCourseData, sections: CoreCourseWSSection[]): boolean {
if (sections?.[0]?.modules?.[0]) {
return CoreCourseModuleDelegate.displayRefresherInSingleActivity(sections[0].modules[0].modname);
if (sections?.[0]?.contents?.[0] && 'modname' in sections[0].contents[0]) {
return CoreCourseModuleDelegate.displayRefresherInSingleActivity(sections[0].contents[0].modname);
} else {
return true;
}

View File

@ -27,7 +27,6 @@ import {
import {
CoreCourseHelper,
CoreCourseModuleCompletionData,
CoreCourseModuleData,
CoreCourseSection,
} from '@features/course/services/course-helper';
import { CoreCourseFormatDelegate } from '@features/course/services/format-delegate';
@ -219,7 +218,7 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy, CoreRefreshCon
if (refresh) {
// 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);
}
@ -228,9 +227,9 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy, CoreRefreshCon
// Get the completion status.
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.
this.modulesHaveCompletion = true;
@ -335,8 +334,7 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy, CoreRefreshCon
if (this.sections) {
// If the completion value is not used, the page won't be reloaded, so update the progress bar.
const completionModules = (<CoreCourseModuleData[]> [])
.concat(...this.sections.map((section) => section.modules))
const completionModules = CoreCourseHelper.getSectionsModules(this.sections)
.map((module) => module.completion && module.completion > 0 ? 1 : module.completion)
.reduce((accumulator, currentValue) => (accumulator || 0) + (currentValue || 0), 0);

View File

@ -18,21 +18,31 @@
<ion-list class="core-course-module-list-wrapper">
<ng-container *ngFor="let section of sections; index as i">
<ion-card *ngIf="i <= lastShownSectionIndex">
<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 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 *ngIf="i <= lastShownSectionIndex">
<ng-container *ngTemplateOutlet="sectionTemplate; context: {section: section}" />
</ng-container>
</ng-container>
</ion-list>
<core-infinite-loading [enabled]="canLoadMore" (action)="showMoreActivities($event)" />
</core-loading>
</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>

View File

@ -15,7 +15,7 @@
import { Component, OnInit } from '@angular/core';
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 { CoreCourseHelper, CoreCourseSection } from '@features/course/services/course-helper';
import { CoreNavigator } from '@services/navigator';
@ -30,6 +30,10 @@ import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics';
@Component({
selector: 'page-core-course-list-mod-type',
templateUrl: 'list-mod-type.html',
styles: `core-course-module:last-child {
--activity-border: 0px;
--card-padding-bottom: 0px;
}`,
})
export class CoreCourseListModTypePage implements OnInit {
@ -41,6 +45,7 @@ export class CoreCourseListModTypePage implements OnInit {
courseId = 0;
canLoadMore = false;
lastShownSectionIndex = -1;
isModule = sectionContentIsModule;
protected modName?: string;
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.
let sections = await CoreCourse.getSections(this.courseId, false, true);
sections = sections.filter((section) => {
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;
});
sections = this.filterSectionsAndContents(sections);
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.
*
@ -153,7 +175,8 @@ export class CoreCourseListModTypePage implements OnInit {
while (this.lastShownSectionIndex < this.sections.length - 1 && modulesLoaded < CoreCourseListModTypePage.PAGE_LENGTH) {
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;

View File

@ -15,7 +15,7 @@
import { Component, OnInit } from '@angular/core';
import { CoreCourseModuleSummaryResult } from '@features/course/components/module-summary/module-summary';
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 { CoreModals } from '@services/modals';
import { CoreNavigator } from '@services/navigator';
@ -35,7 +35,6 @@ export class CoreCourseModulePreviewPage implements OnInit {
title!: string;
module!: CoreCourseModuleData;
section?: CoreCourseSection; // The section the module belongs to.
courseId!: number;
loaded = false;
unsupported = false;
@ -52,7 +51,6 @@ export class CoreCourseModulePreviewPage implements OnInit {
try {
this.module = CoreNavigator.getRequiredRouteParam<CoreCourseModuleData>('module');
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
this.section = CoreNavigator.getRouteParam<CoreCourseSection>('section');
} catch (error) {
CoreDomUtils.showErrorModal(error);

View File

@ -27,6 +27,7 @@ import {
CoreCourseModuleCompletionTracking,
CoreCourseModuleCompletionStatus,
CoreCourseGetContentsWSModule,
sectionContentIsModule,
} from './course';
import { CoreConstants, DownloadStatus, ContextLevel } from '@/core/constants';
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 { CoreModals } from '@services/modals';
import { CoreLoadings } from '@services/loadings';
import { ArrayElement } from '@/core/utils/types';
/**
* Prefetch info of a module.
@ -166,50 +168,56 @@ export class CoreCourseHelperProvider {
let hasContent = false;
const formattedSections = await Promise.all(
sections.map<Promise<CoreCourseSection>>(async (courseSection) => {
const section = {
...courseSection,
hasContent: this.sectionHasContent(courseSection),
};
const treatSection = async (sectionToTreat: CoreCourseWSSection): Promise<CoreCourseSection> => {
const section = {
...sectionToTreat,
hasContent: this.sectionHasContent(sectionToTreat),
};
if (!section.hasContent) {
return section;
if (!section.hasContent) {
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) => {
module.handlerData = await CoreCourseModuleDelegate.getModuleDataFor(
module.modname,
module,
if (!module.completiondata && completionStatus && completionStatus[module.id] !== undefined) {
// Should not happen on > 3.6. Check if activity has completions and if it's marked.
const activityStatus = completionStatus[module.id];
module.completiondata = {
state: activityStatus.state,
timecompleted: activityStatus.timecompleted,
overrideby: activityStatus.overrideby || 0,
valueused: activityStatus.valueused,
tracking: activityStatus.tracking,
courseId,
section.id,
forCoursePage,
);
cmid: module.id,
};
}
if (!module.completiondata && completionStatus && completionStatus[module.id] !== undefined) {
// Should not happen on > 3.6. Check if activity has completions and if it's marked.
const activityStatus = completionStatus[module.id];
// Check if the module is stealth.
module.isStealth = CoreCourseHelper.isModuleStealth(module, section);
module.completiondata = {
state: activityStatus.state,
timecompleted: activityStatus.timecompleted,
overrideby: activityStatus.overrideby || 0,
valueused: activityStatus.valueused,
tracking: activityStatus.tracking,
courseId,
cmid: module.id,
};
}
return module;
}));
// Check if the module is stealth.
module.isStealth = CoreCourseHelper.isModuleStealth(module, section);
}));
return section;
};
return section;
}),
);
const formattedSections = await Promise.all(sections.map((courseSection) => treatSection(courseSection)));
return { hasContent, sections: formattedSections };
}
@ -218,7 +226,8 @@ export class CoreCourseHelperProvider {
* Module is stealth.
*
* @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.
*/
isModuleStealth(module: CoreCourseModuleData, section?: CoreCourseWSSection): boolean {
@ -230,7 +239,8 @@ export class CoreCourseHelperProvider {
* Module is visible by the user.
*
* @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.
*/
canUserViewModule(module: CoreCourseModuleData, section?: CoreCourseWSSection): boolean {
@ -281,7 +291,7 @@ export class CoreCourseHelperProvider {
// Get the status of this section.
const result = await CoreCourseModulePrefetchDelegate.getModulesStatus(
section.modules,
section.contents,
courseId,
section.id,
refresh,
@ -517,7 +527,7 @@ export class CoreCourseHelperProvider {
// Calculate the size of the download.
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.
hasEmbeddedFiles = CoreFilepool.extractDownloadableFilesFromHtml(section.summary).length > 0;
@ -527,7 +537,7 @@ export class CoreCourseHelperProvider {
return;
}
const sectionSize = await CoreCourseModulePrefetchDelegate.getDownloadSize(section.modules, courseId);
const sectionSize = await CoreCourseModulePrefetchDelegate.getDownloadSize(section.contents, courseId);
sizeSum.total = sizeSum.total && sectionSize.total;
sizeSum.size += sectionSize.size;
@ -624,6 +634,7 @@ export class CoreCourseHelperProvider {
summary: '',
summaryformat: 1,
modules: [],
contents: [],
};
}
@ -1121,31 +1132,36 @@ export class CoreCourseHelperProvider {
const totalOffline = offlineCompletions.length;
let loaded = 0;
const offlineCompletionsMap = CoreUtils.arrayToObject(offlineCompletions, 'cmid');
// Load the offline data in the modules.
for (let i = 0; i < sections.length; i++) {
const section = sections[i];
if (!section.modules || !section.modules.length) {
// Section has no modules, ignore it.
continue;
const loadSectionOfflineCompletion = (section: CoreCourseWSSection): void => {
if (!section.contents || !section.contents.length) {
return;
}
for (let j = 0; j < section.modules.length; j++) {
const module = section.modules[j];
const offlineCompletion = offlineCompletionsMap[module.id];
for (let j = 0; j < section.contents.length && loaded < totalOffline; j++) {
const modOrSubsection = section.contents[j];
if (!sectionContentIsModule(modOrSubsection)) {
loadSectionOfflineCompletion(modOrSubsection);
if (offlineCompletion && module.completiondata !== undefined &&
offlineCompletion.timecompleted >= module.completiondata.timecompleted * 1000) {
continue;
}
const offlineCompletion = offlineCompletionsMap[modOrSubsection.id];
if (offlineCompletion && modOrSubsection.completiondata !== undefined &&
offlineCompletion.timecompleted >= modOrSubsection.completiondata.timecompleted * 1000) {
// The module has offline completion. Load it.
module.completiondata.state = offlineCompletion.completed;
module.completiondata.offline = true;
modOrSubsection.completiondata.state = offlineCompletion.completed;
modOrSubsection.completiondata.offline = true;
// If all completions have been loaded, stop.
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.
promises.push(CoreCourses.getCoursesByField('id', course.id));
const sectionWithModules = sections.find((section) => section.modules && section.modules.length > 0);
if (!sectionWithModules || sectionWithModules.modules[0].completion === undefined) {
const modules = this.getSectionsModules(sections);
if (!modules.length || modules[0].completion === undefined) {
promises.push(CoreCourse.getActivitiesCompletionStatus(course.id));
}
@ -1787,10 +1803,10 @@ export class CoreCourseHelperProvider {
*/
protected async syncModulesAndPrefetchSection(section: CoreCourseSectionWithStatus, courseId: number): Promise<void> {
// 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.
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) {
// Section is downloaded or not downloadable, nothing to do.
@ -1844,16 +1860,12 @@ export class CoreCourseHelperProvider {
* @returns Whether the section has content.
*/
sectionHasContent(section: CoreCourseWSSection): boolean {
if (!section.modules) {
return false;
}
if (section.hiddenbynumsections) {
return false;
}
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> {
const siteId = CoreSites.getCurrentSiteId();
const sections = await CoreCourse.getSections(courseId);
const modules = sections.map((section) => section.modules).flat();
const modules = this.getSectionsModules(sections);
await Promise.all([
...modules.map((module) => this.removeModuleStoredData(module, courseId)),
@ -2112,6 +2124,127 @@ export class CoreCourseHelperProvider {
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);
@ -2119,8 +2252,9 @@ export const CoreCourseHelper = makeSingleton(CoreCourseHelperProvider);
/**
* Section with calculated data.
*/
export type CoreCourseSection = CoreCourseWSSection & {
export type CoreCourseSection = Omit<CoreCourseWSSection, 'contents'> & {
hasContent?: boolean;
contents: (CoreCourseModuleData | CoreCourseSection)[];
};
/**
@ -2216,3 +2350,11 @@ export type CoreCourseGuestAccessInfo = {
*/
passwordRequired?: boolean;
};
/**
* Options for get sections modules.
*/
export type CoreCourseGetSectionsModulesOptions<Section, Module> = {
ignoreSection?: (section: Section) => boolean; // Function to filter sections. Return true to ignore it, false to use it.
ignoreModule?: (module: Module) => boolean; // Function to filter module. Return true to ignore it, false to use it.
};

View File

@ -62,6 +62,8 @@ import { firstValueFrom } from 'rxjs';
import { map } from 'rxjs/operators';
import { CoreSiteWSPreSets, WSObservable } from '@classes/sites/authenticated-site';
import { CoreLoadings } from '@services/loadings';
import { CoreArray } from '@singletons/array';
import { CoreText } from '@singletons/text';
const ROOT_CACHE_KEY = 'mmCourse:';
@ -639,9 +641,9 @@ export class CoreCourseProvider {
sectionId = module.section;
}
const site = await CoreSites.getSite(siteId);
let sections: CoreCourseGetContentsWSSection[];
try {
const site = await CoreSites.getSite(siteId);
// We have courseId, we can use core_course_get_contents for compatibility.
this.logger.debug(`Getting module ${moduleId} in course ${courseId}`);
@ -657,7 +659,11 @@ export class CoreCourseProvider {
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;
@ -953,40 +959,10 @@ export class CoreCourseProvider {
courseId: number,
options: CoreCourseGetSectionsOptions = {},
): WSObservable<CoreCourseWSSection[]> {
options.includeStealthModules = options.includeStealthModules ?? true;
return asyncObservable(async () => {
const site = await CoreSites.getSite(options.siteId);
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,
});
}
return site.readObservable<CoreCourseGetContentsWSSection[]>('core_course_get_contents', params, preSets).pipe(
return this.callGetSectionsWS(site, courseId, options).pipe(
map(sections => {
const siteHomeId = site.getSiteHomeId();
let showSections = true;
@ -1000,17 +976,92 @@ export class CoreCourseProvider {
sections.pop();
}
// Add course to all modules.
return sections.map((section) => ({
// First format all the sections and their modules.
const formattedSections: CoreCourseWSSection[] = sections.map((section) => ({
...section,
availabilityinfo: this.treatAvailablityInfo(section.availabilityinfo),
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.
*
@ -1026,13 +1077,10 @@ export class CoreCourseProvider {
*
* @param sections Sections.
* @returns Modules.
* @deprecated since 4.5. Use CoreCourseHelper.getSectionsModules instead.
*/
getSectionsModules(sections: CoreCourseWSSection[]): CoreCourseModuleData[] {
if (!sections || !sections.length) {
return [];
}
return sections.reduce((previous: CoreCourseModuleData[], section) => previous.concat(section.modules || []), []);
return CoreCourseHelper.getSectionsModules(sections);
}
/**
@ -1591,6 +1639,18 @@ export class 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.
*/
@ -1821,9 +1881,21 @@ export type CoreCourseGetContentsWSModule = {
* Data returned by core_course_get_contents WS.
*/
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.
*/

View File

@ -26,7 +26,7 @@ import {
CoreFilterStateValue,
CoreFilterAllStates,
} 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 { makeSingleton } from '@singletons';
import { CoreEvents, CoreEventSiteData } from '@singletons/events';
@ -180,16 +180,18 @@ export class CoreFilterHelperProvider {
const contexts: CoreFiltersGetAvailableInContextWSParamContext[] = [];
sections.forEach((section) => {
if (section.modules) {
section.modules.forEach((module) => {
if (CoreCourseHelper.canUserViewModule(module, section)) {
contexts.push({
contextlevel: ContextLevel.MODULE,
instanceid: module.id,
});
}
});
}
section.contents.forEach((modOrSubsection) => {
if (!sectionContentIsModule(modOrSubsection)) {
return;
}
if (CoreCourseHelper.canUserViewModule(modOrSubsection, section)) {
contexts.push({
contextlevel: ContextLevel.MODULE,
instanceid: modOrSubsection.id,
});
}
});
});
return contexts;

View File

@ -45,7 +45,9 @@
</ion-label>
</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>
<!-- Site home items: news, categories, courses, etc. -->

View File

@ -16,7 +16,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
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 { CoreSites } from '@services/sites';
import { CoreSiteHome } from '@features/sitehome/services/sitehome';
@ -55,6 +55,7 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy {
currentSite!: CoreSite;
searchEnabled = false;
newsForumModule?: CoreCourseModuleData;
isModule = sectionContentIsModule;
protected updateSiteObserver: CoreEventObserver;
protected logView: () => void;
@ -177,9 +178,12 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy {
promises.push(CoreCourse.invalidateCourseBlocks(this.siteHomeId));
if (this.section && this.section.modules) {
if (this.section?.contents.length) {
// 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 () => {

View File

@ -99,7 +99,7 @@ export class CoreSiteHomeProvider {
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);
if (hasContent || hasCourseBlocks) {

View File

@ -148,3 +148,15 @@ export function safeNumber(value?: unknown): SafeNumber | undefined {
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;