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