MOBILE-1927 calendar: Implement events synchronization
parent
7ccfade21e
commit
98776a9c78
|
@ -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",
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"calendar": "Calendar",
|
||||
"calendarevent": "Calendar event",
|
||||
"calendarevents": "Calendar events",
|
||||
"calendarreminders": "Calendar reminders",
|
||||
"defaultnotificationtime": "Default notification time",
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in New Issue