diff --git a/src/assets/img/icons/activities.svg b/src/assets/img/icons/activities.svg
new file mode 100644
index 000000000..56243a53c
--- /dev/null
+++ b/src/assets/img/icons/activities.svg
@@ -0,0 +1,178 @@
+
+
diff --git a/src/assets/img/icons/courses.svg b/src/assets/img/icons/courses.svg
new file mode 100644
index 000000000..7bd9cb672
--- /dev/null
+++ b/src/assets/img/icons/courses.svg
@@ -0,0 +1,257 @@
+
+
diff --git a/src/core/courses/components/components.module.ts b/src/core/courses/components/components.module.ts
index 6cddb3414..d34f17cac 100644
--- a/src/core/courses/components/components.module.ts
+++ b/src/core/courses/components/components.module.ts
@@ -18,26 +18,31 @@ import { IonicModule } from 'ionic-angular';
import { TranslateModule } from '@ngx-translate/core';
import { CoreComponentsModule } from '../../../components/components.module';
import { CoreDirectivesModule } from '../../../directives/directives.module';
+import { CorePipesModule } from '../../../pipes/pipes.module';
import { CoreCoursesCourseProgressComponent } from '../components/course-progress/course-progress';
import { CoreCoursesCourseListItemComponent } from '../components/course-list-item/course-list-item';
+import { CoreCoursesOverviewEventsComponent } from '../components/overview-events/overview-events';
@NgModule({
declarations: [
CoreCoursesCourseProgressComponent,
- CoreCoursesCourseListItemComponent
+ CoreCoursesCourseListItemComponent,
+ CoreCoursesOverviewEventsComponent
],
imports: [
CommonModule,
IonicModule,
TranslateModule.forChild(),
CoreComponentsModule,
- CoreDirectivesModule
+ CoreDirectivesModule,
+ CorePipesModule
],
providers: [
],
exports: [
CoreCoursesCourseProgressComponent,
- CoreCoursesCourseListItemComponent
+ CoreCoursesCourseListItemComponent,
+ CoreCoursesOverviewEventsComponent
]
})
export class CoreCoursesComponentsModule {}
diff --git a/src/core/courses/components/course-progress/course-progress.html b/src/core/courses/components/course-progress/course-progress.html
index 34c96b8d0..fa01a2fe5 100644
--- a/src/core/courses/components/course-progress/course-progress.html
+++ b/src/core/courses/components/course-progress/course-progress.html
@@ -7,7 +7,9 @@
diff --git a/src/core/courses/components/overview-events/overview-events.html b/src/core/courses/components/overview-events/overview-events.html
new file mode 100644
index 000000000..b7240d0f8
--- /dev/null
+++ b/src/core/courses/components/overview-events/overview-events.html
@@ -0,0 +1,66 @@
+
+
+
+
+ {{event.action.itemcount}}
+
+ {{event.timesort * 1000 | coreFormatDate:"dfmediumdate" }}
+
+
+
+ 0">
+ {{ 'core.courses.recentlyoverdue' | translate }}
+
+
+
+ 0">
+ {{ 'core.today' | translate }}
+
+
+
+ 0">
+ {{ 'core.courses.next7days' | translate }}
+
+
+
+ 0">
+ {{ 'core.courses.next30days' | translate }}
+
+
+
+ 0">
+ {{ 'core.courses.future' | translate }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/core/courses/components/overview-events/overview-events.scss b/src/core/courses/components/overview-events/overview-events.scss
new file mode 100644
index 000000000..e24e0ae82
--- /dev/null
+++ b/src/core/courses/components/overview-events/overview-events.scss
@@ -0,0 +1,2 @@
+core-courses-course-progress {
+}
diff --git a/src/core/courses/components/overview-events/overview-events.ts b/src/core/courses/components/overview-events/overview-events.ts
new file mode 100644
index 000000000..fd566c4fa
--- /dev/null
+++ b/src/core/courses/components/overview-events/overview-events.ts
@@ -0,0 +1,135 @@
+// (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, Output, OnChanges, EventEmitter, SimpleChange } from '@angular/core';
+import { CoreSitesProvider } from '../../../../providers/sites';
+import { CoreDomUtilsProvider } from '../../../../providers/utils/dom';
+import { CoreTextUtilsProvider } from '../../../../providers/utils/text';
+import { CoreUtilsProvider } from '../../../../providers/utils/utils';
+import * as moment from 'moment';
+
+/**
+ * Directive to render a list of events in course overview.
+ */
+@Component({
+ selector: 'core-courses-overview-events',
+ templateUrl: 'overview-events.html'
+})
+export class CoreCoursesOverviewEventsComponent implements OnChanges {
+ @Input() events: any[]; // The events to render.
+ @Input() showCourse?: boolean|string; // Whether to show the course name.
+ @Input() canLoadMore?: boolean; // Whether more events can be loaded.
+ @Output() loadMore: EventEmitter; // Notify that more events should be loaded.
+
+ empty: boolean;
+ loadingMore: boolean;
+ recentlyOverdue: any[] = [];
+ today: any[] = [];
+ next7Days: any[] = [];
+ next30Days: any[] = [];
+ future: any[] = [];
+
+ constructor(private utils: CoreUtilsProvider, private textUtils: CoreTextUtilsProvider,
+ private domUtils: CoreDomUtilsProvider, private sitesProvider: CoreSitesProvider) {
+ this.loadMore = new EventEmitter();
+ }
+
+ /**
+ * Detect changes on input properties.
+ */
+ ngOnChanges(changes: {[name: string]: SimpleChange}) {
+ this.showCourse = this.utils.isTrueOrOne(this.showCourse);
+
+ if (changes.events) {
+ this.updateEvents();
+ }
+ }
+
+ /**
+ * Filter the events by time.
+ *
+ * @param {number} start Number of days to start getting events from today. E.g. -1 will get events from yesterday.
+ * @param {number} [end] Number of days after the start.
+ */
+ protected filterEventsByTime(start: number, end?: number) {
+ start = moment().add(start, 'days').unix();
+ end = typeof end != 'undefined' ? moment().add(end, 'days').unix() : end;
+
+ return this.events.filter((event) => {
+ if (end) {
+ return start <= event.timesort && event.timesort < end;
+ }
+
+ return start <= event.timesort;
+ }).map((event) => {
+ // @todo: event.iconUrl = this.courseProvider.getModuleIconSrc(event.icon.component);
+ return event;
+ });
+ }
+
+ /**
+ * Update the events displayed.
+ */
+ protected updateEvents() {
+ this.empty = !this.events || this.events.length <= 0;
+ if (!this.empty) {
+ this.recentlyOverdue = this.filterEventsByTime(-14, 0);
+ this.today = this.filterEventsByTime(0, 1);
+ this.next7Days = this.filterEventsByTime(1, 7);
+ this.next30Days = this.filterEventsByTime(7, 30);
+ this.future = this.filterEventsByTime(30);
+ }
+ }
+
+ /**
+ * Load more events clicked.
+ */
+ loadMoreEvents() {
+ this.loadingMore = true;
+ this.loadMore.emit();
+ // this.loadMore().finally(function() {
+ // scope.loadingMore = false;
+ // });
+ }
+
+ /**
+ * Action clicked.
+ *
+ * @param {Event} e Click event.
+ * @param {string} url Url of the action.
+ */
+ action(e: Event, url: string) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ // Fix URL format.
+ url = this.textUtils.decodeHTMLEntities(url);
+
+ let modal = this.domUtils.showModalLoading();
+ this.sitesProvider.getCurrentSite().openInBrowserWithAutoLoginIfSameSite(url).finally(() => {
+ modal.dismiss();
+ });
+
+ // @todo
+ // $mmContentLinksHelper.handleLink(url).then((treated) => {
+ // if (!treated) {
+ // return this.sitesProvider.getCurrentSite().openInBrowserWithAutoLoginIfSameSite(url);
+ // }
+ // }).finally(() => {
+ // modal.dismiss();
+ // });
+
+ return false;
+ }
+}
diff --git a/src/core/courses/courses.module.ts b/src/core/courses/courses.module.ts
index 6cc2b1056..48462c473 100644
--- a/src/core/courses/courses.module.ts
+++ b/src/core/courses/courses.module.ts
@@ -15,6 +15,7 @@
import { NgModule } from '@angular/core';
import { CoreCoursesProvider } from './providers/courses';
import { CoreCoursesMainMenuHandler } from './providers/handlers';
+import { CoreCoursesMyOverviewProvider } from './providers/my-overview';
import { CoreMainMenuDelegate } from '../mainmenu/providers/delegate';
@NgModule({
@@ -23,7 +24,8 @@ import { CoreMainMenuDelegate } from '../mainmenu/providers/delegate';
],
providers: [
CoreCoursesProvider,
- CoreCoursesMainMenuHandler
+ CoreCoursesMainMenuHandler,
+ CoreCoursesMyOverviewProvider
],
exports: []
})
diff --git a/src/core/courses/lang/en.json b/src/core/courses/lang/en.json
index f04f3836d..09a14d7f8 100644
--- a/src/core/courses/lang/en.json
+++ b/src/core/courses/lang/en.json
@@ -4,6 +4,7 @@
"cannotretrievemorecategories": "Categories deeper than level {{$a}} cannot be retrieved.",
"categories": "Course categories",
"confirmselfenrol": "Are you sure you want to enrol yourself in this course?",
+ "courseoverview": "Course overview",
"courses": "Courses",
"downloadcourses": "Download courses",
"enrolme": "Enrol me",
@@ -13,19 +14,34 @@
"errorselfenrol": "An error occurred while self enrolling.",
"filtermycourses": "Filter my courses",
"frontpage": "Front page",
+ "future": "Future",
+ "inprogress": "In progress",
+ "morecourses": "More courses",
"mycourses": "My courses",
+ "next30days": "Next 30 days",
+ "next7days": "Next 7 days",
"nocourses": "No course information to show.",
+ "nocoursesfuture": "No future courses",
+ "nocoursesinprogress": "No in progress courses",
+ "nocoursesoverview": "No courses",
+ "nocoursespast": "No past courses",
"nocoursesyet": "No courses in this category",
+ "noevents": "No upcoming activities due",
"nosearchresults": "There were no results from your search",
"notenroled": "You are not enrolled in this course",
"notenrollable": "You cannot enrol yourself in this course.",
"password": "Enrolment key",
+ "past": "Past",
"paymentrequired": "This course requires a payment for entry.",
"paypalaccepted": "PayPal payments accepted",
+ "recentlyoverdue": "Recently overdue",
"search": "Search",
"searchcourses": "Search courses",
"searchcoursesadvice": "You can use the search courses button to find courses to access as a guest or enrol yourself in courses that allow it.",
"selfenrolment": "Self enrolment",
"sendpaymentbutton": "Send payment via PayPal",
+ "sortbycourses": "Sort by courses",
+ "sortbydates": "Sort by dates",
+ "timeline": "Timeline",
"totalcoursesearchresults": "Total courses: {{$a}}"
}
\ No newline at end of file
diff --git a/src/core/courses/pages/my-overview/my-overview.html b/src/core/courses/pages/my-overview/my-overview.html
new file mode 100644
index 000000000..1d9e3a1aa
--- /dev/null
+++ b/src/core/courses/pages/my-overview/my-overview.html
@@ -0,0 +1,74 @@
+
+
+ {{ 'core.courses.courseoverview' | translate }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ 'core.courses.sortbydates' | translate }}
+ {{ 'core.courses.sortbycourses' | translate }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ 'core.courses.inprogress' | translate }}
+ {{ 'core.courses.future' | translate }}
+ {{ 'core.courses.past' | translate }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/core/courses/pages/my-overview/my-overview.module.ts b/src/core/courses/pages/my-overview/my-overview.module.ts
new file mode 100644
index 000000000..0259d3bb8
--- /dev/null
+++ b/src/core/courses/pages/my-overview/my-overview.module.ts
@@ -0,0 +1,33 @@
+// (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 { CoreCoursesMyOverviewPage } from './my-overview';
+import { CoreComponentsModule } from '../../../../components/components.module';
+import { CoreCoursesComponentsModule } from '../../components/components.module';
+
+@NgModule({
+ declarations: [
+ CoreCoursesMyOverviewPage,
+ ],
+ imports: [
+ CoreComponentsModule,
+ CoreCoursesComponentsModule,
+ IonicPageModule.forChild(CoreCoursesMyOverviewPage),
+ TranslateModule.forChild()
+ ],
+})
+export class CoreCoursesMyOverviewPageModule {}
diff --git a/src/core/courses/pages/my-overview/my-overview.scss b/src/core/courses/pages/my-overview/my-overview.scss
new file mode 100644
index 000000000..89812f8f7
--- /dev/null
+++ b/src/core/courses/pages/my-overview/my-overview.scss
@@ -0,0 +1,3 @@
+page-core-courses-my-courses {
+
+}
diff --git a/src/core/courses/pages/my-overview/my-overview.ts b/src/core/courses/pages/my-overview/my-overview.ts
new file mode 100644
index 000000000..a1022b51d
--- /dev/null
+++ b/src/core/courses/pages/my-overview/my-overview.ts
@@ -0,0 +1,323 @@
+// (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, NavController } from 'ionic-angular';
+import { CoreDomUtilsProvider } from '../../../../providers/utils/dom';
+import { CoreCoursesProvider } from '../../providers/courses';
+import { CoreCoursesMyOverviewProvider } from '../../providers/my-overview';
+import * as moment from 'moment';
+
+/**
+ * Page that displays My Overview.
+ */
+@IonicPage()
+@Component({
+ selector: 'page-core-courses-my-overview',
+ templateUrl: 'my-overview.html',
+})
+export class CoreCoursesMyOverviewPage {
+ tabShown = 'courses';
+ timeline = {
+ sort: 'sortbydates',
+ events: [],
+ loaded: false,
+ canLoadMore: undefined
+ };
+ timelineCourses = {
+ courses: [],
+ loaded: false,
+ canLoadMore: false
+ };
+ courses = {
+ selected: 'inprogress',
+ loaded: false,
+ filter: '',
+ past: [],
+ inprogress: [],
+ future: []
+ };
+ showGrid = true;
+ showFilter = false;
+ searchEnabled: boolean;
+ filteredCourses: any[];
+
+ protected prefetchIconInitialized = false;
+ protected myCoursesObserver;
+ protected siteUpdatedObserver;
+
+ constructor(private navCtrl: NavController, private coursesProvider: CoreCoursesProvider,
+ private domUtils: CoreDomUtilsProvider, private myOverviewProvider: CoreCoursesMyOverviewProvider) {}
+
+ /**
+ * View loaded.
+ */
+ ionViewDidLoad() {
+ this.searchEnabled = !this.coursesProvider.isSearchCoursesDisabledInSite();
+
+ this.switchTab(this.tabShown);
+
+ // @todo: Course download.
+ }
+
+ /**
+ * Fetch the timeline.
+ *
+ * @param {number} [afterEventId] The last event id.
+ * @return {Promise} Promise resolved when done.
+ */
+ protected fetchMyOverviewTimeline(afterEventId?: number) : Promise {
+ return this.myOverviewProvider.getActionEventsByTimesort(afterEventId).then((events) => {
+ this.timeline.events = events.events;
+ this.timeline.canLoadMore = events.canLoadMore;
+ }).catch((error) => {
+ this.domUtils.showErrorModalDefault(error, 'Error getting my overview data.');
+ });
+ }
+
+ /**
+ * Fetch the timeline by courses.
+ *
+ * @return {Promise} Promise resolved when done.
+ */
+ protected fetchMyOverviewTimelineByCourses() : Promise {
+ return this.fetchUserCourses().then((courses) => {
+ let today = moment().unix(),
+ courseIds;
+ courses = courses.filter((course) => {
+ return course.startdate <= today && (!course.enddate || course.enddate >= today);
+ });
+
+ this.timelineCourses.courses = courses;
+ if (courses.length > 0) {
+ courseIds = courses.map((course) => {
+ return course.id;
+ });
+
+ return this.myOverviewProvider.getActionEventsByCourses(courseIds).then((courseEvents) => {
+ this.timelineCourses.courses.forEach((course) => {
+ course.events = courseEvents[course.id].events;
+ course.canLoadMore = courseEvents[course.id].canLoadMore;
+ });
+ });
+ }
+ }).catch((error) => {
+ this.domUtils.showErrorModalDefault(error, 'Error getting my overview data.');
+ });
+ }
+
+ /**
+ * Fetch the courses for my overview.
+ *
+ * @return {Promise} Promise resolved when done.
+ */
+ protected fetchMyOverviewCourses() : Promise {
+ return this.fetchUserCourses().then((courses) => {
+ const today = moment().unix();
+
+ this.courses.past = [];
+ this.courses.inprogress = [];
+ this.courses.future = [];
+
+ courses.forEach((course) => {
+ if (course.startdate > today) {
+ // Courses that have not started yet.
+ this.courses.future.push(course);
+ } else if (course.enddate && course.enddate < today) {
+ // Courses that have already ended.
+ this.courses.past.push(course);
+ } else {
+ // Courses still in progress.
+ this.courses.inprogress.push(course);
+ }
+ });
+
+ this.courses.filter = '';
+ this.showFilter = false;
+ this.filteredCourses = this.courses[this.courses.selected];
+ }).catch((error) => {
+ this.domUtils.showErrorModalDefault(error, 'Error getting my overview data.');
+ });
+ }
+
+ /**
+ * Fetch user courses.
+ *
+ * @return {Promise} Promise resolved when done.
+ */
+ protected fetchUserCourses() : Promise {
+ let courseIds;
+ return this.coursesProvider.getUserCourses().then((courses) => {
+ courseIds = courses.map((course) => {
+ return course.id;
+ });
+
+ // Load course options of the course.
+ return this.coursesProvider.getCoursesOptions(courseIds).then((options) => {
+ courses.forEach((course) => {
+ course.showProgress = true;
+ course.progress = isNaN(parseInt(course.progress, 10)) ? false : parseInt(course.progress, 10);
+
+ course.navOptions = options.navOptions[course.id];
+ course.admOptions = options.admOptions[course.id];
+ });
+
+ return courses.sort((a, b) => {
+ const compareA = a.fullname.toLowerCase(),
+ compareB = b.fullname.toLowerCase();
+
+ return compareA.localeCompare(compareB);
+ });
+ });
+ });
+ }
+
+ /**
+ * Show or hide the filter.
+ */
+ switchFilter() {
+ this.showFilter = !this.showFilter;
+ this.courses.filter = '';
+ this.filteredCourses = this.courses[this.courses.selected];
+ }
+
+ /**
+ * The filter has changed.
+ *
+ * @param {string} newValue New filter value.
+ */
+ filterChanged(newValue: string) {
+ if (!newValue || !this.courses[this.courses.selected]) {
+ this.filteredCourses = this.courses[this.courses.selected];
+ } else {
+ this.filteredCourses = this.courses[this.courses.selected].filter((course) => {
+ return course.fullname.indexOf(newValue) > -1;
+ });
+ }
+ }
+
+ /**
+ * Switch grid/list view.
+ */
+ switchGrid() {
+ this.showGrid = !this.showGrid;
+ }
+
+ /**
+ * Refresh the data.
+ *
+ * @param {any} refresher Refresher.
+ */
+ refreshMyOverview(refresher: any) {
+ let promises = [];
+
+ if (this.tabShown == 'timeline') {
+ promises.push(this.myOverviewProvider.invalidateActionEventsByTimesort());
+ promises.push(this.myOverviewProvider.invalidateActionEventsByCourses());
+ }
+
+ promises.push(this.coursesProvider.invalidateUserCourses());
+ // promises.push(this.coursesDelegate.clearAndInvalidateCoursesOptions());
+
+ return Promise.all(promises).finally(() => {
+ switch (this.tabShown) {
+ case 'timeline':
+ switch (this.timeline.sort) {
+ case 'sortbydates':
+ return this.fetchMyOverviewTimeline();
+ case 'sortbycourses':
+ return this.fetchMyOverviewTimelineByCourses();
+ }
+ break;
+ case 'courses':
+ return this.fetchMyOverviewCourses();
+ }
+ }).finally(() => {
+ refresher.complete();
+ });
+ }
+
+ /**
+ * Change timeline sort being viewed.
+ */
+ switchSort() {
+ switch (this.timeline.sort) {
+ case 'sortbydates':
+ if (!this.timeline.loaded) {
+ this.fetchMyOverviewTimeline().finally(() => {
+ this.timeline.loaded = true;
+ });
+ }
+ break;
+ case 'sortbycourses':
+ if (!this.timelineCourses.loaded) {
+ this.fetchMyOverviewTimelineByCourses().finally(() => {
+ this.timelineCourses.loaded = true;
+ });
+ }
+ break;
+ }
+ }
+
+ /**
+ * Change tab being viewed.
+ *
+ * @param {string} tab Tab to display.
+ */
+ switchTab(tab: string) {
+ this.tabShown = tab;
+ switch (this.tabShown) {
+ case 'timeline':
+ if (!this.timeline.loaded) {
+ this.fetchMyOverviewTimeline().finally(() => {
+ this.timeline.loaded = true;
+ });
+ }
+ break;
+ case 'courses':
+ if (!this.courses.loaded) {
+ this.fetchMyOverviewCourses().finally(() => {
+ this.courses.loaded = true;
+ });
+ }
+ break;
+ }
+ }
+
+ /**
+ * Load more events.
+ */
+ loadMoreTimeline() : Promise {
+ return this.fetchMyOverviewTimeline(this.timeline.canLoadMore);
+ }
+
+ /**
+ * Load more events.
+ *
+ * @param {any} course Course.
+ */
+ loadMoreCourse(course) {
+ return this.myOverviewProvider.getActionEventsByCourse(course.id, course.canLoadMore).then((courseEvents) => {
+ course.events = course.events.concat(courseEvents.events);
+ course.canLoadMore = courseEvents.canLoadMore;
+ });
+ }
+
+ /**
+ * Go to search courses.
+ */
+ openSearch() {
+ this.navCtrl.push('CoreCoursesSearchPage');
+ }
+}
diff --git a/src/core/courses/providers/handlers.ts b/src/core/courses/providers/handlers.ts
index cece22a00..c003917b3 100644
--- a/src/core/courses/providers/handlers.ts
+++ b/src/core/courses/providers/handlers.ts
@@ -15,6 +15,7 @@
import { Injectable } from '@angular/core';
import { CoreCoursesProvider } from './courses';
import { CoreMainMenuHandler, CoreMainMenuHandlerData } from '../../mainmenu/providers/delegate';
+import { CoreCoursesMyOverviewProvider } from '../providers/my-overview';
/**
* Handler to inject an option into main menu.
@@ -23,8 +24,9 @@ import { CoreMainMenuHandler, CoreMainMenuHandlerData } from '../../mainmenu/pro
export class CoreCoursesMainMenuHandler implements CoreMainMenuHandler {
name = 'mmCourses';
priority = 1100;
+ isOverviewEnabled: boolean;
- constructor(private coursesProvider: CoreCoursesProvider) {}
+ constructor(private coursesProvider: CoreCoursesProvider, private myOverviewProvider: CoreCoursesMyOverviewProvider) {}
/**
* Check if the handler is enabled on a site level.
@@ -32,21 +34,16 @@ export class CoreCoursesMainMenuHandler implements CoreMainMenuHandler {
* @return {boolean} Whether or not the handler is enabled on a site level.
*/
isEnabled(): boolean|Promise {
- let myCoursesDisabled = this.coursesProvider.isMyCoursesDisabledInSite();
+ // Check if my overview is enabled.
+ return this.myOverviewProvider.isEnabled().then((enabled) => {
+ this.isOverviewEnabled = enabled;
+ if (enabled) {
+ return true;
+ }
- // Check if overview side menu is available, so it won't show My courses.
- // var $mmaMyOverview = $mmAddonManager.get('$mmaMyOverview');
- // if ($mmaMyOverview) {
- // return $mmaMyOverview.isSideMenuAvailable().then(function(enabled) {
- // if (enabled) {
- // return false;
- // }
- // // Addon not enabled, check my courses.
- // return !myCoursesDisabled;
- // });
- // }
- // Addon not present, check my courses.
- return !myCoursesDisabled;
+ // My overview not enabled, check if my courses is enabled.
+ return !this.coursesProvider.isMyCoursesDisabledInSite();
+ });
}
/**
@@ -55,11 +52,20 @@ export class CoreCoursesMainMenuHandler implements CoreMainMenuHandler {
* @return {CoreMainMenuHandlerData} Data needed to render the handler.
*/
getDisplayData(): CoreMainMenuHandlerData {
- return {
- icon: 'ionic',
- title: 'core.courses.mycourses',
- page: 'CoreCoursesMyCoursesPage',
- class: 'mm-mycourses-handler'
- };
+ if (this.isOverviewEnabled) {
+ return {
+ icon: 'ionic',
+ title: 'core.courses.courseoverview',
+ page: 'CoreCoursesMyOverviewPage',
+ class: 'mm-courseoverview-handler'
+ };
+ } else {
+ return {
+ icon: 'ionic',
+ title: 'core.courses.mycourses',
+ page: 'CoreCoursesMyCoursesPage',
+ class: 'mm-mycourses-handler'
+ };
+ }
}
}
diff --git a/src/core/courses/providers/my-overview.ts b/src/core/courses/providers/my-overview.ts
new file mode 100644
index 000000000..aab67f913
--- /dev/null
+++ b/src/core/courses/providers/my-overview.ts
@@ -0,0 +1,275 @@
+// (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 { CoreSitesProvider } from '../../../providers/sites';
+import { CoreSite } from '../../../classes/site';
+import * as moment from 'moment';
+
+/**
+ * Service that provides some features regarding course overview.
+ */
+@Injectable()
+export class CoreCoursesMyOverviewProvider {
+ public static EVENTS_LIMIT = 20;
+ public static EVENTS_LIMIT_PER_COURSE = 10;
+
+ constructor(private sitesProvider: CoreSitesProvider) {}
+
+ /**
+ * Get calendar action events for the given course.
+ *
+ * @param {number} courseId Only events in this course.
+ * @param {number} [afterEventId] The last seen event id.
+ * @param {string} [siteId] Site ID. If not defined, use current site.
+ * @return {Promise<{events: any[], canLoadMore: number}>} Promise resolved when the info is retrieved.
+ */
+ getActionEventsByCourse(courseId: number, afterEventId?: number, siteId?: string) :
+ Promise<{events: any[], canLoadMore: number}> {
+
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ let time = moment().subtract(14, 'days').unix(), // Check two weeks ago.
+ data: any = {
+ timesortfrom: time,
+ courseid: courseId,
+ limitnum: CoreCoursesMyOverviewProvider.EVENTS_LIMIT_PER_COURSE
+ },
+ preSets = {
+ cacheKey: this.getActionEventsByCourseCacheKey(courseId)
+ };
+
+ if (afterEventId) {
+ data.aftereventid = afterEventId;
+ }
+
+ return site.read('core_calendar_get_action_events_by_course', data, preSets).then((courseEvents) : any => {
+ if (courseEvents && courseEvents.events) {
+ return this.treatCourseEvents(courseEvents, time);
+ }
+ return Promise.reject(null);
+ });
+ });
+ }
+
+ /**
+ * Get cache key for get calendar action events for the given course value WS call.
+ *
+ * @param {number} courseId Only events in this course.
+ * @return {string} Cache key.
+ */
+ protected getActionEventsByCourseCacheKey(courseId: number) : string {
+ return this.getActionEventsByCoursesCacheKey() + ':' + courseId;
+ }
+
+ /**
+ * Get calendar action events for a given list of courses.
+ *
+ * @param {number[]} courseIds Course IDs.
+ * @param {string} [siteId] Site ID. If not defined, use current site.
+ * @return {Promise<{[s: string]: {events: any[], canLoadMore: number}}>} Promise resolved when the info is retrieved.
+ */
+ getActionEventsByCourses(courseIds: number[], siteId?: string) : Promise<{[s: string]: {events: any[], canLoadMore: number}}> {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ let time = moment().subtract(14, 'days').unix(), // Check two weeks ago.
+ data = {
+ timesortfrom: time,
+ courseids: courseIds,
+ limitnum: CoreCoursesMyOverviewProvider.EVENTS_LIMIT_PER_COURSE
+ },
+ preSets = {
+ cacheKey: this.getActionEventsByCoursesCacheKey()
+ };
+
+ return site.read('core_calendar_get_action_events_by_courses', data, preSets).then((events) : any => {
+ if (events && events.groupedbycourse) {
+ let courseEvents = {};
+
+ events.groupedbycourse.forEach((course) => {
+ courseEvents[course.courseid] = this.treatCourseEvents(course, time);
+ });
+
+ return courseEvents;
+ }
+
+ return Promise.reject(null);
+ });
+ });
+ }
+
+ /**
+ * Get cache key for get calendar action events for a given list of courses value WS call.
+ *
+ * @return {string} Cache key.
+ */
+ protected getActionEventsByCoursesCacheKey() : string {
+ return this.getRootCacheKey() + 'bycourse';
+ }
+
+ /**
+ * Get calendar action events based on the timesort value.
+ *
+ * @param {number} [afterEventId] The last seen event id.
+ * @param {string} [siteId] Site ID. If not defined, use current site.
+ * @return {Promise<{events: any[], canLoadMore: number}>} Promise resolved when the info is retrieved.
+ */
+ getActionEventsByTimesort(afterEventId: number, siteId?: string) : Promise<{events: any[], canLoadMore: number}> {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ let time = moment().subtract(14, 'days').unix(), // Check two weeks ago.
+ data: any = {
+ timesortfrom: time,
+ limitnum: CoreCoursesMyOverviewProvider.EVENTS_LIMIT
+ },
+ preSets = {
+ cacheKey: this.getActionEventsByTimesortCacheKey(afterEventId, data.limitnum),
+ getCacheUsingCacheKey: true,
+ uniqueCacheKey: true
+ };
+
+ if (afterEventId) {
+ data.aftereventid = afterEventId;
+ }
+
+ return site.read('core_calendar_get_action_events_by_timesort', data, preSets).then((events) : any => {
+ if (events && events.events) {
+ let canLoadMore = events.events.length >= data.limitnum ? events.lastid : undefined;
+
+ // Filter events by time in case it uses cache.
+ events = events.events.filter((element) => {
+ return element.timesort >= time;
+ });
+
+ return {
+ events: events,
+ canLoadMore: canLoadMore
+ };
+ }
+ return Promise.reject(null);
+ });
+ });
+ }
+
+ /**
+ * Get prefix cache key for calendar action events based on the timesort value WS calls.
+ *
+ * @return {string} Cache key.
+ */
+ protected getActionEventsByTimesortPrefixCacheKey() : string {
+ return this.getRootCacheKey() + 'bytimesort:';
+ }
+
+ /**
+ * Get cache key for get calendar action events based on the timesort value WS call.
+ *
+ * @param {number} [afterEventId] The last seen event id.
+ * @param {number} [limit] Limit num of the call.
+ * @return {string} Cache key.
+ */
+ protected getActionEventsByTimesortCacheKey(afterEventId?: number, limit?: number) : string {
+ afterEventId = afterEventId || 0;
+ limit = limit || 0;
+ return this.getActionEventsByTimesortPrefixCacheKey() + afterEventId + ':' + limit;
+ }
+
+ /**
+ * Get the root cache key for the WS calls related to overview.
+ *
+ * @return {string} Root cache key.
+ */
+ protected getRootCacheKey() : string {
+ return 'myoverview:';
+ }
+
+ /**
+ * Invalidates get calendar action events for a given list of courses WS call.
+ *
+ * @param {string} [siteId] Site ID to invalidate. If not defined, use current site.
+ * @return {Promise} Promise resolved when the data is invalidated.
+ */
+ invalidateActionEventsByCourses(siteId?: string) : Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ return site.invalidateWsCacheForKeyStartingWith(this.getActionEventsByCoursesCacheKey());
+ });
+ }
+
+ /**
+ * Invalidates get calendar action events based on the timesort value WS call.
+ *
+ * @param {string} [siteId] Site ID to invalidate. If not defined, use current site.
+ * @return {Promise} Promise resolved when the data is invalidated.
+ */
+ invalidateActionEventsByTimesort(siteId?: string) : Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ return site.invalidateWsCacheForKeyStartingWith(this.getActionEventsByTimesortPrefixCacheKey());
+ });
+ }
+
+ /**
+ * Returns whether or not My Overview is available for a certain site.
+ *
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved with true if available, resolved with false or rejected otherwise.
+ */
+ isAvailable(siteId?: string) : Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ return site.wsAvailable('core_calendar_get_action_events_by_courses');
+ });
+ }
+
+ /**
+ * Check if My Overview is disabled in a certain site.
+ *
+ * @param {CoreSite} [site] Site. If not defined, use current site.
+ * @return {boolean} Whether it's disabled.
+ */
+ isDisabledInSite(site?: CoreSite) : boolean {
+ site = site || this.sitesProvider.getCurrentSite();
+ return site.isFeatureDisabled('$mmSideMenuDelegate_mmaMyOverview');
+ }
+
+ /**
+ * Check if My Overview is available and not disabled.
+ *
+ * @return {Promise} Promise resolved with true if enabled, resolved with false otherwise.
+ */
+ isEnabled() : Promise {
+ if (!this.isDisabledInSite()) {
+ return this.isAvailable().catch(() => {
+ return false;
+ });
+ }
+ return Promise.resolve(false);
+ }
+
+ /**
+ * Handles course events, filtering and treating if more can be loaded.
+ *
+ * @param {any} course Object containing response course events info.
+ * @param {number} timeFrom Current time to filter events from.
+ * @return {{events: any[], canLoadMore: number}} Object with course events and last loaded event id if more can be loaded.
+ */
+ protected treatCourseEvents(course: any, timeFrom: number) : {events: any[], canLoadMore: number} {
+ let canLoadMore : number =
+ course.events.length >= CoreCoursesMyOverviewProvider.EVENTS_LIMIT_PER_COURSE ? course.lastid : undefined;
+
+ // Filter events by time in case it uses cache.
+ course.events = course.events.filter((element) => {
+ return element.timesort >= timeFrom;
+ });
+
+ return {
+ events: course.events,
+ canLoadMore: canLoadMore
+ };
+ }
+}