From c14bc3856987b02799810aacf47d517ead82d2c8 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 29 Dec 2017 09:18:17 +0100 Subject: [PATCH] MOBILE-2310 course: Implement format delegate and section view --- src/app/app.module.ts | 7 +- .../course/components/components.module.ts | 40 ++ src/core/course/components/format/format.html | 65 ++++ src/core/course/components/format/format.ts | 185 +++++++++ src/core/course/course.module.ts | 31 ++ src/core/course/lang/en.json | 1 + src/core/course/pages/section/section.html | 24 ++ .../course/pages/section/section.module.ts | 35 ++ src/core/course/pages/section/section.ts | 128 ++++++ src/core/course/providers/course.ts | 2 + src/core/course/providers/format-delegate.ts | 368 ++++++++++++++++++ src/core/course/providers/helper.ts | 40 ++ .../course-progress/course-progress.ts | 6 +- 13 files changed, 929 insertions(+), 3 deletions(-) create mode 100644 src/core/course/components/components.module.ts create mode 100644 src/core/course/components/format/format.html create mode 100644 src/core/course/components/format/format.ts create mode 100644 src/core/course/course.module.ts create mode 100644 src/core/course/pages/section/section.html create mode 100644 src/core/course/pages/section/section.module.ts create mode 100644 src/core/course/pages/section/section.ts create mode 100644 src/core/course/providers/format-delegate.ts create mode 100644 src/core/course/providers/helper.ts diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 807ed7af9..12c7bfd48 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -48,6 +48,7 @@ import { CoreFilepoolProvider } from '../providers/filepool'; import { CoreUpdateManagerProvider } from '../providers/update-manager'; import { CorePluginFileDelegate } from '../providers/plugin-file-delegate'; +// Core modules. import { CoreComponentsModule } from '../components/components.module'; import { CoreEmulatorModule } from '../core/emulator/emulator.module'; import { CoreLoginModule } from '../core/login/login.module'; @@ -55,6 +56,9 @@ import { CoreMainMenuModule } from '../core/mainmenu/mainmenu.module'; import { CoreCoursesModule } from '../core/courses/courses.module'; import { CoreFileUploaderModule } from '../core/fileuploader/fileuploader.module'; import { CoreSharedFilesModule } from '../core/sharedfiles/sharedfiles.module'; +import { CoreCourseModule } from '../core/course/course.module'; + +// Addon modules. import { AddonCalendarModule } from '../addon/calendar/calendar.module'; // For translate loader. AoT requires an exported function for factories. @@ -80,13 +84,14 @@ export function createTranslateLoader(http: HttpClient) { deps: [HttpClient] } }), + CoreComponentsModule, CoreEmulatorModule, CoreLoginModule, CoreMainMenuModule, CoreCoursesModule, CoreFileUploaderModule, CoreSharedFilesModule, - CoreComponentsModule, + CoreCourseModule, AddonCalendarModule ], bootstrap: [IonicApp], diff --git a/src/core/course/components/components.module.ts b/src/core/course/components/components.module.ts new file mode 100644 index 000000000..5dd16ac0b --- /dev/null +++ b/src/core/course/components/components.module.ts @@ -0,0 +1,40 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { IonicModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreComponentsModule } from '../../../components/components.module'; +import { CoreDirectivesModule } from '../../../directives/directives.module'; +import { CoreCourseFormatComponent } from './format/format'; + +@NgModule({ + declarations: [ + CoreCourseFormatComponent + ], + imports: [ + CommonModule, + IonicModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule + ], + providers: [ + ], + exports: [ + CoreCourseFormatComponent + ] +}) +export class CoreCourseComponentsModule {} diff --git a/src/core/course/components/format/format.html b/src/core/course/components/format/format.html new file mode 100644 index 000000000..662a409a4 --- /dev/null +++ b/src/core/course/components/format/format.html @@ -0,0 +1,65 @@ + +
+ + + + + + + + + + + + + + + + + {{section.formattedName || section.name}} + + + + + + + + + + +
+ + + + + +
+ + +
+ + + + + + +
+
+ + +
+ + + + + + + + + + +
+
+ + + \ No newline at end of file diff --git a/src/core/course/components/format/format.ts b/src/core/course/components/format/format.ts new file mode 100644 index 000000000..c0fce8c58 --- /dev/null +++ b/src/core/course/components/format/format.ts @@ -0,0 +1,185 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { Component, Input, OnInit, OnChanges, ViewContainerRef, ComponentFactoryResolver, ViewChild, ChangeDetectorRef, + SimpleChange } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { CoreLoggerProvider } from '../../../../providers/logger'; +import { CoreCourseProvider } from '../../../course/providers/course'; +import { CoreCourseFormatDelegate } from '../../../course/providers/format-delegate'; + +/** + * Component to display course contents using a certain format. If the format isn't found, use default one. + * + * The inputs of this component will be shared with the course format components. Please use CoreCourseFormatDelegate + * to register your handler for course formats. + * + * Example usage: + * + * + */ +@Component({ + selector: 'core-course-format', + templateUrl: 'format.html' +}) +export class CoreCourseFormatComponent implements OnInit, OnChanges { + @Input() course: any; // The course to render. + @Input() sections: any[]; // List of course sections. + + // Get the containers where to inject dynamic components. We use a setter because they might be inside a *ngIf. + @ViewChild('courseFormat', { read: ViewContainerRef }) set courseFormat(el: ViewContainerRef) { + if (this.course) { + this.createComponent('courseFormat', this.cfDelegate.getCourseFormatComponent(this.course), el); + } else { + // The component hasn't been initialized yet. Store the container. + this.componentContainers['courseFormat'] = el; + } + }; + @ViewChild('courseSummary', { read: ViewContainerRef }) set courseSummary(el: ViewContainerRef) { + this.createComponent('courseSummary', this.cfDelegate.getCourseSummaryComponent(this.course), el); + }; + @ViewChild('sectionSelector', { read: ViewContainerRef }) set sectionSelector(el: ViewContainerRef) { + this.createComponent('sectionSelector', this.cfDelegate.getSectionSelectorComponent(this.course), el); + }; + @ViewChild('singleSection', { read: ViewContainerRef }) set singleSection(el: ViewContainerRef) { + this.createComponent('singleSection', this.cfDelegate.getSingleSectionComponent(this.course), el); + }; + @ViewChild('allSections', { read: ViewContainerRef }) set allSections(el: ViewContainerRef) { + this.createComponent('allSections', this.cfDelegate.getAllSectionsComponent(this.course), el); + }; + + // Instances and containers of all the components that the handler could define. + protected componentContainers: {[type: string]: ViewContainerRef} = {}; + componentInstances: {[type: string]: any} = {}; + + displaySectionSelector: boolean; + selectedSection: any; + allSectionsId: number = CoreCourseProvider.ALL_SECTIONS_ID; + selectOptions: any = {}; + + protected logger; + + constructor(logger: CoreLoggerProvider, private cfDelegate: CoreCourseFormatDelegate, translate: TranslateService, + private factoryResolver: ComponentFactoryResolver, private cdr: ChangeDetectorRef) { + this.logger = logger.getInstance('CoreCourseFormatComponent'); + this.selectOptions.title = translate.instant('core.course.sections'); + } + + /** + * Component being initialized. + */ + ngOnInit() { + this.displaySectionSelector = this.cfDelegate.displaySectionSelector(this.course); + + this.createComponent( + 'courseFormat', this.cfDelegate.getCourseFormatComponent(this.course), this.componentContainers['courseFormat']); + } + + /** + * Detect changes on input properties. + */ + ngOnChanges(changes: {[name: string]: SimpleChange}) { + if (!this.selectedSection && changes.sections && this.sections) { + this.sectionChanged(this.cfDelegate.getCurrentSection(this.course, this.sections)); + } + + if (!Object.keys(this.componentInstances).length) { + // We haven't created any component dynamically, stop. + return; + } + + // Apply the changes to the components and call ngOnChanges if it exists. + for (let type in this.componentInstances) { + let instance = this.componentInstances[type]; + + for (let name in changes) { + instance[name] = changes[name].currentValue; + } + + if (instance.ngOnChanges) { + instance.ngOnChanges(changes); + } + } + } + + /** + * Create a component, add it to a container and set the input data. + * + * @param {string} type The "type" of the component. + * @param {any} componentClass The class of the component to create. + * @param {ViewContainerRef} container The container to add the component to. + * @return {boolean} Whether the component was successfully created. + */ + protected createComponent(type: string, componentClass: any, container: ViewContainerRef) : boolean { + if (!componentClass || !container) { + // No component to instantiate or container doesn't exist right now. + return false; + } + + if (this.componentInstances[type] && container === this.componentContainers[type]) { + // Component already instantiated and the component hasn't been destroyed, nothing to do. + return true; + } + + try { + // Create the component and add it to the container. + const factory = this.factoryResolver.resolveComponentFactory(componentClass), + componentRef = container.createComponent(factory); + + this.componentContainers[type] = container; + this.componentInstances[type] = componentRef.instance; + this.cdr.detectChanges(); // The instances are used in ngIf, tell Angular that something has changed. + + // Set the Input data. + this.componentInstances[type].course = this.course; + this.componentInstances[type].sections = this.sections; + + return true; + } catch(ex) { + this.logger.error('Error creating component', type, ex, componentClass); + return false; + } + } + + /** + * Function called when selected section changes. + * + * @param {any} newSection The new selected section. + */ + sectionChanged(newSection: any) { + let previousValue = this.selectedSection; + this.selectedSection = newSection; + + // If there is a component to render the current section, update its section. + if (this.componentInstances.singleSection) { + this.componentInstances.singleSection.section = this.selectedSection; + if (this.componentInstances.singleSection.ngOnChanges) { + this.componentInstances.singleSection.ngOnChanges({ + section: new SimpleChange(previousValue, newSection, typeof previousValue != 'undefined') + }); + } + } + } + + /** + * Compare if two sections are equal. + * + * @param {any} s1 First section. + * @param {any} s2 Second section. + * @return {boolean} Whether they're equal. + */ + compareSections(s1: any, s2: any) : boolean { + return s1 && s2 ? s1.id === s2.id : s1 === s2; + } +} diff --git a/src/core/course/course.module.ts b/src/core/course/course.module.ts new file mode 100644 index 000000000..e54c6b8b1 --- /dev/null +++ b/src/core/course/course.module.ts @@ -0,0 +1,31 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { NgModule } from '@angular/core'; +import { CoreCourseProvider } from './providers/course'; +import { CoreCourseHelperProvider } from './providers/helper'; +import { CoreCourseFormatDelegate } from './providers/format-delegate'; + +@NgModule({ + declarations: [], + imports: [ + ], + providers: [ + CoreCourseProvider, + CoreCourseHelperProvider, + CoreCourseFormatDelegate + ], + exports: [] +}) +export class CoreCourseModule {} diff --git a/src/core/course/lang/en.json b/src/core/course/lang/en.json index 8c6cb9d35..f4997d593 100644 --- a/src/core/course/lang/en.json +++ b/src/core/course/lang/en.json @@ -18,5 +18,6 @@ "hiddenfromstudents": "Hidden from students", "nocontentavailable": "No content available at the moment.", "overriddennotice": "Your final grade from this activity was manually adjusted.", + "sections": "Sections", "useactivityonbrowser": "You can still use it using your device's web browser." } \ No newline at end of file diff --git a/src/core/course/pages/section/section.html b/src/core/course/pages/section/section.html new file mode 100644 index 000000000..e0d033b3a --- /dev/null +++ b/src/core/course/pages/section/section.html @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/core/course/pages/section/section.module.ts b/src/core/course/pages/section/section.module.ts new file mode 100644 index 000000000..c9ab2da0f --- /dev/null +++ b/src/core/course/pages/section/section.module.ts @@ -0,0 +1,35 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { NgModule } from '@angular/core'; +import { IonicPageModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreCourseSectionPage } from './section'; +import { CoreComponentsModule } from '../../../../components/components.module'; +import { CoreDirectivesModule } from '../../../../directives/directives.module'; +import { CoreCourseComponentsModule } from '../../components/components.module'; + +@NgModule({ + declarations: [ + CoreCourseSectionPage, + ], + imports: [ + CoreComponentsModule, + CoreDirectivesModule, + CoreCourseComponentsModule, + IonicPageModule.forChild(CoreCourseSectionPage), + TranslateModule.forChild() + ], +}) +export class CoreCourseSectionPageModule {} diff --git a/src/core/course/pages/section/section.ts b/src/core/course/pages/section/section.ts new file mode 100644 index 000000000..337eb3584 --- /dev/null +++ b/src/core/course/pages/section/section.ts @@ -0,0 +1,128 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { Component } from '@angular/core'; +import { IonicPage, NavParams } from 'ionic-angular'; +import { TranslateService } from '@ngx-translate/core'; +import { CoreDomUtilsProvider } from '../../../../providers/utils/dom'; +import { CoreTextUtilsProvider } from '../../../../providers/utils/text'; +import { CoreCourseProvider } from '../../providers/course'; +import { CoreCourseHelperProvider } from '../../providers/helper'; +import { CoreCourseFormatDelegate } from '../../providers/format-delegate'; +import { CoreCoursesDelegate } from '../../../courses/providers/delegate'; + +/** + * Page that displays the list of courses the user is enrolled in. + */ +@IonicPage({segment: 'core-course-section'}) +@Component({ + selector: 'page-core-course-section', + templateUrl: 'section.html', +}) +export class CoreCourseSectionPage { + title: string; + course: any; + sections: any[]; + courseHandlers: any[]; + dataLoaded: boolean; + + constructor(navParams: NavParams, private courseProvider: CoreCourseProvider, private domUtils: CoreDomUtilsProvider, + private courseFormatDelegate: CoreCourseFormatDelegate, private coursesDelegate: CoreCoursesDelegate, + private translate: TranslateService, private courseHelper: CoreCourseHelperProvider, + private textUtils: CoreTextUtilsProvider) { + this.course = navParams.get('course'); + this.title = courseFormatDelegate.getCourseTitle(this.course); + } + + /** + * View loaded. + */ + ionViewDidLoad() { + this.loadData().finally(() => { + this.dataLoaded = true; + }); + } + + /** + * Fetch and load all the data required for the view. + */ + protected loadData(refresh?: boolean) { + let promises = [], + promise; + + // Get the completion status. + if (this.course.enablecompletion === false) { + // Completion not enabled. + promise = Promise.resolve({}); + } else { + promise = this.courseProvider.getActivitiesCompletionStatus(this.course.id).catch(() => { + // It failed, don't use completion. + return {}; + }); + } + + promises.push(promise.then((completionStatus) => { + // Get all the sections. + promises.push(this.courseProvider.getSections(this.course.id, false, true).then((sections) => { + // Format the name of each section and check if it has content. + this.sections = sections.map((section) => { + this.textUtils.formatText(section.name.trim(), true, true).then((name) => { + section.formattedName = name; + }); + section.hasContent = this.courseHelper.sectionHasContent(section); + return section; + }); + + + if (this.courseFormatDelegate.canViewAllSections(this.course)) { + // Add a fake first section (all sections). + this.sections.unshift({ + name: this.translate.instant('core.course.allsections'), + id: CoreCourseProvider.ALL_SECTIONS_ID + }); + } + })); + })); + + // Load the course handlers. + promises.push(this.coursesDelegate.getHandlersToDisplay(this.course, refresh, false).then((handlers) => { + this.courseHandlers = handlers; + })); + + return Promise.all(promises).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'mm.course.couldnotloadsectioncontent', true); + }); + } + + /** + * Refresh the data. + * + * @param {any} refresher Refresher. + */ + doRefresh(refresher: any) { + let promises = []; + + promises.push(this.courseProvider.invalidateSections(this.course.id)); + + // if ($scope.sections) { + // promises.push($mmCoursePrefetchDelegate.invalidateCourseUpdates(courseId)); + // } + + Promise.all(promises).finally(() => { + this.loadData(true).finally(() => { + refresher.complete(); + }); + }); + } +} diff --git a/src/core/course/providers/course.ts b/src/core/course/providers/course.ts index abc44bb22..1323b9ef9 100644 --- a/src/core/course/providers/course.ts +++ b/src/core/course/providers/course.ts @@ -27,6 +27,8 @@ import { CoreConstants } from '../../constants'; */ @Injectable() export class CoreCourseProvider { + public static ALL_SECTIONS_ID = -1; + // Variables for database. protected COURSE_STATUS_TABLE = 'course_status'; protected courseStatusTableSchema = { diff --git a/src/core/course/providers/format-delegate.ts b/src/core/course/providers/format-delegate.ts new file mode 100644 index 000000000..5e4efe956 --- /dev/null +++ b/src/core/course/providers/format-delegate.ts @@ -0,0 +1,368 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { NavController } from 'ionic-angular'; +import { CoreEventsProvider } from '../../../providers/events'; +import { CoreLoggerProvider } from '../../../providers/logger'; +import { CoreSitesProvider } from '../../../providers/sites'; +import { CoreCourseProvider } from './course'; + +/** + * Interface that all course format handlers should implement. + */ +export interface CoreCourseFormatHandler { + /** + * Name of the format. It should match the "format" returned in core_course_get_courses. + * @type {string} + */ + name: string; + + /** + * Whether or not the handler is enabled on a site level. + * + * @return {boolean|Promise} True or promise resolved with true if enabled. + */ + isEnabled(): boolean|Promise; + + /** + * Get the title to use in course page. If not defined, course fullname. + * + * @param {any} course The course. + * @return {string} Title. + */ + getCourseTitle?(course: any) : string; + + /** + * Whether it allows seeing all sections at the same time. Defaults to true. + * + * @param {any} course The course to check. + * @type {boolean} Whether it can view all sections. + */ + canViewAllSections?(course: any) : boolean; + + /** + * Whether the default section selector should be displayed. Defaults to true. + * + * @param {any} course The course to check. + * @type {boolean} Whether the default section selector should be displayed. + */ + displaySectionSelector?(course: any) : boolean; + + /** + * Given a list of sections, get the "current" section that should be displayed first. Defaults to first section. + * + * @param {any} course The course to get the title. + * @param {any[]} sections List of sections. + * @return {any} Current section. + */ + getCurrentSection?(course: any, sections: any[]) : any; + + /** + * 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 {NavController} navCtrl The NavController instance to use. + * @param {any} course The course to open. It should contain a "format" attribute. + * @return {Promise} Promise resolved when done. + */ + openCourse?(navCtrl: NavController, course: any) : 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. + * + * @param {any} course The course to render. + * @return {any} The component to use, undefined if not found. + */ + getCourseFormatComponent?(course: any) : any; + + /** + * Return the Component to use to display the course summary inside the default course format. + * + * @param {any} course The course to render. + * @return {any} The component to use, undefined if not found. + */ + getCourseSummaryComponent?(course: any): any; + + /** + * Return the Component to use to display the section selector inside the default course format. + * + * @param {any} course The course to render. + * @return {any} The component to use, undefined if not found. + */ + getSectionSelectorComponent?(course: any): any; + + /** + * 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. + * + * @param {any} course The course to render. + * @return {any} The component to use, undefined if not found. + */ + getSingleSectionComponent?(course: any): any; + + /** + * Return the Component to use to display all sections in a course. + * + * @param {any} course The course to render. + * @return {any} The component to use, undefined if not found. + */ + getAllSectionsComponent?(course: any): any; +}; + +/** + * Service to interact with course formats. Provides the functions to register and interact with the addons. + */ +@Injectable() +export class CoreCourseFormatDelegate { + protected logger; + protected handlers: {[s: string]: CoreCourseFormatHandler} = {}; // All registered handlers. + protected enabledHandlers: {[s: string]: CoreCourseFormatHandler} = {}; // Handlers enabled for the current site. + protected lastUpdateHandlersStart: number; + + constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, eventsProvider: CoreEventsProvider) { + this.logger = logger.getInstance('CoreCoursesCourseFormatDelegate'); + + eventsProvider.on(CoreEventsProvider.LOGIN, this.updateHandlers.bind(this)); + eventsProvider.on(CoreEventsProvider.SITE_UPDATED, this.updateHandlers.bind(this)); + eventsProvider.on(CoreEventsProvider.REMOTE_ADDONS_LOADED, this.updateHandlers.bind(this)); + } + + /** + * Whether it allows seeing all sections at the same time. Defaults to true. + * + * @param {any} course The course to check. + * @return {boolean} Whether it allows seeing all sections at the same time. + */ + canViewAllSections(course: any) : boolean { + return this.executeFunction(course.format, 'canViewAllSections', true, [course]); + } + + /** + * Whether the default section selector should be displayed. Defaults to true. + * + * @param {any} course The course to check. + * @return {boolean} Whether the section selector should be displayed. + */ + displaySectionSelector(course: any) : boolean { + return this.executeFunction(course.format, 'displaySectionSelector', true, [course]); + } + + /** + * Execute a certain function in a course format handler. If the handler isn't found or function isn't defined, + * return the default value. + * + * @param {string} format The format name. + * @param {string} fnName Name of the function to execute. + * @param {any} defaultValue Value to return if not found. + * @param {any[]} params Parameters to pass to the function. + * @return {any} Function returned value or default value. + */ + protected executeFunction(format: string, fnName: string, defaultValue?: any, params?: any[]) : any { + let handler = this.enabledHandlers[format]; + if (handler && handler[fnName]) { + return handler[fnName].apply(handler, params); + } + return defaultValue; + } + + /** + * Get the component to use to display all sections in a course. + * + * @param {any} course The course to render. + * @return {any} The component to use, undefined if not found. + */ + getAllSectionsComponent(course: any) : any { + return this.executeFunction(course.format, 'getAllSectionsComponent', undefined, [course]); + } + + /** + * Get the component to use to display a course format. + * + * @param {any} course The course to render. + * @return {any} The component to use, undefined if not found. + */ + getCourseFormatComponent(course: any) : any { + return this.executeFunction(course.format, 'getCourseFormatComponent', undefined, [course]); + } + + /** + * Get the component to use to display the course summary in the default course format. + * + * @param {any} course The course to render. + * @return {any} The component to use, undefined if not found. + */ + getCourseSummaryComponent(course: any) : any { + return this.executeFunction(course.format, 'getCourseSummaryComponent', undefined, [course]); + } + + /** + * Given a course, return the title to use in the course page. + * + * @param {any} course The course to get the title. + * @return {string} Course title. + */ + getCourseTitle(course: any) : string { + return this.executeFunction(course.format, 'getCourseTitle', course.fullname || '', [course]); + } + + /** + * Given a course and a list of sections, return the current section that should be displayed first. + * + * @param {any} course The course to get the title. + * @param {any[]} sections List of sections. + * @return {any} Current section. + */ + getCurrentSection(course: any, sections: any[]) : any { + // Calculate default section (the first one that isn't all sections). + let defaultSection; + for (let i = 0; i < sections.length; i++) { + let section = sections[i]; + if (section.id != CoreCourseProvider.ALL_SECTIONS_ID) { + defaultSection = section; + break; + } + } + + return this.executeFunction(course.format, 'getCurrentSection', defaultSection, [course, sections]); + } + + /** + * Get the component to use to display the section selector inside the default course format. + * + * @param {any} course The course to render. + * @return {any} The component to use, undefined if not found. + */ + getSectionSelectorComponent(course: any) : any { + return this.executeFunction(course.format, 'getSectionSelectorComponent', undefined, [course]); + } + + /** + * 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 {any} course The course to render. + * @return {any} The component to use, undefined if not found. + */ + getSingleSectionComponent(course: any) : any { + return this.executeFunction(course.format, 'getSingleSectionComponent', undefined, [course]); + } + + /** + * Check if a time belongs to the last update handlers call. + * This is to handle the cases where updateHandlers don't finish in the same order as they're called. + * + * @param {number} time Time to check. + * @return {boolean} Whether it's the last call. + */ + isLastUpdateCall(time: number) : boolean { + if (!this.lastUpdateHandlersStart) { + return true; + } + return time == this.lastUpdateHandlersStart; + } + + /** + * Open a course. + * + * @param {NavController} navCtrl The NavController instance to use. + * @param {any} course The course to open. It should contain a "format" attribute. + * @return {Promise} Promise resolved when done. + */ + openCourse(navCtrl: NavController, course: any) : Promise { + if (this.enabledHandlers[course.format] && this.enabledHandlers[course.format].openCourse) { + return this.enabledHandlers[course.format].openCourse(navCtrl, course); + } + return navCtrl.push('CoreCourseSectionPage', {course: course}); + } + + /** + * Register a handler. + * + * @param {CoreCourseFormatHandler} handler The handler to register. + * @return {boolean} True if registered successfully, false otherwise. + */ + registerHandler(handler: CoreCourseFormatHandler) : boolean { + if (typeof this.handlers[handler.name] !== 'undefined') { + this.logger.log(`Addon '${handler.name}' already registered`); + return false; + } + this.logger.log(`Registered addon '${handler.name}'`); + this.handlers[handler.name] = handler; + return true; + } + + /** + * Update the handler for the current site. + * + * @param {CoreCourseFormatHandler} handler The handler to check. + * @param {number} time Time this update process started. + * @return {Promise} Resolved when done. + */ + protected updateHandler(handler: CoreCourseFormatHandler, time: number) : Promise { + let promise, + siteId = this.sitesProvider.getCurrentSiteId(), + currentSite = this.sitesProvider.getCurrentSite(); + + if (!this.sitesProvider.isLoggedIn()) { + promise = Promise.reject(null); + } else if (currentSite.isFeatureDisabled('CoreCourseFormatHandler_' + handler.name)) { + promise = Promise.resolve(false); + } else { + promise = Promise.resolve(handler.isEnabled()); + } + + // Checks if the handler is enabled. + return promise.catch(() => { + return false; + }).then((enabled: boolean) => { + // Verify that this call is the last one that was started. + // Check that site hasn't changed since the check started. + if (this.isLastUpdateCall(time) && this.sitesProvider.getCurrentSiteId() === siteId) { + if (enabled) { + this.enabledHandlers[handler.name] = handler; + } else { + delete this.enabledHandlers[handler.name]; + } + } + }); + } + + /** + * Update the handlers for the current site. + * + * @return {Promise} Resolved when done. + */ + protected updateHandlers() : Promise { + let promises = [], + now = Date.now(); + + this.logger.debug('Updating handlers for current site.'); + + this.lastUpdateHandlersStart = now; + + // Loop over all the handlers. + for (let name in this.handlers) { + promises.push(this.updateHandler(this.handlers[name], now)); + } + + return Promise.all(promises).catch(() => { + // Never reject. + }); + } +} diff --git a/src/core/course/providers/helper.ts b/src/core/course/providers/helper.ts new file mode 100644 index 000000000..cf9ac5900 --- /dev/null +++ b/src/core/course/providers/helper.ts @@ -0,0 +1,40 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { CoreCourseProvider } from './course'; + +/** + * Helper to gather some common course functions. + */ +@Injectable() +export class CoreCourseHelperProvider { + + constructor() {} + + /** + * Check if a section has content. + * + * @param {any} section Section to check. + * @return {boolean} Whether the section has content. + */ + sectionHasContent(section: any) : boolean { + if (section.id == CoreCourseProvider.ALL_SECTIONS_ID || section.hiddenbynumsections) { + return false; + } + + return (typeof section.availabilityinfo != 'undefined' && section.availabilityinfo != '') || + section.summary != '' || (section.modules && section.modules.length > 0); + } +} diff --git a/src/core/courses/components/course-progress/course-progress.ts b/src/core/courses/components/course-progress/course-progress.ts index e7a2fb49c..db705c889 100644 --- a/src/core/courses/components/course-progress/course-progress.ts +++ b/src/core/courses/components/course-progress/course-progress.ts @@ -15,6 +15,7 @@ import { Component, Input, OnInit } from '@angular/core'; import { NavController } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; +import { CoreCourseFormatDelegate } from '../../../course/providers/format-delegate'; /** * This component is meant to display a course for a list of courses with progress. @@ -43,7 +44,8 @@ export class CoreCoursesCourseProgressComponent implements OnInit { }; protected buttons; - constructor(private navCtrl: NavController, private translate: TranslateService) { + constructor(private navCtrl: NavController, private translate: TranslateService, + private courseFormatDelegate: CoreCourseFormatDelegate) { this.downloadText = this.translate.instant('core.course.downloadcourse'); this.downloadingText = this.translate.instant('core.downloading'); } @@ -59,7 +61,7 @@ export class CoreCoursesCourseProgressComponent implements OnInit { * Open a course. */ openCourse(course) { - this.navCtrl.push('CoreCourseSectionPage', {course: course}); + this.courseFormatDelegate.openCourse(this.navCtrl, course); } }