From e1bc11e44bfa4f4fb4bd98f5753ddf09bc455358 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 11 Jan 2018 13:18:17 +0100 Subject: [PATCH] MOBILE-2310 course: Implement topics and weeks formats --- src/core/constants.ts | 1 + src/core/course/components/format/format.html | 66 ++++----- src/core/course/components/format/format.ts | 6 +- src/core/course/course.module.ts | 8 +- .../formats/topics/providers/handler.ts | 35 +++++ .../course/formats/topics/topics.module.ts | 32 +++++ .../course/formats/weeks/providers/handler.ts | 87 ++++++++++++ src/core/course/formats/weeks/weeks.module.ts | 32 +++++ src/core/course/pages/section/section.ts | 1 + src/core/course/providers/default-format.ts | 128 ++++++++++++++++++ src/core/course/providers/format-delegate.ts | 76 +++++++---- 11 files changed, 410 insertions(+), 62 deletions(-) create mode 100644 src/core/course/formats/topics/providers/handler.ts create mode 100644 src/core/course/formats/topics/topics.module.ts create mode 100644 src/core/course/formats/weeks/providers/handler.ts create mode 100644 src/core/course/formats/weeks/weeks.module.ts create mode 100644 src/core/course/providers/default-format.ts diff --git a/src/core/constants.ts b/src/core/constants.ts index 08b8d83e8..524cc86ec 100644 --- a/src/core/constants.ts +++ b/src/core/constants.ts @@ -17,6 +17,7 @@ */ export class CoreConstants { public static secondsYear = 31536000; + public static secondsWeek = 604800; public static secondsDay = 86400; public static secondsHour = 3600; public static secondsMinute = 60; diff --git a/src/core/course/components/format/format.html b/src/core/course/components/format/format.html index 043f659c7..f95f21e6f 100644 --- a/src/core/course/components/format/format.html +++ b/src/core/course/components/format/format.html @@ -1,6 +1,6 @@
- + @@ -8,39 +8,41 @@ - - - - - - {{section.formattedName || section.name}} - - - - - - - - + + + + + + + {{section.formattedName || section.name}} + + + + + + + + - -
- - - - - -
- - -
- - - + +
+ + + - - -
+ +
+ + +
+ + + + + + +
+
diff --git a/src/core/course/components/format/format.ts b/src/core/course/components/format/format.ts index 7af959cd8..7b93ef378 100644 --- a/src/core/course/components/format/format.ts +++ b/src/core/course/components/format/format.ts @@ -68,6 +68,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges { selectedSection: any; allSectionsId: number = CoreCourseProvider.ALL_SECTIONS_ID; selectOptions: any = {}; + loaded: boolean; protected logger; @@ -95,7 +96,10 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges { if (changes.sections && this.sections) { if (!this.selectedSection) { // There is no selected section yet, calculate which one to get. - this.sectionChanged(this.cfDelegate.getCurrentSection(this.course, this.sections)); + this.cfDelegate.getCurrentSection(this.course, this.sections).then((section) => { + this.loaded = true; + this.sectionChanged(section); + }); } else { // We have a selected section, but the list has changed. Search the section in the list. let newSection; diff --git a/src/core/course/course.module.ts b/src/core/course/course.module.ts index a9e9baf1a..f94c17fa5 100644 --- a/src/core/course/course.module.ts +++ b/src/core/course/course.module.ts @@ -17,16 +17,22 @@ import { CoreCourseProvider } from './providers/course'; import { CoreCourseHelperProvider } from './providers/helper'; import { CoreCourseFormatDelegate } from './providers/format-delegate'; import { CoreCourseModuleDelegate } from './providers/module-delegate'; +import { CoreCourseFormatDefaultHandler } from './providers/default-format'; +import { CoreCourseFormatTopicsModule} from './formats/topics/topics.module'; +import { CoreCourseFormatWeeksModule } from './formats/weeks/weeks.module'; @NgModule({ declarations: [], imports: [ + CoreCourseFormatTopicsModule, + CoreCourseFormatWeeksModule ], providers: [ CoreCourseProvider, CoreCourseHelperProvider, CoreCourseFormatDelegate, - CoreCourseModuleDelegate + CoreCourseModuleDelegate, + CoreCourseFormatDefaultHandler ], exports: [] }) diff --git a/src/core/course/formats/topics/providers/handler.ts b/src/core/course/formats/topics/providers/handler.ts new file mode 100644 index 000000000..18d2c1759 --- /dev/null +++ b/src/core/course/formats/topics/providers/handler.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 { Injectable } from '@angular/core'; +import { CoreCourseFormatHandler } from '../../../providers/format-delegate'; + +/** + * Handler to support topics course format. + */ +@Injectable() +export class CoreCourseFormatTopicsHandler implements CoreCourseFormatHandler { + name = 'topics'; + + constructor() {} + + /** + * 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 { + return true; + } +} diff --git a/src/core/course/formats/topics/topics.module.ts b/src/core/course/formats/topics/topics.module.ts new file mode 100644 index 000000000..97cedcc73 --- /dev/null +++ b/src/core/course/formats/topics/topics.module.ts @@ -0,0 +1,32 @@ +// (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 { CoreCourseFormatTopicsHandler } from './providers/handler'; +import { CoreCourseFormatDelegate } from '../../providers/format-delegate'; + +@NgModule({ + declarations: [], + imports: [ + ], + providers: [ + CoreCourseFormatTopicsHandler + ], + exports: [] +}) +export class CoreCourseFormatTopicsModule { + constructor(formatDelegate: CoreCourseFormatDelegate, handler: CoreCourseFormatTopicsHandler) { + formatDelegate.registerHandler(handler); + } +} diff --git a/src/core/course/formats/weeks/providers/handler.ts b/src/core/course/formats/weeks/providers/handler.ts new file mode 100644 index 000000000..3405f9721 --- /dev/null +++ b/src/core/course/formats/weeks/providers/handler.ts @@ -0,0 +1,87 @@ +// (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 { CoreCourseFormatHandler } from '../../../providers/format-delegate'; +import { CoreTimeUtilsProvider } from '../../../../../providers/utils/time'; +import { CoreConstants } from '../../../../constants'; + +/** + * Handler to support weeks course format. + */ +@Injectable() +export class CoreCourseFormatWeeksHandler implements CoreCourseFormatHandler { + name = 'weeks'; + + constructor(private timeUtils: CoreTimeUtilsProvider) {} + + /** + * 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 { + return true; + } + + /** + * Given a list of sections, get 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|Promise} Current section (or promise resolved with current section). + */ + getCurrentSection(course: any, sections: any[]) : any|Promise { + let now = this.timeUtils.timestamp(); + + if (now < course.startdate || (course.enddate && now > course.enddate)) { + // Course hasn't started yet or it has ended already. Return the first section. + return sections[1]; + } + + for (let i = 0; i < sections.length; i++) { + let section = sections[i]; + if (typeof section.section == 'undefined' || section.section < 1) { + continue; + } + + let dates = this.getSectionDates(section, course.startdate); + if ((now >= dates.start) && (now < dates.end)) { + return section; + } + } + + // The section wasn't found, return the first section. + return sections[1]; + } + + /** + * Return the start and end date of a section. + * + * @param {any} section The section to treat. + * @param {number} startDate The course start date (in seconds). + * @return {{start: number, end: number}} An object with the start and end date of the section. + */ + protected getSectionDates(section: any, startDate: number) : {start: number, end: number} { + // Hack alert. We add 2 hours to avoid possible DST problems. (e.g. we go into daylight savings and the date changes). + startDate = startDate + 7200; + + let dates = { + start: startDate + (CoreConstants.secondsWeek * (section.section - 1)), + end: 0 + }; + dates.end = dates.start + CoreConstants.secondsWeek; + return dates; + } +} diff --git a/src/core/course/formats/weeks/weeks.module.ts b/src/core/course/formats/weeks/weeks.module.ts new file mode 100644 index 000000000..b26e66f44 --- /dev/null +++ b/src/core/course/formats/weeks/weeks.module.ts @@ -0,0 +1,32 @@ +// (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 { CoreCourseFormatWeeksHandler } from './providers/handler'; +import { CoreCourseFormatDelegate } from '../../providers/format-delegate'; + +@NgModule({ + declarations: [], + imports: [ + ], + providers: [ + CoreCourseFormatWeeksHandler + ], + exports: [] +}) +export class CoreCourseFormatWeeksModule { + constructor(formatDelegate: CoreCourseFormatDelegate, handler: CoreCourseFormatWeeksHandler) { + formatDelegate.registerHandler(handler); + } +} diff --git a/src/core/course/pages/section/section.ts b/src/core/course/pages/section/section.ts index 5c635d91d..cafeef9f1 100644 --- a/src/core/course/pages/section/section.ts +++ b/src/core/course/pages/section/section.ts @@ -157,6 +157,7 @@ export class CoreCourseSectionPage implements OnDestroy { promises.push(this.courseProvider.invalidateSections(this.course.id)); promises.push(this.coursesProvider.invalidateUserCourses()); + promises.push(this.courseFormatDelegate.invalidateData(this.course, this.sections)); // if ($scope.sections) { // promises.push($mmCoursePrefetchDelegate.invalidateCourseUpdates(courseId)); diff --git a/src/core/course/providers/default-format.ts b/src/core/course/providers/default-format.ts new file mode 100644 index 000000000..6a8b294e4 --- /dev/null +++ b/src/core/course/providers/default-format.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 { Injectable } from '@angular/core'; +import { NavController } from 'ionic-angular'; +import { CoreCoursesProvider } from '../../courses/providers/courses'; +import { CoreCourseFormatHandler } from './format-delegate'; +import { CoreCourseProvider } from './course'; + +/** + * Default handler used when the course format doesn't have a specific implementation. + */ +@Injectable() +export class CoreCourseFormatDefaultHandler implements CoreCourseFormatHandler { + name = 'default'; + + constructor(private coursesProvider: CoreCoursesProvider) {} + + /** + * 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 { + return true; + } + + /** + * Get the title to use in course page. + * + * @param {any} course The course. + * @return {string} Title. + */ + getCourseTitle?(course: any) : string { + return course.fullname || ''; + } + + /** + * 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 { + return true; + } + + /** + * 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 { + return true; + } + + /** + * Given a list of sections, get 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|Promise} Current section (or promise resolved with current section). + */ + getCurrentSection(course: any, sections: any[]) : any|Promise { + // We need the "marker" to determine the current section. + return this.coursesProvider.getCoursesByField('id', course.id).catch(() => { + // Ignore errors. + }).then((courses) => { + if (courses && courses[0]) { + // Find the marked section. + let course = courses[0]; + for (let i = 0; i < sections.length; i++) { + let section = sections[i]; + if (section.section == course.marker) { + return section; + } + } + } + + // Marked section not found or we couldn't retrieve the marker. Return the first section. + for (let i = 0; i < sections.length; i++) { + let section = sections[i]; + if (section.id != CoreCourseProvider.ALL_SECTIONS_ID) { + return section; + } + } + + return Promise.reject(null); + }); + } + + /** + * Invalidate the data required to load the course format. + * + * @param {any} course The course to get the title. + * @param {any[]} sections List of sections. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateData(course: any, sections: any[]) : Promise { + return this.coursesProvider.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 {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 navCtrl.push('CoreCourseSectionPage', {course: course}); + } +} diff --git a/src/core/course/providers/format-delegate.ts b/src/core/course/providers/format-delegate.ts index 5e4efe956..efa96dc50 100644 --- a/src/core/course/providers/format-delegate.ts +++ b/src/core/course/providers/format-delegate.ts @@ -18,6 +18,7 @@ import { CoreEventsProvider } from '../../../providers/events'; import { CoreLoggerProvider } from '../../../providers/logger'; import { CoreSitesProvider } from '../../../providers/sites'; import { CoreCourseProvider } from './course'; +import { CoreCourseFormatDefaultHandler } from './default-format'; /** * Interface that all course format handlers should implement. @@ -65,9 +66,10 @@ export interface CoreCourseFormatHandler { * * @param {any} course The course to get the title. * @param {any[]} sections List of sections. - * @return {any} Current section. + * @return {any|Promise} Current section (or promise resolved with current section). If a promise is returned, it should + * never fail. */ - getCurrentSection?(course: any, sections: any[]) : any; + getCurrentSection?(course: any, sections: any[]) : any|Promise; /** * Open the page to display a course. If not defined, the page CoreCourseSectionPage will be opened. @@ -123,6 +125,15 @@ export interface CoreCourseFormatHandler { * @return {any} The component to use, undefined if not found. */ getAllSectionsComponent?(course: any): any; + + /** + * Invalidate the data required to load the course format. + * + * @param {any} course The course to get the title. + * @param {any[]} sections List of sections. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateData?(course: any, sections: any[]) : Promise; }; /** @@ -135,7 +146,8 @@ export class CoreCourseFormatDelegate { protected enabledHandlers: {[s: string]: CoreCourseFormatHandler} = {}; // Handlers enabled for the current site. protected lastUpdateHandlersStart: number; - constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, eventsProvider: CoreEventsProvider) { + constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, eventsProvider: CoreEventsProvider, + private defaultHandler: CoreCourseFormatDefaultHandler) { this.logger = logger.getInstance('CoreCoursesCourseFormatDelegate'); eventsProvider.on(CoreEventsProvider.LOGIN, this.updateHandlers.bind(this)); @@ -150,7 +162,7 @@ export class CoreCourseFormatDelegate { * @return {boolean} Whether it allows seeing all sections at the same time. */ canViewAllSections(course: any) : boolean { - return this.executeFunction(course.format, 'canViewAllSections', true, [course]); + return this.executeFunction(course.format, 'canViewAllSections', [course]); } /** @@ -160,25 +172,25 @@ export class CoreCourseFormatDelegate { * @return {boolean} Whether the section selector should be displayed. */ displaySectionSelector(course: any) : boolean { - return this.executeFunction(course.format, 'displaySectionSelector', true, [course]); + return this.executeFunction(course.format, 'displaySectionSelector', [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. + * Execute a certain function in a course format handler. + * If the handler isn't found or function isn't defined, call the same function in the default handler. * * @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 { + protected executeFunction(format: string, fnName: string, params?: any[]) : any { let handler = this.enabledHandlers[format]; if (handler && handler[fnName]) { return handler[fnName].apply(handler, params); + } else if (this.defaultHandler[fnName]) { + return this.defaultHandler[fnName].apply(this.defaultHandler, params); } - return defaultValue; } /** @@ -188,7 +200,7 @@ export class CoreCourseFormatDelegate { * @return {any} The component to use, undefined if not found. */ getAllSectionsComponent(course: any) : any { - return this.executeFunction(course.format, 'getAllSectionsComponent', undefined, [course]); + return this.executeFunction(course.format, 'getAllSectionsComponent', [course]); } /** @@ -198,7 +210,7 @@ export class CoreCourseFormatDelegate { * @return {any} The component to use, undefined if not found. */ getCourseFormatComponent(course: any) : any { - return this.executeFunction(course.format, 'getCourseFormatComponent', undefined, [course]); + return this.executeFunction(course.format, 'getCourseFormatComponent', [course]); } /** @@ -208,7 +220,7 @@ export class CoreCourseFormatDelegate { * @return {any} The component to use, undefined if not found. */ getCourseSummaryComponent(course: any) : any { - return this.executeFunction(course.format, 'getCourseSummaryComponent', undefined, [course]); + return this.executeFunction(course.format, 'getCourseSummaryComponent', [course]); } /** @@ -218,7 +230,7 @@ export class CoreCourseFormatDelegate { * @return {string} Course title. */ getCourseTitle(course: any) : string { - return this.executeFunction(course.format, 'getCourseTitle', course.fullname || '', [course]); + return this.executeFunction(course.format, 'getCourseTitle', [course]); } /** @@ -226,20 +238,17 @@ export class CoreCourseFormatDelegate { * * @param {any} course The course to get the title. * @param {any[]} sections List of sections. - * @return {any} Current section. + * @return {Promise} Promise resolved with 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; + getCurrentSection(course: any, sections: any[]) : Promise { + // Convert the result to a Promise if it isn't. + return Promise.resolve(this.executeFunction(course.format, 'getCurrentSection', [course, sections])).catch(() => { + // This function should never fail. Just return the first section. + if (sections[0].id != CoreCourseProvider.ALL_SECTIONS_ID) { + return sections[0]; } - } - - return this.executeFunction(course.format, 'getCurrentSection', defaultSection, [course, sections]); + return sections[1]; + }); } /** @@ -249,7 +258,7 @@ export class CoreCourseFormatDelegate { * @return {any} The component to use, undefined if not found. */ getSectionSelectorComponent(course: any) : any { - return this.executeFunction(course.format, 'getSectionSelectorComponent', undefined, [course]); + return this.executeFunction(course.format, 'getSectionSelectorComponent', [course]); } /** @@ -260,7 +269,18 @@ export class CoreCourseFormatDelegate { * @return {any} The component to use, undefined if not found. */ getSingleSectionComponent(course: any) : any { - return this.executeFunction(course.format, 'getSingleSectionComponent', undefined, [course]); + return this.executeFunction(course.format, 'getSingleSectionComponent', [course]); + } + + /** + * Invalidate the data required to load the course format. + * + * @param {any} course The course to get the title. + * @param {any[]} sections List of sections. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateData(course: any, sections: any[]) : Promise { + return this.executeFunction(course.format, 'invalidateData', [course, sections]); } /**