From a9a63e5668f3c4ffd2e1f48ce848cd432b864fda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Fri, 22 Dec 2017 09:09:47 +0100 Subject: [PATCH] MOBILE-2308 calendar: List events page --- src/addon/calendar/calendar.module.ts | 2 + src/addon/calendar/pages/list/list.html | 30 ++ src/addon/calendar/pages/list/list.module.ts | 8 +- src/addon/calendar/pages/list/list.ts | 278 +++++++++++++- src/addon/calendar/providers/calendar.ts | 359 +++++++++++++++++- src/addon/calendar/providers/helper.ts | 53 +++ src/classes/site.ts | 4 +- src/components/components.module.ts | 5 +- .../course-picker-menu-popover.html | 6 + .../course-picker-menu-popover.ts | 45 +++ src/providers/sites.ts | 2 +- 11 files changed, 779 insertions(+), 13 deletions(-) create mode 100644 src/addon/calendar/providers/helper.ts create mode 100644 src/components/course-picker-menu/course-picker-menu-popover.html create mode 100644 src/components/course-picker-menu/course-picker-menu-popover.ts diff --git a/src/addon/calendar/calendar.module.ts b/src/addon/calendar/calendar.module.ts index 38c4411d0..ac20c15e7 100644 --- a/src/addon/calendar/calendar.module.ts +++ b/src/addon/calendar/calendar.module.ts @@ -14,6 +14,7 @@ import { NgModule } from '@angular/core'; import { AddonCalendarProvider } from './providers/calendar'; +import { AddonCalendarHelperProvider } from './providers/helper'; import { AddonCalendarMainMenuHandler } from './providers/handlers'; import { CoreMainMenuDelegate } from '../../core/mainmenu/providers/delegate'; @@ -24,6 +25,7 @@ import { CoreMainMenuDelegate } from '../../core/mainmenu/providers/delegate'; ], providers: [ AddonCalendarProvider, + AddonCalendarHelperProvider, AddonCalendarMainMenuHandler ] }) diff --git a/src/addon/calendar/pages/list/list.html b/src/addon/calendar/pages/list/list.html index 6b26736f8..3ad253f5a 100644 --- a/src/addon/calendar/pages/list/list.html +++ b/src/addon/calendar/pages/list/list.html @@ -1,7 +1,37 @@ {{ 'addon.calendar.calendarevents' | translate }} + + + + + + + + + + + + + + + + + + + +

+

{{ event.timestart | coreToLocaleString }}

+
+
+ + + + +
diff --git a/src/addon/calendar/pages/list/list.module.ts b/src/addon/calendar/pages/list/list.module.ts index 44505a53f..9335a8061 100644 --- a/src/addon/calendar/pages/list/list.module.ts +++ b/src/addon/calendar/pages/list/list.module.ts @@ -15,6 +15,9 @@ import { NgModule } from '@angular/core'; import { IonicPageModule } 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 { AddonCalendarListPage } from './list'; @NgModule({ @@ -22,8 +25,11 @@ import { AddonCalendarListPage } from './list'; AddonCalendarListPage, ], imports: [ + CoreComponentsModule, + CoreDirectivesModule, + CorePipesModule, IonicPageModule.forChild(AddonCalendarListPage), TranslateModule.forChild() ], }) -export class AddonCalendarListPagePageModule {} +export class AddonCalendarListPageModule {} diff --git a/src/addon/calendar/pages/list/list.ts b/src/addon/calendar/pages/list/list.ts index 2b6bec034..5212688ea 100644 --- a/src/addon/calendar/pages/list/list.ts +++ b/src/addon/calendar/pages/list/list.ts @@ -12,12 +12,21 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnDestroy } from '@angular/core'; -import { IonicPage } from 'ionic-angular'; -//import { AddonCalendarProvider } from '../../providers/calendar'; +import { Component, ViewChild, OnDestroy } from '@angular/core'; +import { IonicPage, Content, PopoverController, NavParams, NavController } from 'ionic-angular'; +import { TranslateService } from '@ngx-translate/core'; +import { AddonCalendarProvider } from '../../providers/calendar'; +import { AddonCalendarHelperProvider } from '../../providers/helper'; +import { CoreCoursesProvider } from '../../../../core/courses/providers/courses'; +import { CoreDomUtilsProvider } from '../../../../providers/utils/dom'; +import { CoreUtilsProvider } from '../../../../providers/utils/utils'; +import { CoreSitesProvider } from '../../../../providers/sites'; +import { CoreLocalNotificationsProvider } from '../../../../providers/local-notifications'; +import { CoreCoursePickerMenuPopoverComponent } from '../../../../components/course-picker-menu/course-picker-menu-popover'; +import { CoreEventsProvider } from '../../../../providers/events'; /** - * Page that displays the list of courses the user is enrolled in. + * Page that displays the list of calendar events. */ @IonicPage() @Component({ @@ -25,20 +34,277 @@ import { IonicPage } from 'ionic-angular'; templateUrl: 'list.html', }) export class AddonCalendarListPage implements OnDestroy { - eventsLoaded = false; + @ViewChild(Content) content: Content; - constructor() {} + protected daysLoaded = 0; + protected emptyEventsTimes = 0; // Variable to identify consecutive calls returning 0 events. + protected categoriesRetrieved = false; + protected getCategories = false; + protected allCourses = { + id: -1, + fullname: this.translate.instant('core.fulllistofcourses'), + category: -1 + }; + protected categories = {}; + protected siteHomeId: number; + protected obsDefaultTimeChange: any; + + courses: any[]; + eventsLoaded = false; + events = []; + notificationsEnabled = false; + filteredEvents = []; + eventToLoad = 1; + canLoadMore = false; + filter = { + course: this.allCourses + }; + + constructor(private translate: TranslateService, private calendarProvider: AddonCalendarProvider, private navParams: NavParams, + private domUtils: CoreDomUtilsProvider, private coursesProvider: CoreCoursesProvider, private utils: CoreUtilsProvider, + private calendarHelper: AddonCalendarHelperProvider, private sitesProvider: CoreSitesProvider, + private localNotificationsProvider: CoreLocalNotificationsProvider, private popoverCtrl: PopoverController, + private eventsProvider: CoreEventsProvider, private navCtrl: NavController) { + + this.siteHomeId = sitesProvider.getCurrentSite().getSiteHomeId(); + this.notificationsEnabled = localNotificationsProvider.isAvailable(); + if (this.notificationsEnabled) { + // Re-schedule events if default time changes. + this.obsDefaultTimeChange = eventsProvider.on(AddonCalendarProvider.DEFAULT_NOTIFICATION_TIME_CHANGED, () => { + calendarProvider.scheduleEventsNotifications(this.events); + }); + } + + // @TODO: Split view once single event is done. + // let eventId = navParams.get('eventid') || false; + } /** * View loaded. */ ionViewDidLoad() { + this.fetchData().then(() => { + }).finally(() => { + this.eventsLoaded = true; + }); } + /** + * Fetch all the data required for the view. + * + * @param {boolean} refresh Empty events array first. + */ + fetchData(refresh = false) { + this.daysLoaded = 0; + this.emptyEventsTimes = 0; + + // Load courses for the popover. + return this.coursesProvider.getUserCourses(false).then((courses) => { + // Add "All courses". + courses.unshift(this.allCourses); + this.courses = courses; + return this.fetchEvents(refresh); + }); + } + + /** + * Fetches the events and updates the view. + * + * @param {boolean} refresh Empty events array first. + */ + fetchEvents(refresh = false) { + return this.calendarProvider.getEventsList(this.daysLoaded, AddonCalendarProvider.DAYS_INTERVAL).then((events) => { + this.daysLoaded += AddonCalendarProvider.DAYS_INTERVAL; + if (events.length === 0) { + this.emptyEventsTimes++; + if (this.emptyEventsTimes > 5) { // Stop execution if we retrieve empty list 6 consecutive times. + this.canLoadMore = false; + if (refresh) { + this.events = []; + this.filteredEvents = []; + } + } else { + // No events returned, load next events. + return this.fetchEvents(); + } + } else { + // Sort the events by timestart, they're ordered by id. + events.sort((a, b) => { + return a.timestart - b.timestart; + }); + + events.forEach(this.calendarHelper.formatEventData); + this.getCategories = this.shouldLoadCategories(events); + + if (refresh) { + this.events = events; + } else { + // Filter events with same ID. Repeated events are returned once per WS call, show them only once. + this.events = this.utils.mergeArraysWithoutDuplicates(this.events, events, 'id'); + } + this.filteredEvents = this.getFilteredEvents(); + this.canLoadMore = true; + + // Schedule notifications for the events retrieved (might have new events). + this.calendarProvider.scheduleEventsNotifications(this.events); + } + + // Resize the content so infinite loading is able to calculate if it should load more items or not. + // @TODO: Infinite loading is not working if content is not high enough. + this.content.resize(); + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); + this.canLoadMore = false; // Set to false to prevent infinite calls with infinite-loading. + }).then(() => { + // Success retrieving events. Get categories if needed. + if (this.getCategories) { + this.getCategories = false; + return this.loadCategories(); + } + }); + } + + /** + * Get filtered events. + */ + protected getFilteredEvents() { + if (this.filter.course.id == -1) { + // No filter, display everything. + return this.events; + } + + return this.events.filter(this.shouldDisplayEvent.bind(this)); + } + + /** + * Check if an event should be displayed based on the filter. + * + * @param {any} event Event object. + */ + protected shouldDisplayEvent(event: any) { + if (event.eventtype == 'user' || event.eventtype == 'site') { + // User or site event, display it. + return true; + } + + if (event.eventtype == 'category') { + if (!event.categoryid || !Object.keys(this.categories).length) { + // We can't tell if the course belongs to the category, display them all. + return true; + } + if (event.categoryid == this.filter.course.category) { + // The event is in the same category as the course, display it. + return true; + } + + // Check parent categories. + let category = this.categories[this.filter.course.category]; + while (category) { + if (!category.parent) { + // Category doesn't have parent, stop. + break; + } + + if (event.categoryid == category.parent) { + return true; + } + category = this.categories[category.parent]; + } + + return false; + } + + // Show the event if it is from site home or if it matches the selected course. + return event.courseid === this.siteHomeId || event.courseid == this.filter.course.id; + } + + /** + * Returns if the current state should load categories or not. + * @param {any[]} events Events to parse. + * @return {boolean} True if categories should be loaded. + */ + protected shouldLoadCategories(events: any[]) : boolean { + if (this.categoriesRetrieved || this.getCategories) { + // Use previous value + return this.getCategories; + } + + // Categories not loaded yet. We should get them if there's any category event. + let found = events.some(event => event.categoryid != 'undefined' && event.categoryid > 0); + return found || this.getCategories; + } + + /** + * Load categories to be able to filter events. + */ + protected loadCategories() { + return this.coursesProvider.getCategories(0, true).then((cats) => { + this.categoriesRetrieved = true; + this.categories = {}; + // Index categories by ID. + cats.forEach(function(category) { + this.categories[category.id] = category; + }); + }).catch(() => { + // Ignore errors. + }); + } + + /** + * Refresh the events. + * + * @param {any} refresher Refresher. + */ + refreshEvents(refresher: any) { + let promises = []; + + promises.push(this.calendarProvider.invalidateEventsList(this.courses)); + + if (this.categoriesRetrieved) { + promises.push(this.coursesProvider.invalidateCategories(0, true)); + this.categoriesRetrieved = false; + } + + Promise.all(promises).finally(() => { + this.fetchData(true).finally(() => { + refresher.complete(); + }); + }); + } + + /** + * Show the context menu. + * + * @param {MouseEvent} event Event. + */ + openCourseFilter(event: MouseEvent) : void { + let popover = this.popoverCtrl.create(CoreCoursePickerMenuPopoverComponent, {courses: this.courses, + courseId: this.filter.course.id}); + popover.onDidDismiss(course => { + if (course) { + this.filter.course = course; + this.content.scrollToTop(); + this.filteredEvents = this.getFilteredEvents(); + } + }); + popover.present({ + ev: event + }); + } + + /** + * Open calendar events settings. + */ + openSettings() { + // @TODO: Check the settings page name. + this.navCtrl.push('AddonCalendarSettingsPage'); + }; + /** * Page destroyed. */ ngOnDestroy() { + this.obsDefaultTimeChange && this.obsDefaultTimeChange.off && this.obsDefaultTimeChange.off(); } } \ No newline at end of file diff --git a/src/addon/calendar/providers/calendar.ts b/src/addon/calendar/providers/calendar.ts index 291232b9c..d1a8445ec 100644 --- a/src/addon/calendar/providers/calendar.ts +++ b/src/addon/calendar/providers/calendar.ts @@ -16,16 +16,253 @@ import { Injectable } from '@angular/core'; import { CoreLoggerProvider } from '../../../providers/logger'; import { CoreSitesProvider } from '../../../providers/sites'; import { CoreSite } from '../../../classes/site'; +import { CoreCoursesProvider } from '../../../core/courses/providers/courses'; +import { CoreTimeUtilsProvider } from '../../../providers/utils/time'; +import { CoreGroupsProvider } from '../../../providers/groups'; +import { CoreConstants } from '../../../core/constants'; +import { CoreLocalNotificationsProvider } from '../../../providers/local-notifications'; +import { CoreConfigProvider } from '../../../providers/config'; /** - * Service that provides some features regarding lists of courses and categories. + * Service to handle calendar events. */ @Injectable() export class AddonCalendarProvider { + public static DAYS_INTERVAL = 30; + public static DEFAULT_NOTIFICATION_TIME_CHANGED = 'AddonCalendarDefaultNotificationTimeChangedEvent'; + protected static DEFAULT_NOTIFICATION_TIME_SETTING = 'AddonCalendarDefaultNotifTime'; + protected static DEFAULT_NOTIFICATION_TIME = 60; + protected static COMPONENT = 'AddonCalendarEvents'; + + // Variables for database. + protected static EVENTS_TABLE = 'calendar_events'; // Queue of files to download. + protected static tablesSchema = [ + { + name: AddonCalendarProvider.EVENTS_TABLE, + columns: [ + { + name: 'id', + type: 'INTEGER', + primaryKey: true + }, + { + name: 'notificationtime', + type: 'INTEGER' + }, + { + name: 'name', + type: 'TEXT', + notNull: true + }, + { + name: 'description', + type: 'TEXT' + }, + { + name: 'eventtype', + type: 'TEXT' + }, + { + name: 'courseid', + type: 'INTEGER' + }, + { + name: 'timestart', + type: 'INTEGER' + }, + { + name: 'timeduration', + type: 'INTEGER' + }, + { + name: 'categoryid', + type: 'INTEGER' + }, + { + name: 'groupid', + type: 'INTEGER' + }, + { + name: 'instance', + type: 'INTEGER' + }, + { + name: 'modulename', + type: 'TEXT' + }, + { + name: 'timemodified', + type: 'INTEGER' + }, + { + name: 'repeatid', + type: 'INTEGER' + } + ] + } + ]; + protected logger; - constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider) { + constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private groupsProvider: CoreGroupsProvider, + private coursesProvider: CoreCoursesProvider, private timeUtils: CoreTimeUtilsProvider, + private localNotificationsProvider: CoreLocalNotificationsProvider, private configProvider: CoreConfigProvider) { this.logger = logger.getInstance('AddonCalendarProvider'); + this.sitesProvider.createTablesFromSchema(AddonCalendarProvider.tablesSchema); + } + + /** + * Get the configured default notification time. + * + * @param {string} [siteId] ID of the site. If not defined, use current site. + * @return {Promise} Promise resolved with the default time. + */ + getDefaultNotificationTime(siteId?: string) : Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + let key = AddonCalendarProvider.DEFAULT_NOTIFICATION_TIME_SETTING + '#' + siteId; + return this.configProvider.get(key, AddonCalendarProvider.DEFAULT_NOTIFICATION_TIME); + } + + /** + * Get a calendar event from local Db. + * + * @param {number} id Event ID. + * @param {string} [siteId] ID of the site the event belongs to. If not defined, use current site. + * @return {Promise} Promise resolved when the event data is retrieved. + */ + getEventFromLocalDb(id: number, siteId?: string) : Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().getRecord(AddonCalendarProvider.EVENTS_TABLE, {id: id}); + }); + } + + /** + * Get event notification time. Always returns number of minutes (0 if disabled). + * + * @param {number} id Event ID. + * @param {string} [siteId] ID of the site the event belongs to. If not defined, use current site. + * @return {Promise} Event notification time in minutes. 0 if disabled. + */ + getEventNotificationTime(id: number, siteId?: string) : Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + return this.getEventNotificationTimeOption(id, siteId).then((time: number) => { + if (time == -1) { + return this.getDefaultNotificationTime(siteId); + } + return time; + }); + } + + /** + * Get event notification time for options. Returns -1 for default time. + * + * @param {number} id Event ID. + * @param {string} [siteId] ID of the site the event belongs to. If not defined, use current site. + * @return {Promise} Promise with wvent notification time in minutes. 0 if disabled, -1 if default time. + */ + getEventNotificationTimeOption(id: number, siteId?: string) : Promise { + return this.getEventFromLocalDb(id, siteId).then((e) => { + return e.notificationtime || -1; + }).catch(() => { + return -1; + }); + } + + /** + * Get the events in a certain period. The period is calculated like this: + * start time: now + daysToStart + * end time: start time + daysInterval + * E.g. using provider.getEventsList(30, 30) is going to get the events starting after 30 days from now + * and ending before 60 days from now. + * + * @param {number} [daysToStart=0] Number of days from now to start getting events. + * @param {number} [daysInterval=30] Number of days between timestart and timeend. + * @param {string} [siteId] Site to get the events from. If not defined, use current site. + * @return {Promise} Promise to be resolved when the participants are retrieved. + */ + getEventsList(daysToStart = 0, daysInterval=AddonCalendarProvider.DAYS_INTERVAL, siteId?: string) : Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + siteId = site.getId(); + + return this.coursesProvider.getUserCourses(false, siteId).then((courses) => { + courses.push({id: site.getSiteHomeId()}); // Add front page. + + return this.groupsProvider.getUserGroups(courses, siteId).then((groups) => { + let now = this.timeUtils.timestamp(), + start = now + (CoreConstants.secondsDay * daysToStart), + end = start + (CoreConstants.secondsDay * daysInterval); + + // The core_calendar_get_calendar_events needs all the current user courses and groups. + let data = { + "options[userevents]": 1, + "options[siteevents]": 1, + "options[timestart]": start, + "options[timeend]": end + }; + + courses.forEach((course, index) => { + data["events[courseids][" + index + "]"] = course.id; + }); + + groups.forEach((group, index) => { + data["events[groupids][" + index + "]"] = group.id; + }); + + // We need to retrieve cached data using cache key because we have timestamp in the params. + let preSets = { + cacheKey: this.getEventsListCacheKey(daysToStart, daysInterval), + getCacheUsingCacheKey: true + }; + + return site.read('core_calendar_get_calendar_events', data, preSets).then((response) => { + this.storeEventsInLocalDB(response.events, siteId); + return response.events; + }); + }); + }); + }); + } + + /** + * Get cache key for events list WS calls. + * + * @param {number} daysToStart Number of days from now to start getting events. + * @param {number} daysInterval Number of days between timestart and timeend. + * @return {string} Cache key. + */ + protected getEventsListCacheKey(daysToStart: number, daysInterval: number) : string { + return this.getRootCacheKey() + 'eventslist:' + daysToStart + ':' + daysInterval; + } + + /** + * Get the root cache key for the WS calls related to this provider. + * + * @return {string} Root cache key. + */ + protected getRootCacheKey() : string { + return 'mmaCalendar:'; + } + + /** + * Invalidates events list and all the single events and related info. + * + * @param {any[]} courses List of courses or course ids. + * @param {string} [siteId] Site Id. If not defined, use current site. + * @return {Promise} Promise resolved when the list is invalidated. + */ + invalidateEventsList(courses: any[], siteId?: string) { + return this.sitesProvider.getSite(siteId).then((site) => { + siteId = site.getId(); + + let promises = []; + + promises.push(this.coursesProvider.invalidateUserCourses(siteId)); + promises.push(this.groupsProvider.invalidateUserGroups(courses, siteId)); + promises.push(site.invalidateWsCacheForKeyStartingWith(this.getRootCacheKey())); + return Promise.all(promises); + }); } /** @@ -39,4 +276,122 @@ export class AddonCalendarProvider { return site.isFeatureDisabled('$mmSideMenuDelegate_mmaCalendar'); } + /** + * Schedules an event notification. If time is 0, cancel scheduled notification if any. + * If local notification plugin is not enabled, resolve the promise. + * + * @param {any} event Event to schedule. + * @param {number} time Notification setting time (in minutes). E.g. 10 means "notificate 10 minutes before start". + * @param {string} [siteId] Site ID the event belongs to. If not defined, use current site. + * @return {Promise} Promise resolved when the notification is scheduled. + */ + scheduleEventNotification(event: any, time: number, siteId?: string) : Promise { + if (this.localNotificationsProvider.isAvailable()) { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + if (time === 0) { + // Cancel if it was scheduled. + return this.localNotificationsProvider.cancel(event.id, AddonCalendarProvider.COMPONENT, siteId); + } + + // If time is -1, get event default time. + let promise = time == -1 ? this.getDefaultNotificationTime(siteId) : Promise.resolve(time); + + return promise.then((time) => { + let timeend = (event.timestart + event.timeduration) * 1000; + if (timeend <= new Date().getTime()) { + // The event has finished already, don't schedule it. + return Promise.resolve(); + } + + let dateTriggered = new Date((event.timestart - (time * 60)) * 1000), + startDate = new Date(event.timestart * 1000), + notification = { + id: event.id, + title: event.name, + text: startDate.toLocaleString(), + at: dateTriggered, + data: { + eventid: event.id, + siteid: siteId + } + }; + + return this.localNotificationsProvider.schedule(notification, AddonCalendarProvider.COMPONENT, siteId); + }); + + } else { + return Promise.resolve(); + } + } + + + /** + * Schedules the notifications for a list of events. + * If an event notification time is 0, cancel its scheduled notification (if any). + * If local notification plugin is not enabled, resolve the promise. + * + * @param {any[]} events Events to schedule. + * @param {string} [siteId] ID of the site the events belong to. If not defined, use current site. + * @return {Promise} Promise resolved when all the notifications have been scheduled. + */ + scheduleEventsNotifications(events: any[], siteId?: string) : Promise { + var promises = []; + + if (this.localNotificationsProvider.isAvailable()) { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + events.forEach((e) => { + promises.push(this.getEventNotificationTime(e.id, siteId).then((time) => { + return this.scheduleEventNotification(e, time, siteId); + })); + }); + } + + return Promise.all(promises); + } + + /** + * Store events in local DB. + * + * @param {any[]} events Events to store. + * @param {string} [siteId] ID of the site the event belongs to. If not defined, use current site. + * @return {Promise} Promise resolved when the events are stored. + */ + protected storeEventsInLocalDB(events: any[], siteId?: string) : Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + siteId = site.getId(); + + let promises = [], + db = site.getDb(); + + events.forEach((event) => { + // Don't override event notification time if the user configured it. + promises.push(this.getEventFromLocalDb(event.id, siteId).catch(() => { + // Event not stored, return empty object. + return {}; + }).then((e) => { + let eventRecord = { + id: event.id, + name: event.name, + description: event.description, + eventtype: event.eventtype, + courseid: event.courseid, + timestart: event.timestart, + timeduration: event.timeduration, + categoryid: event.categoryid, + groupid: event.groupid, + instance: event.instance, + modulename: event.modulename, + timemodified: event.timemodified, + repeatid: event.repeatid, + notificationtime: e.notificationtime || -1 + }; + + return db.insertOrUpdateRecord(AddonCalendarProvider.EVENTS_TABLE, eventRecord, {id: eventRecord.id}); + })); + }); + + return Promise.all(promises); + }); + } } diff --git a/src/addon/calendar/providers/helper.ts b/src/addon/calendar/providers/helper.ts new file mode 100644 index 000000000..f8ec7450b --- /dev/null +++ b/src/addon/calendar/providers/helper.ts @@ -0,0 +1,53 @@ +// (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 { CoreLoggerProvider } from '../../../providers/logger'; +import { CoreSitesProvider } from '../../../providers/sites'; +//import { CoreCourseProvider } from '../../../core/course/providers/course'; + +/** + * Service that provides some features regarding lists of courses and categories. + */ +@Injectable() +export class AddonCalendarHelperProvider { + protected logger; + + private static eventicons = { + 'course': 'ionic', + 'group': 'people', + 'site': 'globe', + 'user': 'person', + 'category': 'albums' + }; + + constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider) { + this.logger = logger.getInstance('AddonCalendarHelperProvider'); + } + + /** + * Convenience function to format some event data to be rendered. + * + * @param {any} e Event to format. + */ + formatEventData(e: any) { + let icon = AddonCalendarHelperProvider.eventicons[e.eventtype] || false; + if (!icon) { + // @TODO: It's a module event. + //icon = this.courseProvider.getModuleIconSrc(e.modulename); + e.moduleicon = icon; + } + e.icon = icon; + }; +} diff --git a/src/classes/site.ts b/src/classes/site.ts index b4645cfeb..fea101fa3 100644 --- a/src/classes/site.ts +++ b/src/classes/site.ts @@ -802,8 +802,8 @@ export class CoreSite { } this.logger.debug('Invalidate cache for key starting with: ' + key); - let sql = 'UPDATE ' + this.WS_CACHE_TABLE + ' SET expirationTime=0 WHERE key LIKE ?%'; - return this.db.execute(sql, [key]); + let sql = 'UPDATE ' + this.WS_CACHE_TABLE + ' SET expirationTime=0 WHERE key LIKE ?'; + return this.db.execute(sql, [key + "%"]); } /** diff --git a/src/components/components.module.ts b/src/components/components.module.ts index a3d1ec8ba..6a37a8dae 100644 --- a/src/components/components.module.ts +++ b/src/components/components.module.ts @@ -29,6 +29,7 @@ import { CoreFileComponent } from './file/file'; import { CoreContextMenuComponent } from './context-menu/context-menu'; import { CoreContextMenuItemComponent } from './context-menu/context-menu-item'; import { CoreContextMenuPopoverComponent } from './context-menu/context-menu-popover'; +import { CoreCoursePickerMenuPopoverComponent } from './course-picker-menu/course-picker-menu-popover'; import { CoreChronoComponent } from './chrono/chrono'; import { CoreLocalFileComponent } from './local-file/local-file'; import { CoreSitePickerComponent } from './site-picker/site-picker'; @@ -47,12 +48,14 @@ import { CoreSitePickerComponent } from './site-picker/site-picker'; CoreContextMenuComponent, CoreContextMenuItemComponent, CoreContextMenuPopoverComponent, + CoreCoursePickerMenuPopoverComponent, CoreChronoComponent, CoreLocalFileComponent, CoreSitePickerComponent ], entryComponents: [ - CoreContextMenuPopoverComponent + CoreContextMenuPopoverComponent, + CoreCoursePickerMenuPopoverComponent ], imports: [ IonicModule, diff --git a/src/components/course-picker-menu/course-picker-menu-popover.html b/src/components/course-picker-menu/course-picker-menu-popover.html new file mode 100644 index 000000000..6eea11d5e --- /dev/null +++ b/src/components/course-picker-menu/course-picker-menu-popover.html @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/components/course-picker-menu/course-picker-menu-popover.ts b/src/components/course-picker-menu/course-picker-menu-popover.ts new file mode 100644 index 000000000..e654664dc --- /dev/null +++ b/src/components/course-picker-menu/course-picker-menu-popover.ts @@ -0,0 +1,45 @@ +// (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 { NavParams, ViewController } from 'ionic-angular'; + +/** + * Component to display a list of courses. + */ +@Component({ + selector: 'core-course-picker-menu-popover', + templateUrl: 'course-picker-menu-popover.html' +}) +export class CoreCoursePickerMenuPopoverComponent { + courses: any[]; + courseId = -1; + + constructor(private navParams: NavParams, private viewCtrl: ViewController) { + this.courses = navParams.get('courses') || []; + this.courseId = navParams.get('courseId') || -1; + } + + /** + * Function called when a course is clicked. + * + * @param {Event} event Click event. + * @param {any} course Course object clicked. + * @return {boolean} Return true if success, false if error. + */ + coursePicked(event: Event, course: any) : boolean { + this.viewCtrl.dismiss(course); + return true; + } +} diff --git a/src/providers/sites.ts b/src/providers/sites.ts index c3fbb5ed3..aaba7ee3d 100644 --- a/src/providers/sites.ts +++ b/src/providers/sites.ts @@ -739,7 +739,7 @@ export class CoreSitesProvider { * @param {number} [siteId] The site ID. If not defined, current site (if available). * @return {Promise} Promise resolved with site home ID. */ - getSiteHomeId(siteId: string) : Promise { + getSiteHomeId(siteId?: string) : Promise { return this.getSite(siteId).then((site) => { return site.getSiteHomeId(); });