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