From 98776a9c781edbcdd5477c76c2b97a1fdef07213 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 21 Jun 2019 15:22:05 +0200 Subject: [PATCH] MOBILE-1927 calendar: Implement events synchronization --- scripts/langindex.json | 21 ++ src/addon/calendar/calendar.module.ts | 15 +- src/addon/calendar/lang/en.json | 1 + .../calendar/pages/edit-event/edit-event.ts | 87 ++++--- src/addon/calendar/pages/list/list.html | 3 +- src/addon/calendar/pages/list/list.ts | 164 +++++++++---- .../calendar/providers/calendar-offline.ts | 1 - src/addon/calendar/providers/calendar-sync.ts | 230 ++++++++++++++++++ src/addon/calendar/providers/helper.ts | 25 +- .../calendar/providers/sync-cron-handler.ts | 48 ++++ src/assets/lang/en.json | 2 +- src/lang/en.json | 1 - src/providers/utils/time.ts | 17 +- 13 files changed, 536 insertions(+), 79 deletions(-) create mode 100644 src/addon/calendar/providers/calendar-sync.ts create mode 100644 src/addon/calendar/providers/sync-cron-handler.ts diff --git a/scripts/langindex.json b/scripts/langindex.json index 5b005c6fb..dc86f5c89 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -84,16 +84,30 @@ "addon.blog.showonlyyourentries": "local_moodlemobileapp", "addon.blog.siteblogheading": "blog", "addon.calendar.calendar": "calendar", + "addon.calendar.calendarevent": "local_moodlemobileapp", "addon.calendar.calendarevents": "local_moodlemobileapp", "addon.calendar.calendarreminders": "local_moodlemobileapp", "addon.calendar.defaultnotificationtime": "local_moodlemobileapp", + "addon.calendar.durationminutes": "calendar", + "addon.calendar.durationnone": "calendar", + "addon.calendar.durationuntil": "calendar", + "addon.calendar.editevent": "calendar", "addon.calendar.errorloadevent": "local_moodlemobileapp", "addon.calendar.errorloadevents": "local_moodlemobileapp", + "addon.calendar.eventduration": "calendar", "addon.calendar.eventendtime": "calendar", + "addon.calendar.eventname": "calendar", "addon.calendar.eventstarttime": "calendar", + "addon.calendar.eventtype": "calendar", "addon.calendar.gotoactivity": "calendar", + "addon.calendar.invalidtimedurationminutes": "calendar", + "addon.calendar.invalidtimedurationuntil": "calendar", + "addon.calendar.newevent": "calendar", "addon.calendar.noevents": "local_moodlemobileapp", + "addon.calendar.nopermissiontoupdatecalendar": "error", "addon.calendar.reminders": "local_moodlemobileapp", + "addon.calendar.repeatevent": "calendar", + "addon.calendar.repeatweeksl": "calendar", "addon.calendar.setnewreminder": "local_moodlemobileapp", "addon.calendar.typecategory": "calendar", "addon.calendar.typeclose": "calendar", @@ -1328,6 +1342,7 @@ "core.course.warningmanualcompletionmodified": "local_moodlemobileapp", "core.course.warningofflinemanualcompletiondeleted": "local_moodlemobileapp", "core.coursedetails": "moodle", + "core.coursenogroups": "local_moodlemobileapp", "core.courses.addtofavourites": "block_myoverview", "core.courses.allowguests": "enrol_guest", "core.courses.availablecourses": "moodle", @@ -1457,6 +1472,7 @@ "core.grades.range": "grades", "core.grades.rank": "grades", "core.grades.weight": "grades", + "core.group": "moodle", "core.groupsseparate": "moodle", "core.groupsvisible": "moodle", "core.hasdatatosync": "local_moodlemobileapp", @@ -1692,6 +1708,9 @@ "core.sec": "moodle", "core.secs": "moodle", "core.seemoredetail": "survey", + "core.selectacategory": "moodle", + "core.selectacourse": "moodle", + "core.selectagroup": "moodle", "core.send": "message", "core.sending": "chat", "core.serverconnection": "error", @@ -1760,6 +1779,7 @@ "core.sharedfiles.sharedfiles": "local_moodlemobileapp", "core.sharedfiles.successstorefile": "local_moodlemobileapp", "core.show": "moodle", + "core.showless": "form", "core.showmore": "form", "core.site": "moodle", "core.sitehome.sitehome": "moodle", @@ -1808,6 +1828,7 @@ "core.unlimited": "moodle", "core.unzipping": "local_moodlemobileapp", "core.upgraderunning": "error", + "core.user": "moodle", "core.user.address": "moodle", "core.user.city": "moodle", "core.user.contact": "local_moodlemobileapp", diff --git a/src/addon/calendar/calendar.module.ts b/src/addon/calendar/calendar.module.ts index c8131878a..c8f9cef82 100644 --- a/src/addon/calendar/calendar.module.ts +++ b/src/addon/calendar/calendar.module.ts @@ -16,8 +16,11 @@ import { NgModule } from '@angular/core'; import { AddonCalendarProvider } from './providers/calendar'; import { AddonCalendarOfflineProvider } from './providers/calendar-offline'; import { AddonCalendarHelperProvider } from './providers/helper'; +import { AddonCalendarSyncProvider } from './providers/calendar-sync'; import { AddonCalendarMainMenuHandler } from './providers/mainmenu-handler'; +import { AddonCalendarSyncCronHandler } from './providers/sync-cron-handler'; import { CoreMainMenuDelegate } from '@core/mainmenu/providers/delegate'; +import { CoreCronDelegate } from '@providers/cron'; import { CoreInitDelegate } from '@providers/init'; import { CoreLocalNotificationsProvider } from '@providers/local-notifications'; import { CoreLoginHelperProvider } from '@core/login/providers/helper'; @@ -27,7 +30,8 @@ import { CoreUpdateManagerProvider } from '@providers/update-manager'; export const ADDON_CALENDAR_PROVIDERS: any[] = [ AddonCalendarProvider, AddonCalendarOfflineProvider, - AddonCalendarHelperProvider + AddonCalendarHelperProvider, + AddonCalendarSyncProvider ]; @NgModule({ @@ -39,14 +43,19 @@ export const ADDON_CALENDAR_PROVIDERS: any[] = [ AddonCalendarProvider, AddonCalendarOfflineProvider, AddonCalendarHelperProvider, - AddonCalendarMainMenuHandler + AddonCalendarSyncProvider, + AddonCalendarMainMenuHandler, + AddonCalendarSyncCronHandler ] }) export class AddonCalendarModule { constructor(mainMenuDelegate: CoreMainMenuDelegate, calendarHandler: AddonCalendarMainMenuHandler, initDelegate: CoreInitDelegate, calendarProvider: AddonCalendarProvider, loginHelper: CoreLoginHelperProvider, - localNotificationsProvider: CoreLocalNotificationsProvider, updateManager: CoreUpdateManagerProvider) { + localNotificationsProvider: CoreLocalNotificationsProvider, updateManager: CoreUpdateManagerProvider, + cronDelegate: CoreCronDelegate, syncHandler: AddonCalendarSyncCronHandler) { + mainMenuDelegate.registerHandler(calendarHandler); + cronDelegate.register(syncHandler); initDelegate.ready().then(() => { calendarProvider.scheduleAllSitesEventsNotifications(); diff --git a/src/addon/calendar/lang/en.json b/src/addon/calendar/lang/en.json index 3139bb8a9..a7a531836 100644 --- a/src/addon/calendar/lang/en.json +++ b/src/addon/calendar/lang/en.json @@ -1,5 +1,6 @@ { "calendar": "Calendar", + "calendarevent": "Calendar event", "calendarevents": "Calendar events", "calendarreminders": "Calendar reminders", "defaultnotificationtime": "Default notification time", diff --git a/src/addon/calendar/pages/edit-event/edit-event.ts b/src/addon/calendar/pages/edit-event/edit-event.ts index d6a6ec488..bf394c205 100644 --- a/src/addon/calendar/pages/edit-event/edit-event.ts +++ b/src/addon/calendar/pages/edit-event/edit-event.ts @@ -12,13 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnInit, Optional, ViewChild } from '@angular/core'; +import { Component, OnInit, OnDestroy, Optional, ViewChild } from '@angular/core'; import { FormControl, FormGroup, FormBuilder, Validators } from '@angular/forms'; import { IonicPage, NavController, NavParams } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; import { CoreEventsProvider } from '@providers/events'; import { CoreGroupsProvider } from '@providers/groups'; import { CoreSitesProvider } from '@providers/sites'; +import { CoreSyncProvider } from '@providers/sync'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreUtilsProvider } from '@providers/utils/utils'; @@ -28,6 +29,7 @@ import { CoreRichTextEditorComponent } from '@components/rich-text-editor/rich-t import { AddonCalendarProvider } from '../../providers/calendar'; import { AddonCalendarOfflineProvider } from '../../providers/calendar-offline'; import { AddonCalendarHelperProvider } from '../../providers/helper'; +import { AddonCalendarSyncProvider } from '../../providers/calendar-sync'; import { CoreSite } from '@classes/site'; /** @@ -38,7 +40,7 @@ import { CoreSite } from '@classes/site'; selector: 'page-addon-calendar-edit-event', templateUrl: 'edit-event.html', }) -export class AddonCalendarEditEventPage implements OnInit { +export class AddonCalendarEditEventPage implements OnInit, OnDestroy { @ViewChild(CoreRichTextEditorComponent) descriptionEditor: CoreRichTextEditorComponent; @@ -68,6 +70,7 @@ export class AddonCalendarEditEventPage implements OnInit { protected currentSite: CoreSite; protected types: any; // Object with the supported types. protected showAll: boolean; + protected isDestroyed = false; constructor(navParams: NavParams, private navCtrl: NavController, @@ -82,7 +85,9 @@ export class AddonCalendarEditEventPage implements OnInit { private calendarProvider: AddonCalendarProvider, private calendarOffline: AddonCalendarOfflineProvider, private calendarHelper: AddonCalendarHelperProvider, + private calendarSync: AddonCalendarSyncProvider, private fb: FormBuilder, + private syncProvider: CoreSyncProvider, @Optional() private svComponent: CoreSplitViewComponent) { this.eventId = navParams.get('eventId'); @@ -142,10 +147,10 @@ export class AddonCalendarEditEventPage implements OnInit { let accessInfo; // Get access info. - return this.calendarProvider.getAccessInformation().then((info) => { + return this.calendarProvider.getAccessInformation(this.courseId).then((info) => { accessInfo = info; - return this.calendarProvider.getAllowedEventTypes(); + return this.calendarProvider.getAllowedEventTypes(this.courseId); }).then((types) => { this.types = types; @@ -157,29 +162,38 @@ export class AddonCalendarEditEventPage implements OnInit { } if (this.eventId && !refresh) { - // Get the event data if there's any. - promises.push(this.calendarOffline.getEvent(this.eventId).then((event) => { - this.hasOffline = true; + // If editing an event, get offline data. Wait for sync first. - // Load the data in the form. - this.eventForm.controls.name.setValue(event.name); - this.eventForm.controls.timestart.setValue(this.timeUtils.toDatetimeFormat(event.timestart * 1000)); - this.eventForm.controls.eventtype.setValue(event.eventtype); - this.eventForm.controls.categoryid.setValue(event.categoryid || ''); - this.eventForm.controls.courseid.setValue(event.courseid || ''); - this.eventForm.controls.groupcourseid.setValue(event.groupcourseid || ''); - this.eventForm.controls.groupid.setValue(event.groupid || ''); - this.eventForm.controls.description.setValue(event.description); - this.eventForm.controls.location.setValue(event.location); - this.eventForm.controls.duration.setValue(event.duration); - this.eventForm.controls.timedurationuntil.setValue( - this.timeUtils.toDatetimeFormat((event.timedurationuntil * 1000) || Date.now())); - this.eventForm.controls.timedurationminutes.setValue(event.timedurationminutes || ''); - this.eventForm.controls.repeat.setValue(!!event.repeat); - this.eventForm.controls.repeats.setValue(event.repeats || '1'); - }).catch(() => { - // No offline data. - this.hasOffline = false; + promises.push(this.calendarSync.waitForSync(AddonCalendarSyncProvider.SYNC_ID).then(() => { + // Do not block if the scope is already destroyed. + if (!this.isDestroyed) { + this.syncProvider.blockOperation(AddonCalendarProvider.COMPONENT, this.eventId); + } + + // Get the event data if there's any. + return this.calendarOffline.getEvent(this.eventId).then((event) => { + this.hasOffline = true; + + // Load the data in the form. + this.eventForm.controls.name.setValue(event.name); + this.eventForm.controls.timestart.setValue(this.timeUtils.toDatetimeFormat(event.timestart * 1000)); + this.eventForm.controls.eventtype.setValue(event.eventtype); + this.eventForm.controls.categoryid.setValue(event.categoryid || ''); + this.eventForm.controls.courseid.setValue(event.courseid || ''); + this.eventForm.controls.groupcourseid.setValue(event.groupcourseid || ''); + this.eventForm.controls.groupid.setValue(event.groupid || ''); + this.eventForm.controls.description.setValue(event.description); + this.eventForm.controls.location.setValue(event.location); + this.eventForm.controls.duration.setValue(event.duration); + this.eventForm.controls.timedurationuntil.setValue( + this.timeUtils.toDatetimeFormat((event.timedurationuntil * 1000) || Date.now())); + this.eventForm.controls.timedurationminutes.setValue(event.timedurationminutes || ''); + this.eventForm.controls.repeat.setValue(!!event.repeat); + this.eventForm.controls.repeats.setValue(event.repeats || '1'); + }).catch(() => { + // No offline data. + this.hasOffline = false; + }); })); } @@ -305,8 +319,8 @@ export class AddonCalendarEditEventPage implements OnInit { submit(): void { // Validate data. const formData = this.eventForm.value, - timeStartDate = new Date(formData.timestart), - timeUntilDate = new Date(formData.timedurationuntil), + timeStartDate = this.timeUtils.datetimeToDate(formData.timestart), + timeUntilDate = this.timeUtils.datetimeToDate(formData.timedurationuntil), timeDurationMinutes = parseInt(formData.timedurationminutes || '', 10); let error; @@ -382,6 +396,9 @@ export class AddonCalendarEditEventPage implements OnInit { * @param {number} [event] Event. */ protected returnToList(event?: any): void { + // Unblock the sync because the view will be destroyed and the sync process could be triggered before ngOnDestroy. + this.unblockSync(); + if (event) { const data: any = { event: event @@ -432,4 +449,18 @@ export class AddonCalendarEditEventPage implements OnInit { return Promise.resolve(); } } + + protected unblockSync(): void { + if (this.eventId) { + this.syncProvider.unblockOperation(AddonCalendarProvider.COMPONENT, this.eventId); + } + } + + /** + * Page destroyed. + */ + ngOnDestroy(): void { + this.unblockSync(); + this.isDestroyed = true; + } } diff --git a/src/addon/calendar/pages/list/list.html b/src/addon/calendar/pages/list/list.html index 50a57e777..0204b9acf 100644 --- a/src/addon/calendar/pages/list/list.html +++ b/src/addon/calendar/pages/list/list.html @@ -7,13 +7,14 @@ + - + diff --git a/src/addon/calendar/pages/list/list.ts b/src/addon/calendar/pages/list/list.ts index d799f64c5..1a57c4c0b 100644 --- a/src/addon/calendar/pages/list/list.ts +++ b/src/addon/calendar/pages/list/list.ts @@ -12,12 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, ViewChild, OnDestroy } from '@angular/core'; +import { Component, ViewChild, OnDestroy, NgZone } from '@angular/core'; import { IonicPage, Content, PopoverController, NavParams, NavController } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; import { AddonCalendarProvider } from '../../providers/calendar'; import { AddonCalendarOfflineProvider } from '../../providers/calendar-offline'; import { AddonCalendarHelperProvider } from '../../providers/helper'; +import { AddonCalendarSyncProvider } from '../../providers/calendar-sync'; import { CoreCoursesProvider } from '@core/courses/providers/courses'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreUtilsProvider } from '@providers/utils/utils'; @@ -28,6 +29,7 @@ import { CoreEventsProvider } from '@providers/events'; import { CoreAppProvider } from '@providers/app'; import { CoreSplitViewComponent } from '@components/split-view/split-view'; import * as moment from 'moment'; +import { Network } from '@ionic-native/network'; /** * Page that displays the list of calendar events. @@ -57,6 +59,8 @@ export class AddonCalendarListPage implements OnDestroy { protected preSelectedCourseId: number; protected newEventObserver: any; protected discardedObserver: any; + protected syncObserver: any; + protected onlineObserver: any; courses: any[]; eventsLoaded = false; @@ -71,13 +75,16 @@ export class AddonCalendarListPage implements OnDestroy { }; canCreate = false; hasOffline = false; + isOnline = false; + syncIcon: string; // Sync icon. constructor(private translate: TranslateService, private calendarProvider: AddonCalendarProvider, navParams: NavParams, private domUtils: CoreDomUtilsProvider, private coursesProvider: CoreCoursesProvider, private utils: CoreUtilsProvider, - private calendarHelper: AddonCalendarHelperProvider, sitesProvider: CoreSitesProvider, + private calendarHelper: AddonCalendarHelperProvider, private sitesProvider: CoreSitesProvider, zone: NgZone, localNotificationsProvider: CoreLocalNotificationsProvider, private popoverCtrl: PopoverController, - eventsProvider: CoreEventsProvider, private navCtrl: NavController, appProvider: CoreAppProvider, - private calendarOffline: AddonCalendarOfflineProvider) { + private eventsProvider: CoreEventsProvider, private navCtrl: NavController, private appProvider: CoreAppProvider, + private calendarOffline: AddonCalendarOfflineProvider, private calendarSync: AddonCalendarSyncProvider, + network: Network) { this.siteHomeId = sitesProvider.getCurrentSite().getSiteHomeId(); this.notificationsEnabled = localNotificationsProvider.isAvailable(); @@ -101,7 +108,7 @@ export class AddonCalendarListPage implements OnDestroy { } this.eventsLoaded = false; - this.refreshEvents(false).finally(() => { + this.refreshEvents(true, false).finally(() => { // In tablet mode try to open the event (only if it's an online event). if (this.splitviewCtrl.isOn() && data.event.id > 0) { @@ -119,8 +126,22 @@ export class AddonCalendarListPage implements OnDestroy { } this.eventsLoaded = false; - this.refreshEvents(false); + this.refreshEvents(true, false); }, sitesProvider.getCurrentSiteId()); + + // Refresh data if calendar events are synchronized automatically. + this.syncObserver = eventsProvider.on(AddonCalendarSyncProvider.AUTO_SYNCED, (data) => { + this.eventsLoaded = false; + this.refreshEvents(); + }, sitesProvider.getCurrentSiteId()); + + // Refresh online status when changes. + this.onlineObserver = network.onchange().subscribe((online) => { + // Execute the callback in the Angular zone, so change detection doesn't stop working. + zone.run(() => { + this.isOnline = online; + }); + }); } /** @@ -132,7 +153,9 @@ export class AddonCalendarListPage implements OnDestroy { this.gotoEvent(this.eventId); } - this.fetchData().then(() => { + this.syncIcon = 'spinner'; + + this.fetchData(false, true, false).then(() => { if (!this.eventId && this.splitviewCtrl.isOn() && this.events.length > 0) { // Take first and load it. this.gotoEvent(this.events[0].id); @@ -144,49 +167,76 @@ export class AddonCalendarListPage implements OnDestroy { * Fetch all the data required for the view. * * @param {boolean} [refresh] Empty events array first. + * @param {boolean} [sync] Whether it should try to synchronize offline events. + * @param {boolean} [showErrors] Whether to show sync errors to the user. * @return {Promise} Promise resolved when done. */ - fetchData(refresh: boolean = false): Promise { + fetchData(refresh?: boolean, sync?: boolean, showErrors?: boolean): Promise { this.daysLoaded = 0; this.emptyEventsTimes = 0; + this.isOnline = this.appProvider.isOnline(); - const promises = []; + let promise; - if (this.calendarProvider.canEditEventsInSite()) { - // Site allows creating events. Check if the user has permissions to do so. - promises.push(this.calendarProvider.getAllowedEventTypes().then((types) => { - this.canCreate = Object.keys(types).length > 0; - }).catch(() => { - this.canCreate = false; - })); + if (sync) { + // Try to synchronize offline events. + promise = this.calendarSync.syncEvents().then((result) => { + if (result.warnings && result.warnings.length) { + this.domUtils.showErrorModal(result.warnings[0]); + } + + if (result.updated) { + // Trigger a manual sync event. + this.eventsProvider.trigger(AddonCalendarSyncProvider.MANUAL_SYNCED, { + source: 'list' + }, this.sitesProvider.getCurrentSiteId()); + } + }).catch((error) => { + if (showErrors) { + this.domUtils.showErrorModalDefault(error, 'core.errorsync', true); + } + }); + } else { + promise = Promise.resolve(); } - // Load courses for the popover. - promises.push(this.coursesProvider.getUserCourses(false).then((courses) => { - // Add "All courses". - courses.unshift(this.allCourses); - this.courses = courses; + return promise.then(() => { - if (this.preSelectedCourseId) { - this.filter.course = courses.find((course) => { - return course.id == this.preSelectedCourseId; - }); - } + const promises = []; + const courseId = this.filter.course.id != this.allCourses.id ? this.filter.course.id : undefined; - return this.fetchEvents(refresh); - })); + promises.push(this.calendarHelper.canEditEvents(courseId).then((canEdit) => { + this.canCreate = canEdit; + })); - // Get offline events. - promises.push(this.calendarOffline.getAllEvents().then((events) => { - this.hasOffline = !!events.length; + // Load courses for the popover. + promises.push(this.coursesProvider.getUserCourses(false).then((courses) => { + // Add "All courses". + courses.unshift(this.allCourses); + this.courses = courses; - // Format data and sort by timestart. - events.forEach(this.calendarHelper.formatEventData.bind(this.calendarHelper)); - this.offlineEvents = events.sort((a, b) => a.timestart - b.timestart); - })); + if (this.preSelectedCourseId) { + this.filter.course = courses.find((course) => { + return course.id == this.preSelectedCourseId; + }); + } - return Promise.all(promises).finally(() => { + return this.fetchEvents(refresh); + })); + + // Get offline events. + promises.push(this.calendarOffline.getAllEvents().then((events) => { + this.hasOffline = !!events.length; + + // Format data and sort by timestart. + events.forEach(this.calendarHelper.formatEventData.bind(this.calendarHelper)); + this.offlineEvents = events.sort((a, b) => a.timestart - b.timestart); + })); + + return Promise.all(promises); + }).finally(() => { this.eventsLoaded = true; + this.syncIcon = 'sync'; }); } @@ -196,7 +246,7 @@ export class AddonCalendarListPage implements OnDestroy { * @param {boolean} [refresh] Empty events array first. * @return {Promise} Promise resolved when done. */ - fetchEvents(refresh: boolean = false): Promise { + fetchEvents(refresh?: boolean): Promise { this.loadMoreError = false; return this.calendarProvider.getEventsList(this.daysLoaded, AddonCalendarProvider.DAYS_INTERVAL).then((events) => { @@ -367,12 +417,34 @@ export class AddonCalendarListPage implements OnDestroy { } /** - * Refresh the events. + * Refresh the data. * * @param {any} [refresher] Refresher. + * @param {Function} [done] Function to call when done. + * @param {boolean} [showErrors] Whether to show sync errors to the user. * @return {Promise} Promise resolved when done. */ - refreshEvents(refresher?: any): Promise { + doRefresh(refresher?: any, done?: () => void, showErrors?: boolean): Promise { + if (this.eventsLoaded) { + return this.refreshEvents(true, showErrors).finally(() => { + refresher && refresher.complete(); + done && done(); + }); + } + + return Promise.resolve(); + } + + /** + * Refresh the events. + * + * @param {boolean} [sync] Whether it should try to synchronize offline events. + * @param {boolean} [showErrors] Whether to show sync errors to the user. + * @return {Promise} Promise resolved when done. + */ + refreshEvents(sync?: boolean, showErrors?: boolean): Promise { + this.syncIcon = 'spinner'; + const promises = []; promises.push(this.calendarProvider.invalidateEventsList()); @@ -384,9 +456,7 @@ export class AddonCalendarListPage implements OnDestroy { } return Promise.all(promises).finally(() => { - return this.fetchData(true).finally(() => { - refresher && refresher.complete(); - }); + return this.fetchData(true, sync, showErrors); }); } @@ -440,6 +510,13 @@ export class AddonCalendarListPage implements OnDestroy { this.domUtils.scrollToTop(this.content); this.filteredEvents = this.getFilteredEvents(); + + // Course viewed has changed, check if the user can create events for this course calendar. + const courseId = this.filter.course.id != this.allCourses.id ? this.filter.course.id : undefined; + + this.calendarHelper.canEditEvents(courseId).then((canEdit) => { + this.canCreate = canEdit; + }); } }); popover.present({ @@ -496,5 +573,8 @@ export class AddonCalendarListPage implements OnDestroy { ngOnDestroy(): void { this.obsDefaultTimeChange && this.obsDefaultTimeChange.off(); this.newEventObserver && this.newEventObserver.off(); + this.discardedObserver && this.discardedObserver.off(); + this.syncObserver && this.syncObserver.off(); + this.onlineObserver && this.onlineObserver.off(); } } diff --git a/src/addon/calendar/providers/calendar-offline.ts b/src/addon/calendar/providers/calendar-offline.ts index c720215bc..833ca7d4b 100644 --- a/src/addon/calendar/providers/calendar-offline.ts +++ b/src/addon/calendar/providers/calendar-offline.ts @@ -14,7 +14,6 @@ import { Injectable } from '@angular/core'; import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites'; -import { AddonCalendarProvider } from './calendar'; /** * Service to handle offline calendar events. diff --git a/src/addon/calendar/providers/calendar-sync.ts b/src/addon/calendar/providers/calendar-sync.ts new file mode 100644 index 000000000..cb9eb4b63 --- /dev/null +++ b/src/addon/calendar/providers/calendar-sync.ts @@ -0,0 +1,230 @@ +// (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 { TranslateService } from '@ngx-translate/core'; +import { CoreSyncBaseProvider } from '@classes/base-sync'; +import { CoreCourseProvider } from '@core/course/providers/course'; +import { CoreAppProvider } from '@providers/app'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreSyncProvider } from '@providers/sync'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreTimeUtilsProvider } from '@providers/utils/time'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { AddonCalendarProvider } from './calendar'; +import { AddonCalendarOfflineProvider } from './calendar-offline'; + +/** + * Service to sync calendar. + */ +@Injectable() +export class AddonCalendarSyncProvider extends CoreSyncBaseProvider { + + static AUTO_SYNCED = 'addon_calendar_autom_synced'; + static MANUAL_SYNCED = 'addon_calendar_manual_synced'; + static SYNC_ID = 'calendar'; + + protected componentTranslate: string; + + constructor(translate: TranslateService, + appProvider: CoreAppProvider, + courseProvider: CoreCourseProvider, + private eventsProvider: CoreEventsProvider, + loggerProvider: CoreLoggerProvider, + sitesProvider: CoreSitesProvider, + syncProvider: CoreSyncProvider, + textUtils: CoreTextUtilsProvider, + timeUtils: CoreTimeUtilsProvider, + private utils: CoreUtilsProvider, + private calendarProvider: AddonCalendarProvider, + private calendarOffline: AddonCalendarOfflineProvider) { + + super('AddonCalendarSyncProvider', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate, + timeUtils); + + this.componentTranslate = this.translate.instant('addon.calendar.calendarevent'); + } + + /** + * Try to synchronize all events in a certain site or in all sites. + * + * @param {string} [siteId] Site ID to sync. If not defined, sync all sites. + * @param {boolean} [force] Wether to force sync not depending on last execution. + * @return {Promise} Promise resolved if sync is successful, rejected if sync fails. + */ + syncAllEvents(siteId?: string, force?: boolean): Promise { + return this.syncOnSites('all calendars', this.syncAllEventsFunc.bind(this), [force], siteId); + } + + /** + * Sync all events on a site. + * + * @param {string} siteId Site ID to sync. + * @param {boolean} [force] Wether to force sync not depending on last execution. + * @return {Promise} Promise resolved if sync is successful, rejected if sync fails. + */ + protected syncAllEventsFunc(siteId: string, force?: boolean): Promise { + const promise = force ? this.syncEvents(siteId) : this.syncEventsIfNeeded(siteId); + + return promise.then((result) => { + if (result && result.updated) { + // Sync successful, send event. + this.eventsProvider.trigger(AddonCalendarSyncProvider.AUTO_SYNCED, { + warnings: result.warnings, + events: result.events + }, siteId); + } + }); + } + + /** + * Sync a site events only if a certain time has passed since the last time. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the events are synced or if it doesn't need to be synced. + */ + syncEventsIfNeeded(siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + return this.isSyncNeeded(AddonCalendarSyncProvider.SYNC_ID, siteId).then((needed) => { + if (needed) { + return this.syncEvents(siteId); + } + }); + } + + /** + * Synchronize all offline events of a certain site. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if sync is successful, rejected otherwise. + */ + syncEvents(siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + if (this.isSyncing(AddonCalendarSyncProvider.SYNC_ID, siteId)) { + // There's already a sync ongoing for this site, return the promise. + return this.getOngoingSync(AddonCalendarSyncProvider.SYNC_ID, siteId); + } + + this.logger.debug('Try to sync calendar events for site ' + siteId); + + const result = { + warnings: [], + events: [], + updated: false + }; + let offlineEvents; + + // Get offline events. + const syncPromise = this.calendarOffline.getAllEvents(siteId).catch(() => { + // No offline data found, return empty list. + return []; + }).then((events) => { + offlineEvents = events; + + if (!events.length) { + // Nothing to sync. + return; + } else if (!this.appProvider.isOnline()) { + // Cannot sync in offline. + return Promise.reject(null); + } + + const promises = []; + + events.forEach((event) => { + promises.push(this.syncOfflineEvent(event, result, siteId)); + }); + + return this.utils.allPromises(promises); + }).then(() => { + if (result.updated) { + // Data has been sent to server. Now invalidate the WS calls. + const promises = [ + this.calendarProvider.invalidateEventsList(siteId), + ]; + + offlineEvents.forEach((event) => { + if (event.id > 0) { + // An event was edited, invalidate its data too. + promises.push(this.calendarProvider.invalidateEvent(event.id, siteId)); + } + }); + + return Promise.all(promises).catch(() => { + // Ignore errors. + }); + } + }).then(() => { + // Sync finished, set sync time. + return this.setSyncTime(AddonCalendarSyncProvider.SYNC_ID, siteId).catch(() => { + // Ignore errors. + }); + }).then(() => { + // All done, return the result. + return result; + }); + + return this.addOngoingSync(AddonCalendarSyncProvider.SYNC_ID, syncPromise, siteId); + } + + /** + * Synchronize an offline event. + * + * @param {any} event The event to sync. + * @param {any} result Object where to store the result of the sync. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if sync is successful, rejected otherwise. + */ + protected syncOfflineEvent(event: any, result: any, siteId?: string): Promise { + + // Verify that event isn't blocked. + if (this.syncProvider.isBlocked(AddonCalendarProvider.COMPONENT, event.id, siteId)) { + this.logger.debug('Cannot sync event ' + event.name + ' because it is blocked.'); + + return Promise.reject(this.translate.instant('core.errorsyncblocked', {$a: this.componentTranslate})); + } + + // Try to send the data. + const data = this.utils.clone(event); // Clone the object because it will be modified in the submit function. + + return this.calendarProvider.submitEventOnline(event.id > 0 ? event.id : undefined, data, siteId).then((newEvent) => { + result.updated = true; + result.events.push(newEvent); + + // Event sent, delete the offline data. + return this.calendarOffline.deleteEvent(event.id, siteId); + }).catch((error) => { + if (this.utils.isWebServiceError(error)) { + // The WebService has thrown an error, this means that the event cannot be created. Delete it. + result.updated = true; + + return this.calendarOffline.deleteEvent(event.id, siteId).then(() => { + // Event deleted, add a warning. + result.warnings.push(this.translate.instant('core.warningofflinedatadeleted', { + component: this.componentTranslate, + name: event.name, + error: this.textUtils.getErrorMessageFromError(error) + })); + }); + } + + // Local error, reject. + return Promise.reject(error); + }); + } +} diff --git a/src/addon/calendar/providers/helper.ts b/src/addon/calendar/providers/helper.ts index 30d4b9521..ae857225d 100644 --- a/src/addon/calendar/providers/helper.ts +++ b/src/addon/calendar/providers/helper.ts @@ -33,10 +33,33 @@ export class AddonCalendarHelperProvider { category: 'fa-cubes' }; - constructor(logger: CoreLoggerProvider, private courseProvider: CoreCourseProvider) { + constructor(logger: CoreLoggerProvider, private courseProvider: CoreCourseProvider, + private calendarProvider: AddonCalendarProvider) { this.logger = logger.getInstance('AddonCalendarHelperProvider'); } + /** + * Check if current user can create/edit events. + * + * @param {number} [courseId] Course ID. If not defined, site calendar. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with boolean: whether the user can create events. + */ + canEditEvents(courseId?: number, siteId?: string): Promise { + return this.calendarProvider.canEditEvents(siteId).then((canEdit) => { + if (!canEdit) { + return false; + } + + // Site allows creating events. Check if the user has permissions to do so. + return this.calendarProvider.getAllowedEventTypes(courseId, siteId).then((types) => { + return Object.keys(types).length > 0; + }); + }).catch(() => { + return false; + }); + } + /** * Convenience function to format some event data to be rendered. * diff --git a/src/addon/calendar/providers/sync-cron-handler.ts b/src/addon/calendar/providers/sync-cron-handler.ts new file mode 100644 index 000000000..6aabcca74 --- /dev/null +++ b/src/addon/calendar/providers/sync-cron-handler.ts @@ -0,0 +1,48 @@ +// (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 { CoreCronHandler } from '@providers/cron'; +import { AddonCalendarSyncProvider } from './calendar-sync'; + +/** + * Synchronization cron handler. + */ +@Injectable() +export class AddonCalendarSyncCronHandler implements CoreCronHandler { + name = 'AddonCalendarSyncCronHandler'; + + constructor(private calendarSync: AddonCalendarSyncProvider) {} + + /** + * Execute the process. + * Receives the ID of the site affected, undefined for all sites. + * + * @param {string} [siteId] ID of the site affected, undefined for all sites. + * @param {boolean} [force] Wether the execution is forced (manual sync). + * @return {Promise} Promise resolved when done, rejected if failure. + */ + execute(siteId?: string, force?: boolean): Promise { + return this.calendarSync.syncAllEvents(siteId, force); + } + + /** + * Get the time between consecutive executions. + * + * @return {number} Time between consecutive executions (in ms). + */ + getInterval(): number { + return this.calendarSync.syncInterval; + } +} diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 14730d64b..a7884b46f 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -84,6 +84,7 @@ "addon.blog.showonlyyourentries": "Show only your entries", "addon.blog.siteblogheading": "Site blog", "addon.calendar.calendar": "Calendar", + "addon.calendar.calendarevent": "Calendar event", "addon.calendar.calendarevents": "Calendar events", "addon.calendar.calendarreminders": "Calendar reminders", "addon.calendar.defaultnotificationtime": "Default notification time", @@ -1389,7 +1390,6 @@ "core.deleteduser": "Deleted user", "core.deleting": "Deleting", "core.description": "Description", - "core.dfdatetimeinput": "YYYY-MM-DDThh:mm:ss.SSS", "core.dfdaymonthyear": "MM-DD-YYYY", "core.dfdayweekmonth": "ddd, D MMM", "core.dffulldate": "dddd, D MMMM YYYY h[:]mm A", diff --git a/src/lang/en.json b/src/lang/en.json index 714b64a15..b5c79cb94 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -64,7 +64,6 @@ "deleteduser": "Deleted user", "deleting": "Deleting", "description": "Description", - "dfdatetimeinput": "YYYY-MM-DDThh:mm:ss.SSS", "dfdaymonthyear": "MM-DD-YYYY", "dfdayweekmonth": "ddd, D MMM", "dffulldate": "dddd, D MMMM YYYY h[:]mm A", diff --git a/src/providers/utils/time.ts b/src/providers/utils/time.ts index 020860908..6e2e00aa7 100644 --- a/src/providers/utils/time.ts +++ b/src/providers/utils/time.ts @@ -308,7 +308,22 @@ export class CoreTimeUtilsProvider { toDatetimeFormat(timestamp?: number): string { timestamp = timestamp || Date.now(); - return this.userDate(timestamp, 'core.dfdatetimeinput', false); + return this.userDate(timestamp, 'YYYY-MM-DDTHH:mm:ss.SSS', false); + } + + /** + * Convert the value of a ion-datetime to a Date. + * + * @param {string} value Value of ion-datetime. + * @return {Date} Date. + */ + datetimeToDate(value: string): Date { + if (typeof value == 'string' && value.slice(-1) == 'Z') { + // The value shoudln't have the timezone because it causes problems, remove it. + value = value.substr(0, value.length - 1); + } + + return new Date(value); } /**