diff --git a/src/core/features/course/services/course-helper.ts b/src/core/features/course/services/course-helper.ts index 3d3bd63ae..f13ff9f8a 100644 --- a/src/core/features/course/services/course-helper.ts +++ b/src/core/features/course/services/course-helper.ts @@ -15,7 +15,13 @@ import { Injectable } from '@angular/core'; import { Params } from '@angular/router'; import { CoreSites } from '@services/sites'; -import { CoreCourse, CoreCourseSection } from './course'; +import { + CoreCourse, + CoreCourseCompletionActivityStatus, + CoreCourseModuleCompletionData, + CoreCourseModuleData, + CoreCourseSection, +} from './course'; import { CoreConstants } from '@/core/constants'; import { CoreLogger } from '@singletons/logger'; import { makeSingleton, Translate } from '@singletons'; @@ -36,6 +42,7 @@ import { CoreCourseOptionsHandlerToDisplay, CoreCourseOptionsMenuHandlerToDisplay, } from './course-options-delegate'; +import { CoreCourseModuleDelegate, CoreCourseModuleHandlerData } from './module-delegate'; /** * Prefetch info of a module. @@ -131,11 +138,11 @@ export class CoreCourseHelperProvider { * @return Whether the sections have content. */ addHandlerDataForModules( - sections: CoreCourseSection[], + sections: CoreCourseSectionFormatted[], courseId: number, - completionStatus?: any, // eslint-disable-line @typescript-eslint/no-unused-vars - courseName?: string, // eslint-disable-line @typescript-eslint/no-unused-vars - forCoursePage = false, // eslint-disable-line @typescript-eslint/no-unused-vars + completionStatus?: Record, + courseName?: string, + forCoursePage = false, ): boolean { let hasContent = false; @@ -147,32 +154,39 @@ export class CoreCourseHelperProvider { hasContent = true; - /* @todo section.modules.forEach((module) => { - module.handlerData = this.moduleDelegate.getModuleDataFor(module.modname, module, courseId, section.id, - forCoursePage); + module.handlerData = CoreCourseModuleDelegate.instance.getModuleDataFor( + module.modname, + module, + courseId, + section.id, + forCoursePage, + ); - if (module.completiondata && module.completion > 0) { + if (module.completiondata && module.completion && module.completion > 0) { module.completiondata.courseId = courseId; module.completiondata.courseName = courseName; module.completiondata.tracking = module.completion; module.completiondata.cmid = module.id; - - // Use of completionstatus is deprecated, use completiondata instead. - module.completionstatus = module.completiondata; } else if (completionStatus && typeof completionStatus[module.id] != 'undefined') { // Should not happen on > 3.6. Check if activity has completions and if it's marked. - module.completiondata = completionStatus[module.id]; - module.completiondata.courseId = courseId; - module.completiondata.courseName = courseName; + const activityStatus = completionStatus[module.id]; - // Use of completionstatus is deprecated, use completiondata instead. - module.completionstatus = module.completiondata; + module.completiondata = { + state: activityStatus.state, + timecompleted: activityStatus.timecompleted, + overrideby: activityStatus.overrideby || 0, + valueused: activityStatus.valueused, + tracking: activityStatus.tracking, + courseId, + courseName, + cmid: module.id, + }; } // Check if the module is stealth. - module.isStealth = module.visibleoncoursepage === 0 || (module.visible && !section.visible); - });*/ + module.isStealth = module.visibleoncoursepage === 0 || (!!module.visible && !section.visible); + }); }); return hasContent; @@ -295,7 +309,7 @@ export class CoreCourseHelperProvider { * @return Promise resolved when done. * @todo module type. */ - async confirmAndRemoveFiles(module: any, courseId: number, done?: () => void): Promise { + async confirmAndRemoveFiles(module: CoreCourseModuleData, courseId: number, done?: () => void): Promise { let modal: CoreIonLoadingElement | undefined; try { @@ -724,7 +738,7 @@ export class CoreCourseHelperProvider { * @return Promise resolved with the module's course ID. * @todo module type. */ - async getModuleCourseIdByInstance(id: number, module: any, siteId?: string): Promise { + async getModuleCourseIdByInstance(id: number, module: string, siteId?: string): Promise { try { const cm = await CoreCourse.instance.getModuleBasicInfoByInstance(id, module, siteId); @@ -1017,7 +1031,7 @@ export class CoreCourseHelperProvider { */ // @todo remove when done. // eslint-disable-next-line @typescript-eslint/no-unused-vars - async removeModuleStoredData(module: any, courseId: number): Promise { + async removeModuleStoredData(module: CoreCourseModuleData, courseId: number): Promise { const promises: Promise[] = []; // @todo @@ -1035,3 +1049,29 @@ export class CoreCourseHelperProvider { } export class CoreCourseHelper extends makeSingleton(CoreCourseHelperProvider) {} + +/** + * Section with calculated data. + */ +export type CoreCourseSectionFormatted = Omit & { + modules: CoreCourseModuleDataFormatted[]; +}; + +/** + * Module with calculated data. + */ +export type CoreCourseModuleDataFormatted = Omit & { + isStealth?: boolean; + handlerData?: CoreCourseModuleHandlerData; + completiondata?: CoreCourseModuleCompletionDataFormatted; +}; + +/** + * Module completion with calculated data. + */ +export type CoreCourseModuleCompletionDataFormatted = CoreCourseModuleCompletionData & { + courseId?: number; + courseName?: string; + tracking?: number; + cmid?: number; +}; diff --git a/src/core/features/course/services/course.ts b/src/core/features/course/services/course.ts index c7a442450..4824867b2 100644 --- a/src/core/features/course/services/course.ts +++ b/src/core/features/course/services/course.ts @@ -31,12 +31,13 @@ import { CoreCourseOffline } from './course-offline'; import { CoreError } from '@classes/errors/error'; import { CoreCourseAnyCourseData, + CoreCoursesMyCoursesUpdatedEventData, CoreCoursesProvider, } from '../../courses/services/courses'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreWSError } from '@classes/errors/wserror'; import { CorePushNotifications } from '@features/pushnotifications/services/pushnotifications'; -import { CoreCourseHelper } from './course-helper'; +import { CoreCourseHelper, CoreCourseModuleCompletionDataFormatted } from './course-helper'; import { CoreCourseFormatDelegate } from './format-delegate'; const ROOT_CACHE_KEY = 'mmCourse:'; @@ -110,7 +111,7 @@ export class CoreCourseProvider { * @param completion Completion status of the module. * @todo Add completion type. */ - checkModuleCompletion(courseId: number, completion: any): void { + checkModuleCompletion(courseId: number, completion: CoreCourseModuleCompletionDataFormatted): void { if (completion && completion.tracking === 2 && completion.state === 0) { this.invalidateSections(courseId).finally(() => { CoreEvents.trigger(CoreEvents.COMPLETION_MODULE_VIEWED, { courseId: courseId }); @@ -874,7 +875,7 @@ export class CoreCourseProvider { if (!response.status) { throw Error('WS core_course_view_course failed.'); } else { - CoreEvents.trigger(CoreCoursesProvider.EVENT_MY_COURSES_UPDATED, { + CoreEvents.trigger(CoreCoursesProvider.EVENT_MY_COURSES_UPDATED, { courseId: courseId, action: CoreCoursesProvider.ACTION_VIEW, }, site.getId()); @@ -1352,7 +1353,7 @@ export type CoreCourseGetCourseModuleWSResponse = { /** * Course module type. */ -export type CoreCourseModuleData = { // List of module. +export type CoreCourseModuleData = { id: number; // Activity id. course?: number; // The course id. url?: string; // Activity url. @@ -1374,12 +1375,7 @@ export type CoreCourseModuleData = { // List of module. customdata?: string; // Custom data (JSON encoded). noviewlink?: boolean; // Whether the module has no view page. completion?: number; // Type of completion tracking: 0 means none, 1 manual, 2 automatic. - completiondata?: { // Module completion data. - state: number; // Completion state value: 0 means incomplete, 1 complete, 2 complete pass, 3 complete fail. - timecompleted: number; // Timestamp for completion status. - overrideby: number; // The user id who has overriden the status. - valueused?: boolean; // Whether the completion status affects the availability of another activity. - }; + completiondata?: CoreCourseModuleCompletionData; // Module completion data. contents: CoreCourseModuleContentFile[]; contentsinfo?: { // Contents summary information. filescount: number; // Total number of files. @@ -1390,6 +1386,16 @@ export type CoreCourseModuleData = { // List of module. }; }; +/** + * Module completion data. + */ +export type CoreCourseModuleCompletionData = { + state: number; // Completion state value: 0 means incomplete, 1 complete, 2 complete pass, 3 complete fail. + timecompleted: number; // Timestamp for completion status. + overrideby: number; // The user id who has overriden the status. + valueused?: boolean; // Whether the completion status affects the availability of another activity. +}; + export type CoreCourseModuleContentFile = { type: string; // A file or a folder or external link. filename: string; // Filename. diff --git a/src/core/features/course/services/handlers/default-module.ts b/src/core/features/course/services/handlers/default-module.ts new file mode 100644 index 000000000..8966a92ea --- /dev/null +++ b/src/core/features/course/services/handlers/default-module.ts @@ -0,0 +1,111 @@ +// (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, Type } from '@angular/core'; +import { NavigationOptions } from '@ionic/angular/providers/nav-controller'; + +import { CoreSites } from '@services/sites'; +import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '../module-delegate'; +import { CoreCourse, CoreCourseModuleBasicInfo, CoreCourseModuleData } from '../course'; +import { CoreCourseAnyCourseData } from '@features/courses/services/courses'; + +/** + * Default handler used when the module doesn't have a specific implementation. + */ +@Injectable({ providedIn: 'root' }) +export class CoreCourseModuleDefaultHandler implements CoreCourseModuleHandler { + + name = 'CoreCourseModuleDefault'; + modName = 'default'; + + /** + * Whether or not the handler is enabled on a site level. + * + * @return True or promise resolved with true if enabled. + */ + async isEnabled(): Promise { + return true; + } + + /** + * Get the data required to display the module in the course contents view. + * + * @param module The module object. + * @param courseId The course ID. + * @param sectionId The section ID. + * @return Data to render the module. + */ + getData( + module: CoreCourseModuleData | CoreCourseModuleBasicInfo, + courseId: number, // eslint-disable-line @typescript-eslint/no-unused-vars + sectionId: number, // eslint-disable-line @typescript-eslint/no-unused-vars + forCoursePage: boolean, // eslint-disable-line @typescript-eslint/no-unused-vars + ): CoreCourseModuleHandlerData { + // Return the default data. + const defaultData: CoreCourseModuleHandlerData = { + icon: CoreCourse.instance.getModuleIconSrc(module.modname, 'modicon' in module ? module.modicon : undefined), + title: module.name, + class: 'core-course-default-handler core-course-module-' + module.modname + '-handler', + // eslint-disable-next-line @typescript-eslint/no-unused-vars + action: (event: Event, module: CoreCourseModuleData, courseId: number, options?: NavigationOptions): void => { + event.preventDefault(); + event.stopPropagation(); + + // @todo navCtrl.push('CoreCourseUnsupportedModulePage', { module: module, courseId: courseId }, options); + }, + }; + + if ('url' in module && module.url) { + defaultData.buttons = [{ + icon: 'open', + label: 'core.openinbrowser', + action: (e: Event): void => { + e.preventDefault(); + e.stopPropagation(); + + CoreSites.instance.getCurrentSite()!.openInBrowserWithAutoLoginIfSameSite(module.url!); + }, + }]; + } + + return defaultData; + } + + /** + * Get the component to render the module. This is needed to support singleactivity course format. + * The component returned must implement CoreCourseModuleMainComponent. + * It's recommended to return the class of the component, but you can also return an instance of the component. + * + * @param course The course object. + * @param module The module object. + * @return The component (or promise resolved with component) to use, undefined if not found. + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async getMainComponent(course: CoreCourseAnyCourseData, module: CoreCourseModuleData): Promise | undefined> { + // We can't inject CoreCourseUnsupportedModuleComponent here due to circular dependencies. + // Don't return anything, by default it will use CoreCourseUnsupportedModuleComponent. + return; + } + + /** + * Whether to display the course refresher in single activity course format. If it returns false, a refresher must be + * included in the template that calls the doRefresh method of the component. Defaults to true. + * + * @return Whether the refresher should be displayed. + */ + displayRefresherInSingleActivity(): boolean { + return true; + } + +} diff --git a/src/core/features/course/services/module-delegate.ts b/src/core/features/course/services/module-delegate.ts new file mode 100644 index 000000000..905800baa --- /dev/null +++ b/src/core/features/course/services/module-delegate.ts @@ -0,0 +1,369 @@ +// (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, Type } from '@angular/core'; +import { SafeUrl } from '@angular/platform-browser'; +import { Params } from '@angular/router'; +import { IonRefresher } from '@ionic/angular'; + +import { CoreSite } from '@classes/site'; +import { CoreCourseModuleDefaultHandler } from './handlers/default-module'; +import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate'; +import { CoreCourseAnyCourseData } from '@features/courses/services/courses'; +import { CoreCourse, CoreCourseModuleBasicInfo, CoreCourseModuleData } from './course'; +import { CoreSites } from '@services/sites'; +import { NavigationOptions } from '@ionic/angular/providers/nav-controller'; +import { makeSingleton } from '@singletons'; + +/** + * Interface that all course module handlers must implement. + */ +export interface CoreCourseModuleHandler extends CoreDelegateHandler { + /** + * Name of the module. It should match the "modname" of the module returned in core_course_get_contents. + */ + modName: string; + + /** + * List of supported features. The keys should be the name of the feature. + * This is to replicate the "plugin_supports" function of Moodle. + * If you need some dynamic checks please implement the supportsFeature function. + */ + supportedFeatures?: Record; + + /** + * Get the data required to display the module in the course contents view. + * + * @param module The module object. + * @param courseId The course ID. + * @param sectionId The section ID. + * @param forCoursePage Whether the data will be used to render the course page. + * @return Data to render the module. + */ + getData( + module: CoreCourseModuleData | CoreCourseModuleBasicInfo, + courseId: number, + sectionId: number, + forCoursePage: boolean, + ): CoreCourseModuleHandlerData; + + /** + * Get the component to render the module. This is needed to support singleactivity course format. + * The component returned must implement CoreCourseModuleMainComponent. + * It's recommended to return the class of the component, but you can also return an instance of the component. + * + * @param course The course object. + * @param module The module object. + * @return Promise resolved with component to use, undefined if not found. + */ + getMainComponent(course: CoreCourseAnyCourseData, module: CoreCourseModuleData): Promise | undefined>; + + /** + * Whether to display the course refresher in single activity course format. If it returns false, a refresher must be + * included in the template that calls the doRefresh method of the component. Defaults to true. + * + * @return Whether the refresher should be displayed. + */ + displayRefresherInSingleActivity?(): boolean; + + /** + * Get the icon src for the module. + * + * @return The icon src. + */ + getIconSrc?(): string; + + /** + * Check if this type of module supports a certain feature. + * If this function is implemented, the supportedFeatures object will be ignored. + * + * @param feature The feature to check. + * @return The result of the supports check. + */ + supportsFeature?(feature: string): unknown; +} + +/** + * Data needed to render the module in course contents. + */ +export interface CoreCourseModuleHandlerData { + /** + * The title to display in the module. + */ + title: string; + + /** + * The accessibility title to use in the module. If not provided, title will be used. + */ + a11yTitle?: string; + + /** + * The image to use as icon (path to the image). + */ + icon?: string | SafeUrl; + + /** + * The class to assign to the item. + */ + class?: string; + + /** + * The text to show in an extra badge. + */ + extraBadge?: string; + + /** + * The color of the extra badge. Default: primary. + */ + extraBadgeColor?: string; + + /** + * Whether to display a button to download/refresh the module if it's downloadable. + * If it's set to true, the app will show a download/refresh button when needed and will handle the download of the + * module using CoreCourseModulePrefetchDelegate. + */ + showDownloadButton?: boolean; + + /** + * The buttons to display in the module item. + */ + buttons?: CoreCourseModuleHandlerButton[]; + + /** + * Whether to display a spinner where the download button is displayed. The module icon, title, etc. will be displayed. + */ + spinner?: boolean; + + /** + * Whether the data is being loaded. If true, it will display a spinner in the whole module, nothing else will be shown. + */ + loading?: boolean; + + /** + * Action to perform when the module is clicked. + * + * @param event The click event. + * @param module The module object. + * @param courseId The course ID. + * @param options Options for the navigation. + * @param params Params for the new page. + */ + action?(event: Event, module: CoreCourseModuleData, courseId: number, options?: NavigationOptions, params?: Params): void; + + /** + * Updates the status of the module. + * + * @param status Module status. + */ + updateStatus?(status: string): void; + + /** + * On Destroy function in case it's needed. + */ + onDestroy?(): void; +} + +/** + * Interface that all the components to render the module in singleactivity must implement. + */ +export interface CoreCourseModuleMainComponent { + /** + * Refresh the data. + * + * @param refresher Refresher. + * @param done Function to call when done. + * @return Promise resolved when done. + */ + doRefresh(refresher?: CustomEvent, done?: () => void): Promise; +} + +/** + * A button to display in a module item. + */ +export interface CoreCourseModuleHandlerButton { + /** + * The label to add to the button. + */ + label: string; + + /** + * The name of the button icon. + */ + icon: string; + + /** + * Whether the button should be hidden. + */ + hidden?: boolean; + + /** + * The name of the button icon to use in iOS instead of "icon". + */ + iosIcon?: string; + + /** + * The name of the button icon to use in MaterialDesign instead of "icon". + */ + mdIcon?: string; + + /** + * Action to perform when the button is clicked. + * + * @param event The click event. + * @param navCtrl NavController instance. + * @param module The module object. + * @param courseId The course ID. + */ + action(event: Event, module: CoreCourseModuleData, courseId: number): void; +} + +/** + * Delegate to register module handlers. + */ +@Injectable({ providedIn: 'root' }) +export class CoreCourseModuleDelegateService extends CoreDelegate { + + protected featurePrefix = 'CoreCourseModuleDelegate_'; + protected handlerNameProperty = 'modName'; + + constructor(protected defaultHandler: CoreCourseModuleDefaultHandler) { + super('CoreCourseModuleDelegate', true); + } + + /** + * Get the component to render the module. + * + * @param course The course object. + * @param module The module object. + * @return Promise resolved with component to use, undefined if not found. + */ + async getMainComponent(course: CoreCourseAnyCourseData, module: CoreCourseModuleData): Promise | undefined> { + try { + return await this.executeFunctionOnEnabled>(module.modname, 'getMainComponent', [course, module]); + } catch (error) { + this.logger.error('Error getting main component', error); + } + } + + /** + * Get the data required to display the module in the course contents view. + * + * @param modname The name of the module type. + * @param module The module object. + * @param courseId The course ID. + * @param sectionId The section ID. + * @param forCoursePage Whether the data will be used to render the course page. + * @return Data to render the module. + */ + getModuleDataFor( + modname: string, + module: CoreCourseModuleData | CoreCourseModuleBasicInfo, + courseId: number, + sectionId: number, + forCoursePage?: boolean, + ): CoreCourseModuleHandlerData | undefined { + return this.executeFunctionOnEnabled( + modname, + 'getData', + [module, courseId, sectionId, forCoursePage], + ); + } + + /** + * Check if a certain module type is disabled in a site. + * + * @param modname The name of the module type. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with boolean: whether module is disabled. + */ + async isModuleDisabled(modname: string, siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + + return this.isModuleDisabledInSite(modname, site); + } + + /** + * Check if a certain module type is disabled in a site. + * + * @param modname The name of the module type. + * @param site Site. If not defined, use current site. + * @return Whether module is disabled. + */ + isModuleDisabledInSite(modname: string, site?: CoreSite): boolean { + const handler = this.getHandler(modname, false); + + if (handler) { + site = site || CoreSites.instance.getCurrentSite(); + + if (!site) { + return true; + } + + return this.isFeatureDisabled(handler, site); + } + + return false; + } + + /** + * Whether to display the course refresher in single activity course format. If it returns false, a refresher must be + * included in the template that calls the doRefresh method of the component. Defaults to true. + * + * @param modname The name of the module type. + * @return Whether the refresher should be displayed. + */ + displayRefresherInSingleActivity(modname: string): boolean { + return !!this.executeFunctionOnEnabled(modname, 'displayRefresherInSingleActivity'); + } + + /** + * Get the icon src for a certain type of module. + * + * @param modname The name of the module type. + * @param modicon The mod icon string. + * @return The icon src. + */ + getModuleIconSrc(modname: string, modicon?: string): string { + return this.executeFunctionOnEnabled(modname, 'getIconSrc') || + CoreCourse.instance.getModuleIconSrc(modname, modicon); + } + + /** + * Check if a certain type of module supports a certain feature. + * + * @param modname The modname. + * @param feature The feature to check. + * @param defaultValue Value to return if the module is not supported or doesn't know if it's supported. + * @return The result of the supports check. + */ + supportsFeature(modname: string, feature: string, defaultValue: T): T { + const handler = this.enabledHandlers[modname]; + let result: T | undefined; + + if (handler) { + if (handler.supportsFeature) { + // The handler specified a function to determine the feature, use it. + result = handler.supportsFeature(feature); + } else if (handler.supportedFeatures) { + // Handler has an object to determine the feature, use it. + result = handler.supportedFeatures[feature]; + } + } + + return result ?? defaultValue; + } + +} + +export class CoreCourseModuleDelegate extends makeSingleton(CoreCourseModuleDelegateService) {} diff --git a/src/core/features/courses/services/courses.ts b/src/core/features/courses/services/courses.ts index 423196968..f2126f01a 100644 --- a/src/core/features/courses/services/courses.ts +++ b/src/core/features/courses/services/courses.ts @@ -1174,7 +1174,7 @@ export class CoreCourses extends makeSingleton(CoreCoursesProvider) {} export type CoreCoursesMyCoursesUpdatedEventData = { action: string; // Action performed. courseId?: number; // Course ID affected (if any). - course?: any; // Course affected (if any). + course?: CoreCourseAnyCourseData; // Course affected (if any). state?: string; // Only for ACTION_STATE_CHANGED. The state that changed (hidden, favourite). value?: boolean; // The new value for the state changed. }; diff --git a/src/core/features/sitehome/pages/index/index.ts b/src/core/features/sitehome/pages/index/index.ts index 905c2a6f8..4a310b554 100644 --- a/src/core/features/sitehome/pages/index/index.ts +++ b/src/core/features/sitehome/pages/index/index.ts @@ -25,6 +25,7 @@ import { CoreCourses, CoreCoursesProvider } from '@features//courses/services/co import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreCourseHelper } from '@features/course/services/course-helper'; import { CoreBlockCourseBlocksComponent } from '@features/block/components/course-blocks/course-blocks'; +import { CoreCourseModuleDelegate, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate'; /** * Page that displays site home index. @@ -51,7 +52,7 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy { downloadCourseEnabled = false; downloadCoursesEnabled = false; downloadEnabledIcon = 'far-square'; - newsForumModule?: CoreCourseModuleBasicInfo; + newsForumModule?: NewsForum; protected updateSiteObserver?: CoreEventObserver; @@ -112,13 +113,13 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy { try { const forum = await CoreSiteHome.instance.getNewsForum(); this.newsForumModule = await CoreCourse.instance.getModuleBasicInfo(forum.cmid); - /* @todo this.newsForumModule.handlerData = this.moduleDelegate.getModuleDataFor( + this.newsForumModule.handlerData = CoreCourseModuleDelegate.instance.getModuleDataFor( this.newsForumModule.modname, this.newsForumModule, this.siteHomeId, this.newsForumModule.section, true, - );*/ + ); } catch { // Ignore errors. } @@ -235,3 +236,7 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy { } } + +type NewsForum = CoreCourseModuleBasicInfo & { + handlerData?: CoreCourseModuleHandlerData; +};