From c617ec2dcec391406663e8ccbba45b1345ca8ab3 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 13 Jan 2021 13:25:13 +0100 Subject: [PATCH] MOBILE-3659 course: Implement course format delegate --- .../features/course/services/course-helper.ts | 22 +- src/core/features/course/services/course.ts | 47 +-- .../course/services/format-delegate.ts | 377 ++++++++++++++++++ .../services/handlers/default-format.ts | 193 +++++++++ .../course-list-item/course-list-item.ts | 7 +- src/core/features/courses/services/courses.ts | 2 + 6 files changed, 598 insertions(+), 50 deletions(-) create mode 100644 src/core/features/course/services/format-delegate.ts create mode 100644 src/core/features/course/services/handlers/default-format.ts diff --git a/src/core/features/course/services/course-helper.ts b/src/core/features/course/services/course-helper.ts index 01dbf096a..3d3bd63ae 100644 --- a/src/core/features/course/services/course-helper.ts +++ b/src/core/features/course/services/course-helper.ts @@ -23,12 +23,9 @@ import { CoreFilepool } from '@services/filepool'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreUtils } from '@services/utils/utils'; import { + CoreCourseAnyCourseData, CoreCourseBasicData, - CoreCourseGetCoursesData, CoreCourses, - CoreCourseSearchedData, - CoreEnrolledCourseBasicData, - CoreEnrolledCourseData, } from '@features/courses/services/courses'; import { CoreEnrolledCourseDataWithExtraInfoAndOptions } from '@features/courses/services/courses-helper'; import { CoreArray } from '@singletons/array'; @@ -463,10 +460,10 @@ export class CoreCourseHelperProvider { async getCourse( courseId: number, siteId?: string, - ): Promise<{ enrolled: boolean; course: CoreEnrolledCourseData | CoreCourseSearchedData | CoreCourseGetCoursesData }> { + ): Promise<{ enrolled: boolean; course: CoreCourseAnyCourseData }> { siteId = siteId || CoreSites.instance.getCurrentSiteId(); - let course: CoreEnrolledCourseData | CoreCourseSearchedData | CoreCourseGetCoursesData; + let course: CoreCourseAnyCourseData; // Try with enrolled courses first. try { @@ -495,11 +492,12 @@ export class CoreCourseHelperProvider { * @param courseId Course ID. * @param params Other params to pass to the course page. * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when done. */ - async getAndOpenCourse(courseId: number, params?: Params, siteId?: string): Promise { + async getAndOpenCourse(courseId: number, params?: Params, siteId?: string): Promise { const modal = await CoreDomUtils.instance.showModalLoading(); - let course: CoreEnrolledCourseData | CoreCourseSearchedData | CoreCourseGetCoursesData | { id: number }; + let course: CoreCourseAnyCourseData | { id: number }; try { const data = await this.getCourse(courseId, siteId); @@ -575,7 +573,7 @@ export class CoreCourseHelperProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved when done. */ - async loadOfflineCompletion(courseId: number, sections: any[], siteId?: string): Promise { + async loadOfflineCompletion(courseId: number, sections: CoreCourseSection[], siteId?: string): Promise { const offlineCompletions = await CoreCourseOffline.instance.getCourseManualCompletions(courseId, siteId); if (!offlineCompletions || !offlineCompletions.length) { @@ -602,7 +600,7 @@ export class CoreCourseHelperProvider { offlineCompletion.timecompleted >= module.completiondata.timecompleted * 1000) { // The module has offline completion. Load it. module.completiondata.state = offlineCompletion.completed; - module.completiondata.offline = true; + // @todo module.completiondata.offline = true; // If all completions have been loaded, stop. loaded++; @@ -758,7 +756,7 @@ export class CoreCourseHelperProvider { * @return Section download ID. * @todo section type. */ - getSectionDownloadId(section: any): string { + getSectionDownloadId(section: CoreCourseSection): string { return 'Section-' + section.id; } @@ -978,7 +976,7 @@ export class CoreCourseHelperProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved when done. */ - async openCourse(course: CoreEnrolledCourseBasicData | { id: number }, params?: Params, siteId?: string): Promise { + async openCourse(course: CoreCourseAnyCourseData | { id: number }, params?: Params, siteId?: string): Promise { if (!siteId || siteId == CoreSites.instance.getCurrentSiteId()) { // Current site, we can open the course. return CoreCourse.instance.openCourse(course, params); diff --git a/src/core/features/course/services/course.ts b/src/core/features/course/services/course.ts index 98fd4b097..c7a442450 100644 --- a/src/core/features/course/services/course.ts +++ b/src/core/features/course/services/course.ts @@ -30,12 +30,14 @@ import { CoreCourseStatusDBRecord, COURSE_STATUS_TABLE } from './database/course import { CoreCourseOffline } from './course-offline'; import { CoreError } from '@classes/errors/error'; import { - CoreCourses, + CoreCourseAnyCourseData, 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 { CoreCourseFormatDelegate } from './format-delegate'; const ROOT_CACHE_KEY = 'mmCourse:'; @@ -71,9 +73,6 @@ export class CoreCourseProvider { protected logger: CoreLogger; constructor() { - // @todo - // protected courseFormatDelegate: CoreCourseFormatDelegate, - // protected sitePluginsProvider: CoreSitePluginsProvider, this.logger = CoreLogger.getInstance('CoreCourseProvider'); } @@ -981,39 +980,22 @@ export class CoreCourseProvider { * @param params Other params to pass to the course page. * @return Promise resolved when done. */ - async openCourse( - course: { id: number ; format?: string }, - params?: Params, // eslint-disable-line @typescript-eslint/no-unused-vars - ): Promise { + async openCourse(course: CoreCourseAnyCourseData | { id: number }, params?: Params): Promise { // @todo const loading = await CoreDomUtils.instance.showModalLoading(); // Wait for site plugins to be fetched. // @todo await this.sitePluginsProvider.waitFetchPlugins(); - if (typeof course.format == 'undefined') { - // This block can be replaced by a call to CourseHelper.getCourse(), but it is circular dependant. - const coursesProvider = CoreCourses.instance; - try { - course = await coursesProvider.getUserCourse(course.id, true); - } catch (error) { - // Not enrolled or an error happened. Try to use another WebService. - const available = coursesProvider.isGetCoursesByFieldAvailableInSite(); - try { - if (available) { - course = await coursesProvider.getCourseByField('id', course.id); - } else { - course = await coursesProvider.getCourse(course.id); - } - } catch (error) { - // Ignore errors. - } - } + if (!('format' in course) || typeof course.format == 'undefined') { + const result = await CoreCourseHelper.instance.getCourse(course.id); + + course = result.course; } /* @todo if (!this.sitePluginsProvider.sitePluginPromiseExists('format_' + course.format)) { // No custom format plugin. We don't need to wait for anything. - await this.courseFormatDelegate.openCourse(course, params); + await CoreCourseFormatDelegate.instance.openCourse(course, params); loading.dismiss(); return; @@ -1024,20 +1006,17 @@ export class CoreCourseProvider { /* @todo await this.sitePluginsProvider.sitePluginLoaded('format_' + course.format); // The format loaded successfully, but the handlers wont be registered until all site plugins have loaded. if (this.sitePluginsProvider.sitePluginsFinishedLoading) { - return this.courseFormatDelegate.openCourse(course, params); + return CoreCourseFormatDelegate.instance.openCourse(course, params); }*/ // Wait for plugins to be loaded. const deferred = CoreUtils.instance.promiseDefer(); const observer = CoreEvents.on(CoreEvents.SITE_PLUGINS_LOADED, () => { - observer && observer.off(); + observer?.off(); - /* @todo this.courseFormatDelegate.openCourse(course, params).then((response) => { - deferred.resolve(response); - }).catch((error) => { - deferred.reject(error); - });*/ + CoreCourseFormatDelegate.instance.openCourse( course, params) + .then(deferred.resolve).catch(deferred.reject); }); return deferred.promise; diff --git a/src/core/features/course/services/format-delegate.ts b/src/core/features/course/services/format-delegate.ts new file mode 100644 index 000000000..be1e4cbdf --- /dev/null +++ b/src/core/features/course/services/format-delegate.ts @@ -0,0 +1,377 @@ +// (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 { Params } from '@angular/router'; + +import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate'; +import { CoreCourseAnyCourseData } from '@features/courses/services/courses'; +import { makeSingleton } from '@singletons'; +import { CoreCourseSection } from './course'; +import { CoreCourseFormatDefaultHandler } from './handlers/default-format'; + +/** + * Interface that all course format handlers must implement. + */ +export interface CoreCourseFormatHandler extends CoreDelegateHandler { + /** + * Name of the format the handler supports. E.g. 'singleactivity'. + */ + format: string; + + /** + * Get the title to use in course page. If not defined, course fullname. + * This function will be called without sections first, and then call it again when the sections are retrieved. + * + * @param course The course. + * @param sections List of sections. + * @return Title. + */ + getCourseTitle?(course: CoreCourseAnyCourseData, sections?: CoreCourseSection[]): string; + + /** + * Whether it allows seeing all sections at the same time. Defaults to true. + * + * @param course The course to check. + * @return Whether it can view all sections. + */ + canViewAllSections?(course: CoreCourseAnyCourseData): boolean; + + /** + * Whether the option blocks should be displayed. Defaults to true. + * + * @param course The course to check. + * @return Whether it can display blocks. + */ + displayBlocks?(course: CoreCourseAnyCourseData): boolean; + + /** + * Whether the option to enable section/module download should be displayed. Defaults to true. + * + * @param course The course to check. + * @return Whether the option to enable section/module download should be displayed. + */ + displayEnableDownload?(course: CoreCourseAnyCourseData): boolean; + + /** + * Whether the default section selector should be displayed. Defaults to true. + * + * @param course The course to check. + * @return Whether the default section selector should be displayed. + */ + displaySectionSelector?(course: CoreCourseAnyCourseData): boolean; + + /** + * Whether the course refresher should be displayed. If it returns false, a refresher must be included in the course format, + * and the doRefresh method of CoreCourseSectionPage must be called on refresh. Defaults to true. + * + * @param course The course to check. + * @param sections List of course sections. + * @return Whether the refresher should be displayed. + */ + displayRefresher?(course: CoreCourseAnyCourseData, sections: CoreCourseSection[]): boolean; + + /** + * Given a list of sections, get the "current" section that should be displayed first. Defaults to first section. + * + * @param course The course to get the title. + * @param sections List of sections. + * @return Promise resolved with current section. + */ + getCurrentSection?(course: CoreCourseAnyCourseData, sections: CoreCourseSection[]): Promise; + + /** + * Open the page to display a course. If not defined, the page CoreCourseSectionPage will be opened. + * Implement it only if you want to create your own page to display the course. In general it's better to use the method + * getCourseFormatComponent because it will display the course handlers at the top. + * Your page should include the course handlers using CoreCoursesDelegate. + * + * @param course The course to open. It should contain a "format" attribute. + * @param params Params to pass to the course page. + * @return Promise resolved when done. + */ + openCourse?(course: CoreCourseAnyCourseData, params?: Params): Promise; + + /** + * Return the Component to use to display the course format instead of using the default one. + * Use it if you want to display a format completely different from the default one. + * If you want to customize the default format there are several methods to customize parts of it. + * It's recommended to return the class of the component, but you can also return an instance of the component. + * + * @param course The course to render. + * @return Promise resolved with component to use, undefined if not found. + */ + getCourseFormatComponent?(course: CoreCourseAnyCourseData): Promise | undefined>; + + /** + * Return the Component to use to display the course summary inside the default course format. + * It's recommended to return the class of the component, but you can also return an instance of the component. + * + * @param course The course to render. + * @return Promise resolved with component to use, undefined if not found. + */ + getCourseSummaryComponent?(course: CoreCourseAnyCourseData): Promise | undefined>; + + /** + * Return the Component to use to display the section selector inside the default course format. + * It's recommended to return the class of the component, but you can also return an instance of the component. + * + * @param course The course to render. + * @return Promise resolved with component to use, undefined if not found. + */ + getSectionSelectorComponent?(course: CoreCourseAnyCourseData): Promise | undefined>; + + /** + * Return the Component to use to display a single section. This component will only be used if the user is viewing a + * single section. If all the sections are displayed at once then it won't be used. + * It's recommended to return the class of the component, but you can also return an instance of the component. + * + * @param course The course to render. + * @return Promise resolved with component to use, undefined if not found. + */ + getSingleSectionComponent?(course: CoreCourseAnyCourseData): Promise | undefined>; + + /** + * Return the Component to use to display all sections in a course. + * It's recommended to return the class of the component, but you can also return an instance of the component. + * + * @param course The course to render. + * @return Promise resolved with component to use, undefined if not found. + */ + getAllSectionsComponent?(course: CoreCourseAnyCourseData): Promise | undefined>; + + /** + * Invalidate the data required to load the course format. + * + * @param course The course to get the title. + * @param sections List of sections. + * @return Promise resolved when the data is invalidated. + */ + invalidateData?(course: CoreCourseAnyCourseData, sections: CoreCourseSection[]): Promise; + + /** + * Whether the view should be refreshed when completion changes. If your course format doesn't display + * activity completion then you should return false. + * + * @param course The course. + * @return Whether course view should be refreshed when an activity completion changes. + */ + shouldRefreshWhenCompletionChanges?(course: CoreCourseAnyCourseData): Promise; +} + +/** + * Service to interact with course formats. + */ +@Injectable({ providedIn: 'root' }) +export class CoreCourseFormatDelegateService extends CoreDelegate { + + protected featurePrefix = 'CoreCourseFormatDelegate_'; + protected handlerNameProperty = 'format'; + + constructor(protected defaultHandler: CoreCourseFormatDefaultHandler) { + super('CoreCoursesCourseFormatDelegate', true); + } + + /** + * Whether it allows seeing all sections at the same time. Defaults to true. + * + * @param course The course to check. + * @return Whether it allows seeing all sections at the same time. + */ + canViewAllSections(course: CoreCourseAnyCourseData): boolean { + return !!this.executeFunctionOnEnabled(course.format || '', 'canViewAllSections', [course]); + } + + /** + * Whether the option blocks should be displayed. Defaults to true. + * + * @param course The course to check. + * @return Whether it can display blocks. + */ + displayBlocks(course: CoreCourseAnyCourseData): boolean { + return !!this.executeFunctionOnEnabled(course.format || '', 'displayBlocks', [course]); + } + + /** + * Whether the option to enable section/module download should be displayed. Defaults to true. + * + * @param course The course to check. + * @return Whether the option to enable section/module download should be displayed + */ + displayEnableDownload(course: CoreCourseAnyCourseData): boolean { + return !!this.executeFunctionOnEnabled(course.format || '', 'displayEnableDownload', [course]); + } + + /** + * Whether the course refresher should be displayed. If it returns false, a refresher must be included in the course format, + * and the doRefresh method of CoreCourseSectionPage must be called on refresh. Defaults to true. + * + * @param course The course to check. + * @param sections List of course sections. + * @return Whether the refresher should be displayed. + */ + displayRefresher(course: CoreCourseAnyCourseData, sections: CoreCourseSection[]): boolean { + return !!this.executeFunctionOnEnabled(course.format || '', 'displayRefresher', [course, sections]); + } + + /** + * Whether the default section selector should be displayed. Defaults to true. + * + * @param course The course to check. + * @return Whether the section selector should be displayed. + */ + displaySectionSelector(course: CoreCourseAnyCourseData): boolean { + return !!this.executeFunctionOnEnabled(course.format || '', 'displaySectionSelector', [course]); + } + + /** + * Get the component to use to display all sections in a course. + * + * @param course The course to render. + * @return Promise resolved with component to use, undefined if not found. + */ + async getAllSectionsComponent(course: CoreCourseAnyCourseData): Promise | undefined> { + try { + return await this.executeFunctionOnEnabled>(course.format || '', 'getAllSectionsComponent', [course]); + } catch (error) { + this.logger.error('Error getting all sections component', error); + } + } + + /** + * Get the component to use to display a course format. + * + * @param course The course to render. + * @return Promise resolved with component to use, undefined if not found. + */ + async getCourseFormatComponent(course: CoreCourseAnyCourseData): Promise | undefined> { + try { + return await this.executeFunctionOnEnabled>(course.format || '', 'getCourseFormatComponent', [course]); + } catch (error) { + this.logger.error('Error getting course format component', error); + } + } + + /** + * Get the component to use to display the course summary in the default course format. + * + * @param course The course to render. + * @return Promise resolved with component to use, undefined if not found. + */ + async getCourseSummaryComponent(course: CoreCourseAnyCourseData): Promise | undefined> { + try { + return await this.executeFunctionOnEnabled>(course.format || '', 'getCourseSummaryComponent', [course]); + } catch (error) { + this.logger.error('Error getting course summary component', error); + } + } + + /** + * Given a course, return the title to use in the course page. + * + * @param course The course to get the title. + * @param sections List of sections. + * @return Course title. + */ + getCourseTitle(course: CoreCourseAnyCourseData, sections?: CoreCourseSection[]): string | undefined { + return this.executeFunctionOnEnabled(course.format || '', 'getCourseTitle', [course, sections]); + } + + /** + * Given a course and a list of sections, return the current section that should be displayed first. + * + * @param course The course to get the title. + * @param sections List of sections. + * @return Promise resolved with current section. + */ + async getCurrentSection(course: CoreCourseAnyCourseData, sections: CoreCourseSection[]): Promise { + try { + const section = await this.executeFunctionOnEnabled( + course.format || '', + 'getCurrentSection', + [course, sections], + ); + + return section || sections[0]; + } catch { + // This function should never fail. Just return the first section (usually, "All sections"). + return sections[0]; + } + } + + /** + * Get the component to use to display the section selector inside the default course format. + * + * @param course The course to render. + * @return Promise resolved with component to use, undefined if not found. + */ + async getSectionSelectorComponent(course: CoreCourseAnyCourseData): Promise | undefined> { + try { + return await this.executeFunctionOnEnabled>(course.format || '', 'getSectionSelectorComponent', [course]); + } catch (error) { + this.logger.error('Error getting section selector component', error); + } + } + + /** + * Get the component to use to display a single section. This component will only be used if the user is viewing + * a single section. If all the sections are displayed at once then it won't be used. + * + * @param course The course to render. + * @return Promise resolved with component to use, undefined if not found. + */ + async getSingleSectionComponent(course: CoreCourseAnyCourseData): Promise | undefined> { + try { + return await this.executeFunctionOnEnabled>(course.format || '', 'getSingleSectionComponent', [course]); + } catch (error) { + this.logger.error('Error getting single section component', error); + } + } + + /** + * Invalidate the data required to load the course format. + * + * @param course The course to get the title. + * @param sections List of sections. + * @return Promise resolved when the data is invalidated. + */ + async invalidateData(course: CoreCourseAnyCourseData, sections: CoreCourseSection[]): Promise { + await this.executeFunctionOnEnabled(course.format || '', 'invalidateData', [course, sections]); + } + + /** + * Open a course. Should not be called directly. Call CoreCourseHelper.openCourse instead. + * + * @param course The course to open. It should contain a "format" attribute. + * @param params Params to pass to the course page. + * @return Promise resolved when done. + */ + async openCourse(course: CoreCourseAnyCourseData, params?: Params): Promise { + await this.executeFunctionOnEnabled(course.format || '', 'openCourse', [course, params]); + } + + /** + * Whether the view should be refreshed when completion changes. If your course format doesn't display + * activity completion then you should return false. + * + * @param course The course. + * @return Whether course view should be refreshed when an activity completion changes. + */ + async shouldRefreshWhenCompletionChanges(course: CoreCourseAnyCourseData): Promise { + return await this.executeFunctionOnEnabled(course.format || '', 'shouldRefreshWhenCompletionChanges', [course]); + } + +} + +export class CoreCourseFormatDelegate extends makeSingleton(CoreCourseFormatDelegateService) {} diff --git a/src/core/features/course/services/handlers/default-format.ts b/src/core/features/course/services/handlers/default-format.ts new file mode 100644 index 000000000..6bdd96bf0 --- /dev/null +++ b/src/core/features/course/services/handlers/default-format.ts @@ -0,0 +1,193 @@ +// (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 { Params } from '@angular/router'; + +import { CoreCourseAnyCourseData, CoreCourses } from '@features/courses/services/courses'; +import { CoreUtils } from '@services/utils/utils'; +import { CoreCourseSection } from '../course'; +import { CoreCourseFormatHandler } from '../format-delegate'; + +/** + * Default handler used when the course format doesn't have a specific implementation. + */ +@Injectable({ providedIn: 'root' }) +export class CoreCourseFormatDefaultHandler implements CoreCourseFormatHandler { + + name = 'CoreCourseFormatDefault'; + format = 'default'; + + /** + * Whether or not the handler is enabled on a site level. + * + * @return Promise resolved with true if enabled. + */ + async isEnabled(): Promise { + return true; + } + + /** + * Get the title to use in course page. + * + * @param course The course. + * @return Title. + */ + getCourseTitle(course: CoreCourseAnyCourseData): string { + if (course.displayname) { + return course.displayname; + } else if (course.fullname) { + return course.fullname; + } + + return ''; + } + + /** + * Whether it allows seeing all sections at the same time. Defaults to true. + * + * @param course The course to check. + * @return Whether it can view all sections. + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + canViewAllSections(course: CoreCourseAnyCourseData): boolean { + return true; + } + + /** + * Whether the option blocks should be displayed. Defaults to true. + * + * @param course The course to check. + * @return Whether it can display blocks. + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + displayBlocks(course: CoreCourseAnyCourseData): boolean { + return true; + } + + /** + * Whether the option to enable section/module download should be displayed. Defaults to true. + * + * @param course The course to check. + * @return Whether the option to enable section/module download should be displayed + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + displayEnableDownload(course: CoreCourseAnyCourseData): boolean { + return true; + } + + /** + * Whether the default section selector should be displayed. Defaults to true. + * + * @param course The course to check. + * @return Whether the default section selector should be displayed. + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + displaySectionSelector(course: CoreCourseAnyCourseData): boolean { + return true; + } + + /** + * Whether the course refresher should be displayed. If it returns false, a refresher must be included in the course format, + * and the doRefresh method of CoreCourseSectionPage must be called on refresh. Defaults to true. + * + * @param course The course to check. + * @param sections List of course sections. + * @return Whether the refresher should be displayed. + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + displayRefresher?(course: CoreCourseAnyCourseData, sections: CoreCourseSection[]): boolean { + return true; + } + + /** + * Given a list of sections, get the "current" section that should be displayed first. + * + * @param course The course to get the title. + * @param sections List of sections. + * @return Current section (or promise resolved with current section). + */ + async getCurrentSection(course: CoreCourseAnyCourseData, sections: CoreCourseSection[]): Promise { + let marker: number | undefined; + + // We need the "marker" to determine the current section. + if ('marker' in course) { + // We already have it. + marker = course.marker; + } else if (!CoreCourses.instance.isGetCoursesByFieldAvailable()) { + // Cannot get the current section, return all of them. + return sections[0]; + } else { + // Try to retrieve the marker. + const courseData = await CoreUtils.instance.ignoreErrors(CoreCourses.instance.getCourseByField('id', course.id)); + + marker = courseData?.marker; + } + + if (marker && marker > 0) { + // Find the marked section. + const section = sections.find((sect) => sect.section == marker); + + if (section) { + return section; + } + } + + // Marked section not found or we couldn't retrieve the marker. Return all sections. + return sections[0]; + } + + /** + * Invalidate the data required to load the course format. + * + * @param course The course to get the title. + * @param sections List of sections. + * @return Promise resolved when the data is invalidated. + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async invalidateData(course: CoreCourseAnyCourseData, sections: CoreCourseSection[]): Promise { + await CoreCourses.instance.invalidateCoursesByField('id', course.id); + } + + /** + * Open the page to display a course. If not defined, the page CoreCourseSectionPage will be opened. + * Implement it only if you want to create your own page to display the course. In general it's better to use the method + * getCourseFormatComponent because it will display the course handlers at the top. + * Your page should include the course handlers using CoreCoursesDelegate. + * + * @param course The course to open. It should contain a "format" attribute. + * @param params Params to pass to the course page. + * @return Promise resolved when done. + */ + async openCourse(course: CoreCourseAnyCourseData, params?: Params): Promise { + params = params || {}; + Object.assign(params, { course: course }); + + // Don't return the .push promise, we don't want to display a loading modal during the page transition. + // @todo navCtrl.push('CoreCourseSectionPage', params); + } + + /** + * Whether the view should be refreshed when completion changes. If your course format doesn't display + * activity completion then you should return false. + * + * @param course The course. + * @return Whether course view should be refreshed when an activity completion changes. + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async shouldRefreshWhenCompletionChanges(course: CoreCourseAnyCourseData): Promise { + return true; + } + +} diff --git a/src/core/features/courses/components/course-list-item/course-list-item.ts b/src/core/features/courses/components/course-list-item/course-list-item.ts index 5d6361075..6598fd2e2 100644 --- a/src/core/features/courses/components/course-list-item/course-list-item.ts +++ b/src/core/features/courses/components/course-list-item/course-list-item.ts @@ -13,6 +13,7 @@ // limitations under the License. import { Component, Input, OnInit } from '@angular/core'; +import { CoreCourseHelper } from '@features/course/services/course-helper'; import { NavController } from '@ionic/angular'; import { CoreCourses, CoreCourseSearchedData } from '../../services/courses'; import { CoreCoursesHelper, CoreCourseWithImageAndColor } from '../../services/courses-helper'; @@ -95,13 +96,11 @@ export class CoreCoursesCourseListItemComponent implements OnInit { * @param course The course to open. */ openCourse(): void { - /* if (this.isEnrolled) { + if (this.isEnrolled) { CoreCourseHelper.instance.openCourse(this.course); } else { this.navCtrl.navigateForward('/main/home/courses/preview', { queryParams: { course: this.course } }); - } */ - // @todo while opencourse function is not completed, open preview page. - this.navCtrl.navigateForward('/main/home/courses/preview', { queryParams: { course: this.course } }); + } } } diff --git a/src/core/features/courses/services/courses.ts b/src/core/features/courses/services/courses.ts index dc1414c0d..423196968 100644 --- a/src/core/features/courses/services/courses.ts +++ b/src/core/features/courses/services/courses.ts @@ -1588,3 +1588,5 @@ type CoreCourseSetFavouriteCoursesWSParams = { favourite: boolean; // Favourite status. }[]; }; + +export type CoreCourseAnyCourseData = CoreEnrolledCourseData | CoreCourseSearchedData | CoreCourseGetCoursesData;