MOBILE-1927 calendar: Implement events synchronization

main
Dani Palou 2019-06-21 15:22:05 +02:00
parent 7ccfade21e
commit 98776a9c78
13 changed files with 536 additions and 79 deletions

View File

@ -84,16 +84,30 @@
"addon.blog.showonlyyourentries": "local_moodlemobileapp", "addon.blog.showonlyyourentries": "local_moodlemobileapp",
"addon.blog.siteblogheading": "blog", "addon.blog.siteblogheading": "blog",
"addon.calendar.calendar": "calendar", "addon.calendar.calendar": "calendar",
"addon.calendar.calendarevent": "local_moodlemobileapp",
"addon.calendar.calendarevents": "local_moodlemobileapp", "addon.calendar.calendarevents": "local_moodlemobileapp",
"addon.calendar.calendarreminders": "local_moodlemobileapp", "addon.calendar.calendarreminders": "local_moodlemobileapp",
"addon.calendar.defaultnotificationtime": "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.errorloadevent": "local_moodlemobileapp",
"addon.calendar.errorloadevents": "local_moodlemobileapp", "addon.calendar.errorloadevents": "local_moodlemobileapp",
"addon.calendar.eventduration": "calendar",
"addon.calendar.eventendtime": "calendar", "addon.calendar.eventendtime": "calendar",
"addon.calendar.eventname": "calendar",
"addon.calendar.eventstarttime": "calendar", "addon.calendar.eventstarttime": "calendar",
"addon.calendar.eventtype": "calendar",
"addon.calendar.gotoactivity": "calendar", "addon.calendar.gotoactivity": "calendar",
"addon.calendar.invalidtimedurationminutes": "calendar",
"addon.calendar.invalidtimedurationuntil": "calendar",
"addon.calendar.newevent": "calendar",
"addon.calendar.noevents": "local_moodlemobileapp", "addon.calendar.noevents": "local_moodlemobileapp",
"addon.calendar.nopermissiontoupdatecalendar": "error",
"addon.calendar.reminders": "local_moodlemobileapp", "addon.calendar.reminders": "local_moodlemobileapp",
"addon.calendar.repeatevent": "calendar",
"addon.calendar.repeatweeksl": "calendar",
"addon.calendar.setnewreminder": "local_moodlemobileapp", "addon.calendar.setnewreminder": "local_moodlemobileapp",
"addon.calendar.typecategory": "calendar", "addon.calendar.typecategory": "calendar",
"addon.calendar.typeclose": "calendar", "addon.calendar.typeclose": "calendar",
@ -1328,6 +1342,7 @@
"core.course.warningmanualcompletionmodified": "local_moodlemobileapp", "core.course.warningmanualcompletionmodified": "local_moodlemobileapp",
"core.course.warningofflinemanualcompletiondeleted": "local_moodlemobileapp", "core.course.warningofflinemanualcompletiondeleted": "local_moodlemobileapp",
"core.coursedetails": "moodle", "core.coursedetails": "moodle",
"core.coursenogroups": "local_moodlemobileapp",
"core.courses.addtofavourites": "block_myoverview", "core.courses.addtofavourites": "block_myoverview",
"core.courses.allowguests": "enrol_guest", "core.courses.allowguests": "enrol_guest",
"core.courses.availablecourses": "moodle", "core.courses.availablecourses": "moodle",
@ -1457,6 +1472,7 @@
"core.grades.range": "grades", "core.grades.range": "grades",
"core.grades.rank": "grades", "core.grades.rank": "grades",
"core.grades.weight": "grades", "core.grades.weight": "grades",
"core.group": "moodle",
"core.groupsseparate": "moodle", "core.groupsseparate": "moodle",
"core.groupsvisible": "moodle", "core.groupsvisible": "moodle",
"core.hasdatatosync": "local_moodlemobileapp", "core.hasdatatosync": "local_moodlemobileapp",
@ -1692,6 +1708,9 @@
"core.sec": "moodle", "core.sec": "moodle",
"core.secs": "moodle", "core.secs": "moodle",
"core.seemoredetail": "survey", "core.seemoredetail": "survey",
"core.selectacategory": "moodle",
"core.selectacourse": "moodle",
"core.selectagroup": "moodle",
"core.send": "message", "core.send": "message",
"core.sending": "chat", "core.sending": "chat",
"core.serverconnection": "error", "core.serverconnection": "error",
@ -1760,6 +1779,7 @@
"core.sharedfiles.sharedfiles": "local_moodlemobileapp", "core.sharedfiles.sharedfiles": "local_moodlemobileapp",
"core.sharedfiles.successstorefile": "local_moodlemobileapp", "core.sharedfiles.successstorefile": "local_moodlemobileapp",
"core.show": "moodle", "core.show": "moodle",
"core.showless": "form",
"core.showmore": "form", "core.showmore": "form",
"core.site": "moodle", "core.site": "moodle",
"core.sitehome.sitehome": "moodle", "core.sitehome.sitehome": "moodle",
@ -1808,6 +1828,7 @@
"core.unlimited": "moodle", "core.unlimited": "moodle",
"core.unzipping": "local_moodlemobileapp", "core.unzipping": "local_moodlemobileapp",
"core.upgraderunning": "error", "core.upgraderunning": "error",
"core.user": "moodle",
"core.user.address": "moodle", "core.user.address": "moodle",
"core.user.city": "moodle", "core.user.city": "moodle",
"core.user.contact": "local_moodlemobileapp", "core.user.contact": "local_moodlemobileapp",

View File

@ -16,8 +16,11 @@ import { NgModule } from '@angular/core';
import { AddonCalendarProvider } from './providers/calendar'; import { AddonCalendarProvider } from './providers/calendar';
import { AddonCalendarOfflineProvider } from './providers/calendar-offline'; import { AddonCalendarOfflineProvider } from './providers/calendar-offline';
import { AddonCalendarHelperProvider } from './providers/helper'; import { AddonCalendarHelperProvider } from './providers/helper';
import { AddonCalendarSyncProvider } from './providers/calendar-sync';
import { AddonCalendarMainMenuHandler } from './providers/mainmenu-handler'; import { AddonCalendarMainMenuHandler } from './providers/mainmenu-handler';
import { AddonCalendarSyncCronHandler } from './providers/sync-cron-handler';
import { CoreMainMenuDelegate } from '@core/mainmenu/providers/delegate'; import { CoreMainMenuDelegate } from '@core/mainmenu/providers/delegate';
import { CoreCronDelegate } from '@providers/cron';
import { CoreInitDelegate } from '@providers/init'; import { CoreInitDelegate } from '@providers/init';
import { CoreLocalNotificationsProvider } from '@providers/local-notifications'; import { CoreLocalNotificationsProvider } from '@providers/local-notifications';
import { CoreLoginHelperProvider } from '@core/login/providers/helper'; import { CoreLoginHelperProvider } from '@core/login/providers/helper';
@ -27,7 +30,8 @@ import { CoreUpdateManagerProvider } from '@providers/update-manager';
export const ADDON_CALENDAR_PROVIDERS: any[] = [ export const ADDON_CALENDAR_PROVIDERS: any[] = [
AddonCalendarProvider, AddonCalendarProvider,
AddonCalendarOfflineProvider, AddonCalendarOfflineProvider,
AddonCalendarHelperProvider AddonCalendarHelperProvider,
AddonCalendarSyncProvider
]; ];
@NgModule({ @NgModule({
@ -39,14 +43,19 @@ export const ADDON_CALENDAR_PROVIDERS: any[] = [
AddonCalendarProvider, AddonCalendarProvider,
AddonCalendarOfflineProvider, AddonCalendarOfflineProvider,
AddonCalendarHelperProvider, AddonCalendarHelperProvider,
AddonCalendarMainMenuHandler AddonCalendarSyncProvider,
AddonCalendarMainMenuHandler,
AddonCalendarSyncCronHandler
] ]
}) })
export class AddonCalendarModule { export class AddonCalendarModule {
constructor(mainMenuDelegate: CoreMainMenuDelegate, calendarHandler: AddonCalendarMainMenuHandler, constructor(mainMenuDelegate: CoreMainMenuDelegate, calendarHandler: AddonCalendarMainMenuHandler,
initDelegate: CoreInitDelegate, calendarProvider: AddonCalendarProvider, loginHelper: CoreLoginHelperProvider, initDelegate: CoreInitDelegate, calendarProvider: AddonCalendarProvider, loginHelper: CoreLoginHelperProvider,
localNotificationsProvider: CoreLocalNotificationsProvider, updateManager: CoreUpdateManagerProvider) { localNotificationsProvider: CoreLocalNotificationsProvider, updateManager: CoreUpdateManagerProvider,
cronDelegate: CoreCronDelegate, syncHandler: AddonCalendarSyncCronHandler) {
mainMenuDelegate.registerHandler(calendarHandler); mainMenuDelegate.registerHandler(calendarHandler);
cronDelegate.register(syncHandler);
initDelegate.ready().then(() => { initDelegate.ready().then(() => {
calendarProvider.scheduleAllSitesEventsNotifications(); calendarProvider.scheduleAllSitesEventsNotifications();

View File

@ -1,5 +1,6 @@
{ {
"calendar": "Calendar", "calendar": "Calendar",
"calendarevent": "Calendar event",
"calendarevents": "Calendar events", "calendarevents": "Calendar events",
"calendarreminders": "Calendar reminders", "calendarreminders": "Calendar reminders",
"defaultnotificationtime": "Default notification time", "defaultnotificationtime": "Default notification time",

View File

@ -12,13 +12,14 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // 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 { FormControl, FormGroup, FormBuilder, Validators } from '@angular/forms';
import { IonicPage, NavController, NavParams } from 'ionic-angular'; import { IonicPage, NavController, NavParams } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { CoreEventsProvider } from '@providers/events'; import { CoreEventsProvider } from '@providers/events';
import { CoreGroupsProvider } from '@providers/groups'; import { CoreGroupsProvider } from '@providers/groups';
import { CoreSitesProvider } from '@providers/sites'; import { CoreSitesProvider } from '@providers/sites';
import { CoreSyncProvider } from '@providers/sync';
import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreTimeUtilsProvider } from '@providers/utils/time';
import { CoreUtilsProvider } from '@providers/utils/utils'; 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 { AddonCalendarProvider } from '../../providers/calendar';
import { AddonCalendarOfflineProvider } from '../../providers/calendar-offline'; import { AddonCalendarOfflineProvider } from '../../providers/calendar-offline';
import { AddonCalendarHelperProvider } from '../../providers/helper'; import { AddonCalendarHelperProvider } from '../../providers/helper';
import { AddonCalendarSyncProvider } from '../../providers/calendar-sync';
import { CoreSite } from '@classes/site'; import { CoreSite } from '@classes/site';
/** /**
@ -38,7 +40,7 @@ import { CoreSite } from '@classes/site';
selector: 'page-addon-calendar-edit-event', selector: 'page-addon-calendar-edit-event',
templateUrl: 'edit-event.html', templateUrl: 'edit-event.html',
}) })
export class AddonCalendarEditEventPage implements OnInit { export class AddonCalendarEditEventPage implements OnInit, OnDestroy {
@ViewChild(CoreRichTextEditorComponent) descriptionEditor: CoreRichTextEditorComponent; @ViewChild(CoreRichTextEditorComponent) descriptionEditor: CoreRichTextEditorComponent;
@ -68,6 +70,7 @@ export class AddonCalendarEditEventPage implements OnInit {
protected currentSite: CoreSite; protected currentSite: CoreSite;
protected types: any; // Object with the supported types. protected types: any; // Object with the supported types.
protected showAll: boolean; protected showAll: boolean;
protected isDestroyed = false;
constructor(navParams: NavParams, constructor(navParams: NavParams,
private navCtrl: NavController, private navCtrl: NavController,
@ -82,7 +85,9 @@ export class AddonCalendarEditEventPage implements OnInit {
private calendarProvider: AddonCalendarProvider, private calendarProvider: AddonCalendarProvider,
private calendarOffline: AddonCalendarOfflineProvider, private calendarOffline: AddonCalendarOfflineProvider,
private calendarHelper: AddonCalendarHelperProvider, private calendarHelper: AddonCalendarHelperProvider,
private calendarSync: AddonCalendarSyncProvider,
private fb: FormBuilder, private fb: FormBuilder,
private syncProvider: CoreSyncProvider,
@Optional() private svComponent: CoreSplitViewComponent) { @Optional() private svComponent: CoreSplitViewComponent) {
this.eventId = navParams.get('eventId'); this.eventId = navParams.get('eventId');
@ -142,10 +147,10 @@ export class AddonCalendarEditEventPage implements OnInit {
let accessInfo; let accessInfo;
// Get access info. // Get access info.
return this.calendarProvider.getAccessInformation().then((info) => { return this.calendarProvider.getAccessInformation(this.courseId).then((info) => {
accessInfo = info; accessInfo = info;
return this.calendarProvider.getAllowedEventTypes(); return this.calendarProvider.getAllowedEventTypes(this.courseId);
}).then((types) => { }).then((types) => {
this.types = types; this.types = types;
@ -157,29 +162,38 @@ export class AddonCalendarEditEventPage implements OnInit {
} }
if (this.eventId && !refresh) { if (this.eventId && !refresh) {
// Get the event data if there's any. // If editing an event, get offline data. Wait for sync first.
promises.push(this.calendarOffline.getEvent(this.eventId).then((event) => {
this.hasOffline = true;
// Load the data in the form. promises.push(this.calendarSync.waitForSync(AddonCalendarSyncProvider.SYNC_ID).then(() => {
this.eventForm.controls.name.setValue(event.name); // Do not block if the scope is already destroyed.
this.eventForm.controls.timestart.setValue(this.timeUtils.toDatetimeFormat(event.timestart * 1000)); if (!this.isDestroyed) {
this.eventForm.controls.eventtype.setValue(event.eventtype); this.syncProvider.blockOperation(AddonCalendarProvider.COMPONENT, this.eventId);
this.eventForm.controls.categoryid.setValue(event.categoryid || ''); }
this.eventForm.controls.courseid.setValue(event.courseid || '');
this.eventForm.controls.groupcourseid.setValue(event.groupcourseid || ''); // Get the event data if there's any.
this.eventForm.controls.groupid.setValue(event.groupid || ''); return this.calendarOffline.getEvent(this.eventId).then((event) => {
this.eventForm.controls.description.setValue(event.description); this.hasOffline = true;
this.eventForm.controls.location.setValue(event.location);
this.eventForm.controls.duration.setValue(event.duration); // Load the data in the form.
this.eventForm.controls.timedurationuntil.setValue( this.eventForm.controls.name.setValue(event.name);
this.timeUtils.toDatetimeFormat((event.timedurationuntil * 1000) || Date.now())); this.eventForm.controls.timestart.setValue(this.timeUtils.toDatetimeFormat(event.timestart * 1000));
this.eventForm.controls.timedurationminutes.setValue(event.timedurationminutes || ''); this.eventForm.controls.eventtype.setValue(event.eventtype);
this.eventForm.controls.repeat.setValue(!!event.repeat); this.eventForm.controls.categoryid.setValue(event.categoryid || '');
this.eventForm.controls.repeats.setValue(event.repeats || '1'); this.eventForm.controls.courseid.setValue(event.courseid || '');
}).catch(() => { this.eventForm.controls.groupcourseid.setValue(event.groupcourseid || '');
// No offline data. this.eventForm.controls.groupid.setValue(event.groupid || '');
this.hasOffline = false; 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 { submit(): void {
// Validate data. // Validate data.
const formData = this.eventForm.value, const formData = this.eventForm.value,
timeStartDate = new Date(formData.timestart), timeStartDate = this.timeUtils.datetimeToDate(formData.timestart),
timeUntilDate = new Date(formData.timedurationuntil), timeUntilDate = this.timeUtils.datetimeToDate(formData.timedurationuntil),
timeDurationMinutes = parseInt(formData.timedurationminutes || '', 10); timeDurationMinutes = parseInt(formData.timedurationminutes || '', 10);
let error; let error;
@ -382,6 +396,9 @@ export class AddonCalendarEditEventPage implements OnInit {
* @param {number} [event] Event. * @param {number} [event] Event.
*/ */
protected returnToList(event?: any): void { 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) { if (event) {
const data: any = { const data: any = {
event: event event: event
@ -432,4 +449,18 @@ export class AddonCalendarEditEventPage implements OnInit {
return Promise.resolve(); 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;
}
} }

View File

@ -7,13 +7,14 @@
</button> </button>
<core-context-menu> <core-context-menu>
<core-context-menu-item [hidden]="!notificationsEnabled" [priority]="600" [content]="'core.settings.settings' | translate" (action)="openSettings()" [iconAction]="'cog'"></core-context-menu-item> <core-context-menu-item [hidden]="!notificationsEnabled" [priority]="600" [content]="'core.settings.settings' | translate" (action)="openSettings()" [iconAction]="'cog'"></core-context-menu-item>
<core-context-menu-item *ngIf="eventsLoaded && hasOffline && isOnline" [priority]="400" [content]="'core.settings.synchronizenow' | translate" (action)="doRefresh(null, $event, true)" [iconAction]="syncIcon" [closeOnClick]="false"></core-context-menu-item>
</core-context-menu> </core-context-menu>
</ion-buttons> </ion-buttons>
</ion-navbar> </ion-navbar>
</ion-header> </ion-header>
<core-split-view> <core-split-view>
<ion-content> <ion-content>
<ion-refresher [enabled]="eventsLoaded" (ionRefresh)="refreshEvents($event)"> <ion-refresher [enabled]="eventsLoaded" (ionRefresh)="doRefresh($event)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher> </ion-refresher>
<core-loading [hideUntil]="eventsLoaded"> <core-loading [hideUntil]="eventsLoaded">

View File

@ -12,12 +12,13 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // 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 { IonicPage, Content, PopoverController, NavParams, NavController } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { AddonCalendarProvider } from '../../providers/calendar'; import { AddonCalendarProvider } from '../../providers/calendar';
import { AddonCalendarOfflineProvider } from '../../providers/calendar-offline'; import { AddonCalendarOfflineProvider } from '../../providers/calendar-offline';
import { AddonCalendarHelperProvider } from '../../providers/helper'; import { AddonCalendarHelperProvider } from '../../providers/helper';
import { AddonCalendarSyncProvider } from '../../providers/calendar-sync';
import { CoreCoursesProvider } from '@core/courses/providers/courses'; import { CoreCoursesProvider } from '@core/courses/providers/courses';
import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreUtilsProvider } from '@providers/utils/utils';
@ -28,6 +29,7 @@ import { CoreEventsProvider } from '@providers/events';
import { CoreAppProvider } from '@providers/app'; import { CoreAppProvider } from '@providers/app';
import { CoreSplitViewComponent } from '@components/split-view/split-view'; import { CoreSplitViewComponent } from '@components/split-view/split-view';
import * as moment from 'moment'; import * as moment from 'moment';
import { Network } from '@ionic-native/network';
/** /**
* Page that displays the list of calendar events. * Page that displays the list of calendar events.
@ -57,6 +59,8 @@ export class AddonCalendarListPage implements OnDestroy {
protected preSelectedCourseId: number; protected preSelectedCourseId: number;
protected newEventObserver: any; protected newEventObserver: any;
protected discardedObserver: any; protected discardedObserver: any;
protected syncObserver: any;
protected onlineObserver: any;
courses: any[]; courses: any[];
eventsLoaded = false; eventsLoaded = false;
@ -71,13 +75,16 @@ export class AddonCalendarListPage implements OnDestroy {
}; };
canCreate = false; canCreate = false;
hasOffline = false; hasOffline = false;
isOnline = false;
syncIcon: string; // Sync icon.
constructor(private translate: TranslateService, private calendarProvider: AddonCalendarProvider, navParams: NavParams, constructor(private translate: TranslateService, private calendarProvider: AddonCalendarProvider, navParams: NavParams,
private domUtils: CoreDomUtilsProvider, private coursesProvider: CoreCoursesProvider, private utils: CoreUtilsProvider, 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, localNotificationsProvider: CoreLocalNotificationsProvider, private popoverCtrl: PopoverController,
eventsProvider: CoreEventsProvider, private navCtrl: NavController, appProvider: CoreAppProvider, private eventsProvider: CoreEventsProvider, private navCtrl: NavController, private appProvider: CoreAppProvider,
private calendarOffline: AddonCalendarOfflineProvider) { private calendarOffline: AddonCalendarOfflineProvider, private calendarSync: AddonCalendarSyncProvider,
network: Network) {
this.siteHomeId = sitesProvider.getCurrentSite().getSiteHomeId(); this.siteHomeId = sitesProvider.getCurrentSite().getSiteHomeId();
this.notificationsEnabled = localNotificationsProvider.isAvailable(); this.notificationsEnabled = localNotificationsProvider.isAvailable();
@ -101,7 +108,7 @@ export class AddonCalendarListPage implements OnDestroy {
} }
this.eventsLoaded = false; 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). // In tablet mode try to open the event (only if it's an online event).
if (this.splitviewCtrl.isOn() && data.event.id > 0) { if (this.splitviewCtrl.isOn() && data.event.id > 0) {
@ -119,8 +126,22 @@ export class AddonCalendarListPage implements OnDestroy {
} }
this.eventsLoaded = false; this.eventsLoaded = false;
this.refreshEvents(false); this.refreshEvents(true, false);
}, sitesProvider.getCurrentSiteId()); }, 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.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) { if (!this.eventId && this.splitviewCtrl.isOn() && this.events.length > 0) {
// Take first and load it. // Take first and load it.
this.gotoEvent(this.events[0].id); this.gotoEvent(this.events[0].id);
@ -144,49 +167,76 @@ export class AddonCalendarListPage implements OnDestroy {
* Fetch all the data required for the view. * Fetch all the data required for the view.
* *
* @param {boolean} [refresh] Empty events array first. * @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<any>} Promise resolved when done. * @return {Promise<any>} Promise resolved when done.
*/ */
fetchData(refresh: boolean = false): Promise<any> { fetchData(refresh?: boolean, sync?: boolean, showErrors?: boolean): Promise<any> {
this.daysLoaded = 0; this.daysLoaded = 0;
this.emptyEventsTimes = 0; this.emptyEventsTimes = 0;
this.isOnline = this.appProvider.isOnline();
const promises = []; let promise;
if (this.calendarProvider.canEditEventsInSite()) { if (sync) {
// Site allows creating events. Check if the user has permissions to do so. // Try to synchronize offline events.
promises.push(this.calendarProvider.getAllowedEventTypes().then((types) => { promise = this.calendarSync.syncEvents().then((result) => {
this.canCreate = Object.keys(types).length > 0; if (result.warnings && result.warnings.length) {
}).catch(() => { this.domUtils.showErrorModal(result.warnings[0]);
this.canCreate = false; }
}));
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. return promise.then(() => {
promises.push(this.coursesProvider.getUserCourses(false).then((courses) => {
// Add "All courses".
courses.unshift(this.allCourses);
this.courses = courses;
if (this.preSelectedCourseId) { const promises = [];
this.filter.course = courses.find((course) => { const courseId = this.filter.course.id != this.allCourses.id ? this.filter.course.id : undefined;
return course.id == this.preSelectedCourseId;
});
}
return this.fetchEvents(refresh); promises.push(this.calendarHelper.canEditEvents(courseId).then((canEdit) => {
})); this.canCreate = canEdit;
}));
// Get offline events. // Load courses for the popover.
promises.push(this.calendarOffline.getAllEvents().then((events) => { promises.push(this.coursesProvider.getUserCourses(false).then((courses) => {
this.hasOffline = !!events.length; // Add "All courses".
courses.unshift(this.allCourses);
this.courses = courses;
// Format data and sort by timestart. if (this.preSelectedCourseId) {
events.forEach(this.calendarHelper.formatEventData.bind(this.calendarHelper)); this.filter.course = courses.find((course) => {
this.offlineEvents = events.sort((a, b) => a.timestart - b.timestart); 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.eventsLoaded = true;
this.syncIcon = 'sync';
}); });
} }
@ -196,7 +246,7 @@ export class AddonCalendarListPage implements OnDestroy {
* @param {boolean} [refresh] Empty events array first. * @param {boolean} [refresh] Empty events array first.
* @return {Promise<any>} Promise resolved when done. * @return {Promise<any>} Promise resolved when done.
*/ */
fetchEvents(refresh: boolean = false): Promise<any> { fetchEvents(refresh?: boolean): Promise<any> {
this.loadMoreError = false; this.loadMoreError = false;
return this.calendarProvider.getEventsList(this.daysLoaded, AddonCalendarProvider.DAYS_INTERVAL).then((events) => { 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 {any} [refresher] Refresher.
* @param {Function} [done] Function to call when done.
* @param {boolean} [showErrors] Whether to show sync errors to the user.
* @return {Promise<any>} Promise resolved when done. * @return {Promise<any>} Promise resolved when done.
*/ */
refreshEvents(refresher?: any): Promise<any> { doRefresh(refresher?: any, done?: () => void, showErrors?: boolean): Promise<any> {
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<any>} Promise resolved when done.
*/
refreshEvents(sync?: boolean, showErrors?: boolean): Promise<any> {
this.syncIcon = 'spinner';
const promises = []; const promises = [];
promises.push(this.calendarProvider.invalidateEventsList()); promises.push(this.calendarProvider.invalidateEventsList());
@ -384,9 +456,7 @@ export class AddonCalendarListPage implements OnDestroy {
} }
return Promise.all(promises).finally(() => { return Promise.all(promises).finally(() => {
return this.fetchData(true).finally(() => { return this.fetchData(true, sync, showErrors);
refresher && refresher.complete();
});
}); });
} }
@ -440,6 +510,13 @@ export class AddonCalendarListPage implements OnDestroy {
this.domUtils.scrollToTop(this.content); this.domUtils.scrollToTop(this.content);
this.filteredEvents = this.getFilteredEvents(); 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({ popover.present({
@ -496,5 +573,8 @@ export class AddonCalendarListPage implements OnDestroy {
ngOnDestroy(): void { ngOnDestroy(): void {
this.obsDefaultTimeChange && this.obsDefaultTimeChange.off(); this.obsDefaultTimeChange && this.obsDefaultTimeChange.off();
this.newEventObserver && this.newEventObserver.off(); this.newEventObserver && this.newEventObserver.off();
this.discardedObserver && this.discardedObserver.off();
this.syncObserver && this.syncObserver.off();
this.onlineObserver && this.onlineObserver.off();
} }
} }

View File

@ -14,7 +14,6 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites'; import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites';
import { AddonCalendarProvider } from './calendar';
/** /**
* Service to handle offline calendar events. * Service to handle offline calendar events.

View File

@ -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<any>} Promise resolved if sync is successful, rejected if sync fails.
*/
syncAllEvents(siteId?: string, force?: boolean): Promise<any> {
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<any>} Promise resolved if sync is successful, rejected if sync fails.
*/
protected syncAllEventsFunc(siteId: string, force?: boolean): Promise<any> {
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<any>} Promise resolved when the events are synced or if it doesn't need to be synced.
*/
syncEventsIfNeeded(siteId?: string): Promise<any> {
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<any>} Promise resolved if sync is successful, rejected otherwise.
*/
syncEvents(siteId?: string): Promise<any> {
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<any>} Promise resolved if sync is successful, rejected otherwise.
*/
protected syncOfflineEvent(event: any, result: any, siteId?: string): Promise<any> {
// 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);
});
}
}

View File

@ -33,10 +33,33 @@ export class AddonCalendarHelperProvider {
category: 'fa-cubes' category: 'fa-cubes'
}; };
constructor(logger: CoreLoggerProvider, private courseProvider: CoreCourseProvider) { constructor(logger: CoreLoggerProvider, private courseProvider: CoreCourseProvider,
private calendarProvider: AddonCalendarProvider) {
this.logger = logger.getInstance('AddonCalendarHelperProvider'); 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<boolean>} Promise resolved with boolean: whether the user can create events.
*/
canEditEvents(courseId?: number, siteId?: string): Promise<boolean> {
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. * Convenience function to format some event data to be rendered.
* *

View File

@ -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<any>} Promise resolved when done, rejected if failure.
*/
execute(siteId?: string, force?: boolean): Promise<any> {
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;
}
}

View File

@ -84,6 +84,7 @@
"addon.blog.showonlyyourentries": "Show only your entries", "addon.blog.showonlyyourentries": "Show only your entries",
"addon.blog.siteblogheading": "Site blog", "addon.blog.siteblogheading": "Site blog",
"addon.calendar.calendar": "Calendar", "addon.calendar.calendar": "Calendar",
"addon.calendar.calendarevent": "Calendar event",
"addon.calendar.calendarevents": "Calendar events", "addon.calendar.calendarevents": "Calendar events",
"addon.calendar.calendarreminders": "Calendar reminders", "addon.calendar.calendarreminders": "Calendar reminders",
"addon.calendar.defaultnotificationtime": "Default notification time", "addon.calendar.defaultnotificationtime": "Default notification time",
@ -1389,7 +1390,6 @@
"core.deleteduser": "Deleted user", "core.deleteduser": "Deleted user",
"core.deleting": "Deleting", "core.deleting": "Deleting",
"core.description": "Description", "core.description": "Description",
"core.dfdatetimeinput": "YYYY-MM-DDThh:mm:ss.SSS",
"core.dfdaymonthyear": "MM-DD-YYYY", "core.dfdaymonthyear": "MM-DD-YYYY",
"core.dfdayweekmonth": "ddd, D MMM", "core.dfdayweekmonth": "ddd, D MMM",
"core.dffulldate": "dddd, D MMMM YYYY h[:]mm A", "core.dffulldate": "dddd, D MMMM YYYY h[:]mm A",

View File

@ -64,7 +64,6 @@
"deleteduser": "Deleted user", "deleteduser": "Deleted user",
"deleting": "Deleting", "deleting": "Deleting",
"description": "Description", "description": "Description",
"dfdatetimeinput": "YYYY-MM-DDThh:mm:ss.SSS",
"dfdaymonthyear": "MM-DD-YYYY", "dfdaymonthyear": "MM-DD-YYYY",
"dfdayweekmonth": "ddd, D MMM", "dfdayweekmonth": "ddd, D MMM",
"dffulldate": "dddd, D MMMM YYYY h[:]mm A", "dffulldate": "dddd, D MMMM YYYY h[:]mm A",

View File

@ -308,7 +308,22 @@ export class CoreTimeUtilsProvider {
toDatetimeFormat(timestamp?: number): string { toDatetimeFormat(timestamp?: number): string {
timestamp = timestamp || Date.now(); 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);
} }
/** /**