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.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",

View File

@ -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();

View File

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

View File

@ -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;
}
}

View File

@ -7,13 +7,14 @@
</button>
<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 *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>
</ion-buttons>
</ion-navbar>
</ion-header>
<core-split-view>
<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>
<core-loading [hideUntil]="eventsLoaded">

View File

@ -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<any>} Promise resolved when done.
*/
fetchData(refresh: boolean = false): Promise<any> {
fetchData(refresh?: boolean, sync?: boolean, showErrors?: boolean): Promise<any> {
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<any>} Promise resolved when done.
*/
fetchEvents(refresh: boolean = false): Promise<any> {
fetchEvents(refresh?: boolean): Promise<any> {
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<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 = [];
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();
}
}

View File

@ -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.

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'
};
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<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.
*

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.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",

View File

@ -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",

View File

@ -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);
}
/**