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]);
}
/**