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 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ {{course.progress}}% - + 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" }}

+
+
+ + +
{{ 'core.courses.recentlyoverdue' | translate }}
+ +
+ + +
{{ 'core.today' | translate }}
+ +
+ + +
{{ 'core.courses.next7days' | translate }}
+ +
+ + +
{{ 'core.courses.next30days' | translate }}
+ +
+ + +
{{ '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 + }; + } +}