From cfaaefc184626283e05d60129921741eece521f5 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 20 Jun 2019 16:15:05 +0200 Subject: [PATCH] MOBILE-1927 calendar: Allow creating events in online --- src/addon/calendar/lang/en.json | 13 + .../calendar/pages/edit-event/edit-event.html | 144 +++++++ .../pages/edit-event/edit-event.module.ts | 33 ++ .../calendar/pages/edit-event/edit-event.scss | 35 ++ .../calendar/pages/edit-event/edit-event.ts | 392 ++++++++++++++++++ src/addon/calendar/pages/list/list.html | 7 + src/addon/calendar/pages/list/list.ts | 69 ++- src/addon/calendar/providers/calendar.ts | 169 +++++++- src/addon/calendar/providers/helper.ts | 70 ++++ src/assets/lang/en.json | 21 + src/lang/en.json | 8 + src/providers/utils/utils.ts | 44 +- 12 files changed, 992 insertions(+), 13 deletions(-) create mode 100644 src/addon/calendar/pages/edit-event/edit-event.html create mode 100644 src/addon/calendar/pages/edit-event/edit-event.module.ts create mode 100644 src/addon/calendar/pages/edit-event/edit-event.scss create mode 100644 src/addon/calendar/pages/edit-event/edit-event.ts diff --git a/src/addon/calendar/lang/en.json b/src/addon/calendar/lang/en.json index 6ccb04caa..3139bb8a9 100644 --- a/src/addon/calendar/lang/en.json +++ b/src/addon/calendar/lang/en.json @@ -3,13 +3,26 @@ "calendarevents": "Calendar events", "calendarreminders": "Calendar reminders", "defaultnotificationtime": "Default notification time", + "durationminutes": "Duration in minutes", + "durationnone": "Without duration", + "durationuntil": "Until", + "editevent": "Editing event", "errorloadevent": "Error loading event.", "errorloadevents": "Error loading events.", + "eventduration": "Duration", "eventendtime": "End time", + "eventname": "Event title", "eventstarttime": "Start time", + "eventtype": "Event type", "gotoactivity": "Go to activity", + "invalidtimedurationminutes": "The duration in minutes you have entered is invalid. Please enter the duration in minutes greater than 0 or select no duration.", + "invalidtimedurationuntil": "The date and time you selected for duration until is before the start time of the event. Please correct this before proceeding.", + "newevent": "New event", "noevents": "There are no events", + "nopermissiontoupdatecalendar": "Sorry, but you do not have permission to update the calendar event", "reminders": "Reminders", + "repeatevent": "Repeat this event", + "repeatweeksl": "Repeat weekly, creating altogether", "setnewreminder": "Set a new reminder", "typeclose": "Close event", "typecourse": "Course event", diff --git a/src/addon/calendar/pages/edit-event/edit-event.html b/src/addon/calendar/pages/edit-event/edit-event.html new file mode 100644 index 000000000..bfddc7633 --- /dev/null +++ b/src/addon/calendar/pages/edit-event/edit-event.html @@ -0,0 +1,144 @@ + + + + + + + + + + + +
+ + +

{{ 'addon.calendar.eventname' | translate }}

+ + +
+ + + +

{{ 'core.date' | translate }}

+ + +
+ + + +

{{ 'addon.calendar.eventtype' | translate }}

+ + {{ type.name | translate }} + +
+ + + +

{{ 'core.category' | translate }}

+ + {{ category.name }} + +
+ + + +

{{ 'core.course' | translate }}

+ + {{ course.fullname }} + +
+ + + + + +

{{ 'core.course' | translate }}

+ + {{ course.fullname }} + +
+ + +

{{ 'core.coursenogroups' | translate }}

+
+ + +

{{ 'core.group' | translate }}

+ + {{ group.name }} + +
+ + + + +
+ + + + + {{ 'core.showmore' | translate }} + + {{ 'core.showless' | translate }} + + + + + +

{{ 'core.description' | translate }}

+ +
+ + + +

{{ 'core.location' | translate }}

+ +
+ + +
+

{{ 'addon.calendar.eventduration' | translate }}

+ + {{ 'addon.calendar.durationnone' | translate }} + + + + {{ 'addon.calendar.durationuntil' | translate }} + + + + + + + {{ 'addon.calendar.durationminutes' | translate }} + + + + + +
+ + + +

{{ 'addon.calendar.repeatevent' | translate }}

+ +
+ +

{{ 'addon.calendar.repeatweeksl' | translate }}

+ +
+
+ + + + + + + + + + + +
+
+
diff --git a/src/addon/calendar/pages/edit-event/edit-event.module.ts b/src/addon/calendar/pages/edit-event/edit-event.module.ts new file mode 100644 index 000000000..c5b20e969 --- /dev/null +++ b/src/addon/calendar/pages/edit-event/edit-event.module.ts @@ -0,0 +1,33 @@ +// (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 { NgModule } from '@angular/core'; +import { IonicPageModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { AddonCalendarEditEventPage } from './edit-event'; + +@NgModule({ + declarations: [ + AddonCalendarEditEventPage, + ], + imports: [ + CoreComponentsModule, + CoreDirectivesModule, + IonicPageModule.forChild(AddonCalendarEditEventPage), + TranslateModule.forChild() + ], +}) +export class AddonCalendarEditEventPageModule {} diff --git a/src/addon/calendar/pages/edit-event/edit-event.scss b/src/addon/calendar/pages/edit-event/edit-event.scss new file mode 100644 index 000000000..3c43c635e --- /dev/null +++ b/src/addon/calendar/pages/edit-event/edit-event.scss @@ -0,0 +1,35 @@ +ion-app.app-root page-addon-calendar-edit-event { + .addon-calendar-duration-container ion-item:not(.addon-calendar-duration-title) { + &.item-ios { + @include padding-horizontal($item-ios-padding-start * 2, null); + + ion-input { + @include padding-horizontal($datetime-ios-padding-start - $text-input-ios-margin-start, null); + } + } + &.item-md { + @include padding-horizontal($item-md-padding-start * 2, null); + + ion-input { + @include padding-horizontal($datetime-md-padding-start - $text-input-md-margin-start, null); + } + } + &.item-wp { + @include padding-horizontal($item-wp-padding-start * 2, null); + + ion-input { + @include padding-horizontal($datetime-wp-padding-start - $text-input-wp-margin-start, null); + } + } + } + + .addon-calendar-eventtype-container.item-select-disabled { + ion-label, ion-select { + opacity: 1; + } + + .select-icon { + display: none; + } + } +} \ No newline at end of file diff --git a/src/addon/calendar/pages/edit-event/edit-event.ts b/src/addon/calendar/pages/edit-event/edit-event.ts new file mode 100644 index 000000000..abb8f8b75 --- /dev/null +++ b/src/addon/calendar/pages/edit-event/edit-event.ts @@ -0,0 +1,392 @@ +// (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 { Component, OnInit, 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 { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreTimeUtilsProvider } from '@providers/utils/time'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreCoursesProvider } from '@core/courses/providers/courses'; +import { CoreSplitViewComponent } from '@components/split-view/split-view'; +import { CoreRichTextEditorComponent } from '@components/rich-text-editor/rich-text-editor.ts'; +import { AddonCalendarProvider } from '../../providers/calendar'; +import { AddonCalendarHelperProvider } from '../../providers/helper'; +import { CoreSite } from '@classes/site'; + +/** + * Page that displays a form to create/edit an event. + */ +@IonicPage({ segment: 'addon-calendar-edit-event' }) +@Component({ + selector: 'page-addon-calendar-edit-event', + templateUrl: 'edit-event.html', +}) +export class AddonCalendarEditEventPage implements OnInit { + + @ViewChild(CoreRichTextEditorComponent) descriptionEditor: CoreRichTextEditorComponent; + + title: string; + dateFormat: string; + component = AddonCalendarProvider.COMPONENT; + loaded = false; + hasOffline = false; + eventTypes = []; + categories = []; + courses = []; + groups = []; + loadingGroups = false; + courseGroupSet = false; + advanced = false; + errors: any; + + // Form variables. + eventForm: FormGroup; + eventTypeControl: FormControl; + groupControl: FormControl; + descriptionControl: FormControl; + + protected eventId: number; + protected courseId: number; + protected originalData: any; + protected currentSite: CoreSite; + protected types: any; // Object with the supported types. + protected showAll: boolean; + + constructor(navParams: NavParams, + private navCtrl: NavController, + private translate: TranslateService, + private domUtils: CoreDomUtilsProvider, + private timeUtils: CoreTimeUtilsProvider, + private eventsProvider: CoreEventsProvider, + private groupsProvider: CoreGroupsProvider, + sitesProvider: CoreSitesProvider, + private coursesProvider: CoreCoursesProvider, + private utils: CoreUtilsProvider, + private calendarProvider: AddonCalendarProvider, + private calendarHelper: AddonCalendarHelperProvider, + private fb: FormBuilder, + @Optional() private svComponent: CoreSplitViewComponent) { + + this.eventId = navParams.get('eventId'); + this.courseId = navParams.get('courseId'); + this.title = this.eventId ? 'addon.calendar.editevent' : 'addon.calendar.newevent'; + + this.currentSite = sitesProvider.getCurrentSite(); + this.errors = { + required: this.translate.instant('core.required') + }; + + // Calculate format to use. ion-datetime doesn't support escaping characters ([]), so we remove them. + this.dateFormat = this.timeUtils.convertPHPToMoment(this.translate.instant('core.strftimedatetimeshort')) + .replace(/[\[\]]/g, ''); + + // Initialize form variables. + this.eventForm = new FormGroup({}); + this.eventTypeControl = this.fb.control('', Validators.required); + this.groupControl = this.fb.control(''); + this.descriptionControl = this.fb.control(''); + + this.eventForm.addControl('name', this.fb.control('', Validators.required)); + this.eventForm.addControl('timestart', this.fb.control(new Date().toISOString(), Validators.required)); + this.eventForm.addControl('eventtype', this.eventTypeControl); + this.eventForm.addControl('categoryid', this.fb.control('')); + this.eventForm.addControl('courseid', this.fb.control(this.courseId)); + this.eventForm.addControl('groupcourseid', this.fb.control('')); + this.eventForm.addControl('groupid', this.groupControl); + this.eventForm.addControl('description', this.descriptionControl); + this.eventForm.addControl('location', this.fb.control('')); + this.eventForm.addControl('duration', this.fb.control(0)); + this.eventForm.addControl('timedurationuntil', this.fb.control(new Date().toISOString())); + this.eventForm.addControl('timedurationminutes', this.fb.control('')); + this.eventForm.addControl('repeat', this.fb.control(false)); + this.eventForm.addControl('repeats', this.fb.control('1')); + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + this.fetchData().finally(() => { + this.originalData = this.utils.clone(this.eventForm.value); + this.loaded = true; + }); + } + + /** + * Fetch the data needed to render the form. + * + * @return {Promise} Promise resolved when done. + */ + protected fetchData(): Promise { + let accessInfo; + + // Get access info. + return this.calendarProvider.getAccessInformation().then((info) => { + accessInfo = info; + + return this.calendarProvider.getAllowedEventTypes(); + }).then((types) => { + this.types = types; + + const promises = [], + eventTypes = this.calendarHelper.getEventTypeOptions(types); + + if (!eventTypes.length) { + return Promise.reject(this.translate.instant('addon.calendar.nopermissiontoupdatecalendar')); + } + + if (types.category) { + // Get the categories. + promises.push(this.coursesProvider.getCategories(0, true).then((cats) => { + this.categories = cats; + })); + } + + this.showAll = this.utils.isTrueOrOne(this.currentSite.getStoredConfig('calendar_adminseesall')) && + accessInfo.canmanageentries; + + if (types.course || types.groups) { + // Get the courses. + const promise = this.showAll ? this.coursesProvider.getCoursesByField() : this.coursesProvider.getUserCourses(); + + promises.push(promise.then((courses) => { + if (this.showAll) { + // Remove site home from the list of courses. + const siteHomeId = this.currentSite.getSiteHomeId(); + courses = courses.filter((course) => { + return course.id != siteHomeId; + }); + } + + // Sort courses by name. + this.courses = courses.sort((a, b) => { + const compareA = a.fullname.toLowerCase(), + compareB = b.fullname.toLowerCase(); + + return compareA.localeCompare(compareB); + }); + })); + } + + return Promise.all(promises).then(() => { + // Set event types. If course is allowed, select it first. + if (types.course) { + this.eventTypeControl.setValue(AddonCalendarProvider.TYPE_COURSE); + } else { + this.eventTypeControl.setValue(eventTypes[0].value); + } + + this.eventTypes = eventTypes; + }); + + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'Error getting data.'); + this.originalData = null; // Avoid asking for confirmation. + this.navCtrl.pop(); + }); + } + + /** + * Pull to refresh. + * + * @param {any} refresher Refresher. + */ + refreshData(refresher: any): void { + const promises = [ + this.calendarProvider.invalidateAccessInformation(this.courseId), + this.calendarProvider.invalidateAllowedEventTypes(this.courseId) + ]; + + if (this.types) { + if (this.types.category) { + promises.push(this.coursesProvider.invalidateCategories(0, true)); + } + if (this.types.course || this.types.groups) { + if (this.showAll) { + promises.push(this.coursesProvider.invalidateCoursesByField()); + } else { + promises.push(this.coursesProvider.invalidateUserCourses()); + } + } + } + + Promise.all(promises).finally(() => { + this.fetchData().finally(() => { + refresher.complete(); + }); + }); + } + + /** + * A course was selected, get its groups. + * + * @param {number} courseId Course ID. + */ + groupCourseSelected(courseId: number): void { + if (!courseId) { + return; + } + + const modal = this.domUtils.showModalLoading(); + this.loadingGroups = true; + + this.groupsProvider.getUserGroupsInCourse(courseId).then((groups) => { + this.groups = groups; + this.courseGroupSet = true; + this.groupControl.setValue(''); + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'Error getting data.'); + }).finally(() => { + this.loadingGroups = false; + modal.dismiss(); + }); + } + + /** + * Show or hide advanced form fields. + */ + toggleAdvanced(): void { + this.advanced = !this.advanced; + } + + /** + * Create the event. + */ + submit(): void { + // Validate data. + const formData = this.eventForm.value, + timeStartDate = new Date(formData.timestart), + timeUntilDate = new Date(formData.timedurationuntil), + timeDurationMinutes = parseInt(formData.timedurationminutes || '', 10); + let error; + + if (formData.eventtype == AddonCalendarProvider.TYPE_COURSE && !formData.courseid) { + error = 'core.selectacourse'; + } else if (formData.eventtype == AddonCalendarProvider.TYPE_GROUP && !formData.groupcourseid) { + error = 'core.selectacourse'; + } else if (formData.eventtype == AddonCalendarProvider.TYPE_GROUP && !formData.groupid) { + error = 'core.selectagroup'; + } else if (formData.eventtype == AddonCalendarProvider.TYPE_CATEGORY && !formData.categoryid) { + error = 'core.selectacategory'; + } else if (formData.duration == 1 && timeStartDate.getTime() > timeUntilDate.getTime()) { + error = 'addon.calendar.invalidtimedurationuntil'; + } else if (formData.duration == 2 && (isNaN(timeDurationMinutes) || timeDurationMinutes < 1)) { + error = 'addon.calendar.invalidtimedurationminutes'; + } + + if (error) { + // Show error and stop. + this.domUtils.showErrorModal(this.translate.instant(error)); + + return; + } + + // Format the data to send. + const data: any = { + name: formData.name, + eventtype: formData.eventtype, + timestart: Math.floor(timeStartDate.getTime() / 1000), + description: { + text: formData.description, + format: 1 + }, + location: formData.location, + duration: formData.duration, + repeat: formData.repeat + }; + + if (formData.eventtype == AddonCalendarProvider.TYPE_COURSE) { + data.courseid = formData.courseid; + } else if (formData.eventtype == AddonCalendarProvider.TYPE_GROUP) { + data.groupcourseid = formData.groupcourseid; + data.groupid = formData.groupid; + } else if (formData.eventtype == AddonCalendarProvider.TYPE_CATEGORY) { + data.categoryid = formData.categoryid; + } + + if (formData.duration == 1) { + data.timedurationuntil = Math.floor(timeUntilDate.getTime() / 1000); + } else if (formData.duration == 2) { + data.timedurationminutes = formData.timedurationminutes; + } + + if (formData.repeat) { + data.repeats = formData.repeats; + } + + // Send the data. + const modal = this.domUtils.showModalLoading('core.sending'); + + this.calendarProvider.submitEvent(this.eventId, data).then((event) => { + this.returnToList(event); + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'Error sending data.'); + }).finally(() => { + modal.dismiss(); + }); + } + + /** + * Convenience function to update or return to event list depending on device. + * + * @param {number} [event] Event. + */ + protected returnToList(event?: any): void { + const data: any = { + event: event + }; + this.eventsProvider.trigger(AddonCalendarProvider.NEW_EVENT_EVENT, data, this.currentSite.getId()); + + if (this.svComponent && this.svComponent.isOn()) { + // Empty form. + this.hasOffline = false; + this.eventForm.reset(this.originalData); + this.originalData = this.utils.clone(this.eventForm.value); + } else { + this.originalData = null; // Avoid asking for confirmation. + this.navCtrl.pop(); + } + } + + /** + * Discard an offline saved discussion. + */ + discard(): void { + this.domUtils.showConfirm(this.translate.instant('core.areyousure')).then(() => { + // @todo. + }).catch(() => { + // Cancelled. + }); + } + + /** + * Check if we can leave the page or not. + * + * @return {boolean|Promise} Resolved if we can leave it, rejected if not. + */ + ionViewCanLeave(): boolean | Promise { + + if (this.calendarHelper.hasEventDataChanged(this.eventForm.value, this.originalData)) { + // Show confirmation if some data has been modified. + return this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit')); + } else { + return Promise.resolve(); + } + } +} diff --git a/src/addon/calendar/pages/list/list.html b/src/addon/calendar/pages/list/list.html index e5cdc3eb0..97ae57d40 100644 --- a/src/addon/calendar/pages/list/list.html +++ b/src/addon/calendar/pages/list/list.html @@ -40,5 +40,12 @@ + + + + + \ No newline at end of file diff --git a/src/addon/calendar/pages/list/list.ts b/src/addon/calendar/pages/list/list.ts index 7722dc1c4..237ad2457 100644 --- a/src/addon/calendar/pages/list/list.ts +++ b/src/addon/calendar/pages/list/list.ts @@ -54,6 +54,7 @@ export class AddonCalendarListPage implements OnDestroy { protected obsDefaultTimeChange: any; protected eventId: number; protected preSelectedCourseId: number; + protected newEventObserver: any; courses: any[]; eventsLoaded = false; @@ -65,6 +66,7 @@ export class AddonCalendarListPage implements OnDestroy { filter = { course: this.allCourses }; + canCreate = false; constructor(private translate: TranslateService, private calendarProvider: AddonCalendarProvider, navParams: NavParams, private domUtils: CoreDomUtilsProvider, private coursesProvider: CoreCoursesProvider, private utils: CoreUtilsProvider, @@ -74,6 +76,7 @@ export class AddonCalendarListPage implements OnDestroy { this.siteHomeId = sitesProvider.getCurrentSite().getSiteHomeId(); this.notificationsEnabled = localNotificationsProvider.isAvailable(); + if (this.notificationsEnabled) { // Re-schedule events if default time changes. this.obsDefaultTimeChange = eventsProvider.on(AddonCalendarProvider.DEFAULT_NOTIFICATION_TIME_CHANGED, () => { @@ -83,6 +86,30 @@ export class AddonCalendarListPage implements OnDestroy { this.eventId = navParams.get('eventId') || false; this.preSelectedCourseId = navParams.get('courseId') || null; + + // Listen for events added. When an event is added, we reload the data. + this.newEventObserver = eventsProvider.on(AddonCalendarProvider.NEW_EVENT_EVENT, (data) => { + if (data && data.event) { + if (this.splitviewCtrl.isOn()) { + // Discussion added, clear details page. + this.splitviewCtrl.emptyDetails(); + } + + this.eventsLoaded = false; + this.refreshEvents(false).finally(() => { + this.eventsLoaded = true; + + // In tablet mode try to open the event. + if (this.splitviewCtrl.isOn()) { + if (data.event.id) { + this.gotoEvent(data.event.id); + } else { + // It's an offline event. + } + } + }); + } + }, sitesProvider.getCurrentSiteId()); } /** @@ -114,8 +141,19 @@ export class AddonCalendarListPage implements OnDestroy { this.daysLoaded = 0; this.emptyEventsTimes = 0; + const promises = []; + + 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; + })); + } + // Load courses for the popover. - return this.coursesProvider.getUserCourses(false).then((courses) => { + promises.push(this.coursesProvider.getUserCourses(false).then((courses) => { // Add "All courses". courses.unshift(this.allCourses); this.courses = courses; @@ -127,7 +165,9 @@ export class AddonCalendarListPage implements OnDestroy { } return this.fetchEvents(refresh); - }); + })); + + return Promise.all(promises); } /** @@ -308,21 +348,23 @@ export class AddonCalendarListPage implements OnDestroy { /** * Refresh the events. * - * @param {any} refresher Refresher. + * @param {any} [refresher] Refresher. + * @return {Promise} Promise resolved when done. */ - refreshEvents(refresher: any): void { + refreshEvents(refresher?: any): Promise { const promises = []; promises.push(this.calendarProvider.invalidateEventsList()); + promises.push(this.calendarProvider.invalidateAllowedEventTypes()); if (this.categoriesRetrieved) { promises.push(this.coursesProvider.invalidateCategories(0, true)); this.categoriesRetrieved = false; } - Promise.all(promises).finally(() => { - this.fetchData(true).finally(() => { - refresher.complete(); + return Promise.all(promises).finally(() => { + return this.fetchData(true).finally(() => { + refresher && refresher.complete(); }); }); } @@ -384,6 +426,18 @@ export class AddonCalendarListPage implements OnDestroy { }); } + /** + * Open page to create an event. + */ + openCreate(): void { + const params: any = {}; + if (this.filter.course.id != this.allCourses.id) { + params.courseId = this.filter.course.id; + } + + this.splitviewCtrl.push('AddonCalendarEditEventPage', params); + } + /** * Open calendar events settings. */ @@ -406,5 +460,6 @@ export class AddonCalendarListPage implements OnDestroy { */ ngOnDestroy(): void { this.obsDefaultTimeChange && this.obsDefaultTimeChange.off(); + this.newEventObserver && this.newEventObserver.off(); } } diff --git a/src/addon/calendar/providers/calendar.ts b/src/addon/calendar/providers/calendar.ts index 40dcd2208..23f664371 100644 --- a/src/addon/calendar/providers/calendar.ts +++ b/src/addon/calendar/providers/calendar.ts @@ -18,6 +18,7 @@ import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites'; import { CoreSite } from '@classes/site'; import { CoreCoursesProvider } from '@core/courses/providers/courses'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; +import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreGroupsProvider } from '@providers/groups'; import { CoreConstants } from '@core/constants'; import { CoreLocalNotificationsProvider } from '@providers/local-notifications'; @@ -35,6 +36,12 @@ export class AddonCalendarProvider { static DEFAULT_NOTIFICATION_TIME_CHANGED = 'AddonCalendarDefaultNotificationTimeChangedEvent'; static DEFAULT_NOTIFICATION_TIME_SETTING = 'mmaCalendarDefaultNotifTime'; static DEFAULT_NOTIFICATION_TIME = 60; + static NEW_EVENT_EVENT = 'addon_calendar_new_event'; + static TYPE_CATEGORY = 'category'; + static TYPE_COURSE = 'course'; + static TYPE_GROUP = 'group'; + static TYPE_SITE = 'site'; + static TYPE_USER = 'user'; protected ROOT_CACHE_KEY = 'mmaCalendar:'; // Variables for database. @@ -206,11 +213,37 @@ export class AddonCalendarProvider { constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private groupsProvider: CoreGroupsProvider, private coursesProvider: CoreCoursesProvider, private timeUtils: CoreTimeUtilsProvider, - private localNotificationsProvider: CoreLocalNotificationsProvider, private configProvider: CoreConfigProvider) { + private localNotificationsProvider: CoreLocalNotificationsProvider, private configProvider: CoreConfigProvider, + private utils: CoreUtilsProvider) { this.logger = logger.getInstance('AddonCalendarProvider'); this.sitesProvider.registerSiteSchema(this.siteSchema); } + /** + * Check if a certain site allows creating and editing events. + * + * @param {string} [siteId] Site Id. If not defined, use current site. + * @return {Promise} Promise resolved with true if can create/edit. + */ + canEditEvents(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return this.canEditEventsInSite(site); + }); + } + + /** + * Check if a certain site allows creating and editing events. + * + * @param {CoreSite} [site] Site. If not defined, use current site. + * @return {boolean} Whether events can be created and edited. + */ + canEditEventsInSite(site?: CoreSite): boolean { + site = site || this.sitesProvider.getCurrentSite(); + + // The WS to create/edit events requires a fix that was integrated in 3.7.1. + return site.isVersionGreaterEqualThan('3.7.1'); + } + /** * Removes expired events from local DB. * @@ -255,6 +288,39 @@ export class AddonCalendarProvider { }); } + /** + * Get access information for a calendar (either course calendar or site calendar). + * + * @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 object with access information. + * @since 3.7 + */ + getAccessInformation(courseId?: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params: any = {}, + preSets = { + cacheKey: this.getAccessInformationCacheKey(courseId) + }; + + if (courseId) { + params.courseid = courseId; + } + + return site.read('core_calendar_get_calendar_access_information', params, preSets); + }); + } + + /** + * Get cache key for calendar access information WS calls. + * + * @param {number} [courseId] Course ID. + * @return {string} Cache key. + */ + protected getAccessInformationCacheKey(courseId?: number): string { + return this.ROOT_CACHE_KEY + 'accessInformation:' + (courseId || 0); + } + /** * Get all calendar events from local Db. * @@ -267,6 +333,50 @@ export class AddonCalendarProvider { }); } + /** + * Get the type of events a user can create (either course calendar or site calendar). + * + * @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 an object indicating the types. + * @since 3.7 + */ + getAllowedEventTypes(courseId?: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params: any = {}, + preSets = { + cacheKey: this.getAllowedEventTypesCacheKey(courseId) + }; + + if (courseId) { + params.courseid = courseId; + } + + return site.read('core_calendar_get_allowed_event_types', params, preSets).then((response) => { + // Convert the array to an object. + const result = {}; + + if (response.allowedeventtypes) { + response.allowedeventtypes.map((type) => { + result[type] = true; + }); + } + + return result; + }); + }); + } + + /** + * Get cache key for calendar allowed event types WS calls. + * + * @param {number} [courseId] Course ID. + * @return {string} Cache key. + */ + protected getAllowedEventTypesCacheKey(courseId?: number): string { + return this.ROOT_CACHE_KEY + 'allowedEventTypes:' + (courseId || 0); + } + /** * Get the configured default notification time. * @@ -504,6 +614,32 @@ export class AddonCalendarProvider { return this.getEventsListPrefixCacheKey() + daysToStart + ':' + daysInterval; } + /** + * Invalidates access information. + * + * @param {number} [courseId] Course ID. If not defined, site calendar. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateAccessInformation(courseId?: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKey(this.getAccessInformationCacheKey(courseId)); + }); + } + + /** + * Invalidates allowed event types. + * + * @param {number} [courseId] Course ID. If not defined, site calendar. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateAllowedEventTypes(courseId?: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKey(this.getAllowedEventTypesCacheKey(courseId)); + }); + } + /** * Invalidates events list and all the single events and related info. * @@ -780,4 +916,35 @@ export class AddonCalendarProvider { })); }); } + + /** + * Submit an event, either to create it or to edit it. + * + * @param {number} eventId ID of the event. If undefined/null, create a new event. + * @param {any} formData Form data. + * @param {string} [siteId] Site ID. If not provided, current site. + * @return {Promise} Promise resolved when done. + */ + submitEvent(eventId: number, formData: any, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + // Add data that is "hidden" in web. + formData.id = eventId || 0; + formData.userid = site.getUserId(); + formData.visible = 1; + formData.instance = 0; + formData['_qf__core_calendar_local_event_forms_create'] = 1; + + const params = { + formdata: this.utils.objectToGetParams(formData) + }; + + return site.write('core_calendar_submit_create_update_form', params).then((result) => { + if (result.validationerror) { + return Promise.reject(null); + } + + return result.event; + }); + }); + } } diff --git a/src/addon/calendar/providers/helper.ts b/src/addon/calendar/providers/helper.ts index a1a85759d..818feadf1 100644 --- a/src/addon/calendar/providers/helper.ts +++ b/src/addon/calendar/providers/helper.ts @@ -15,6 +15,7 @@ import { Injectable } from '@angular/core'; import { CoreLoggerProvider } from '@providers/logger'; import { CoreCourseProvider } from '@core/course/providers/course'; +import { AddonCalendarProvider } from './calendar'; /** * Service that provides some features regarding lists of courses and categories. @@ -47,4 +48,73 @@ export class AddonCalendarHelperProvider { e.moduleIcon = e.icon; } } + + /** + * Get options (name & value) for each allowed event type. + * + * @param {any} eventTypes Result of getAllowedEventTypes. + * @return {{name: string, value: string}[]} Options. + */ + getEventTypeOptions(eventTypes: any): {name: string, value: string}[] { + const options = []; + + if (eventTypes.user) { + options.push({name: 'core.user', value: AddonCalendarProvider.TYPE_USER}); + } + if (eventTypes.group) { + options.push({name: 'core.group', value: AddonCalendarProvider.TYPE_GROUP}); + } + if (eventTypes.course) { + options.push({name: 'core.course', value: AddonCalendarProvider.TYPE_COURSE}); + } + if (eventTypes.category) { + options.push({name: 'core.category', value: AddonCalendarProvider.TYPE_CATEGORY}); + } + if (eventTypes.site) { + options.push({name: 'core.site', value: AddonCalendarProvider.TYPE_SITE}); + } + + return options; + } + + /** + * Check if the data of an event has changed. + * + * @param {any} data Current data. + * @param {any} [original] Original data. + * @return {boolean} True if data has changed, false otherwise. + */ + hasEventDataChanged(data: any, original?: any): boolean { + if (!original) { + // There is no original data, assume it hasn't changed. + return false; + } + + // Check the fields that don't depend on any other. + if (data.name != original.name || data.timestart != original.timestart || data.eventtype != original.eventtype || + data.description != original.description || data.location != original.location || + data.duration != original.duration || data.repeat != original.repeat) { + return true; + } + + // Check data that depends on eventtype. + if ((data.eventtype == AddonCalendarProvider.TYPE_CATEGORY && data.categoryid != original.categoryid) || + (data.eventtype == AddonCalendarProvider.TYPE_COURSE && data.courseid != original.courseid) || + (data.eventtype == AddonCalendarProvider.TYPE_GROUP && data.groupcourseid != original.groupcourseid && + data.groupid != original.groupid)) { + return true; + } + + // Check data that depends on duration. + if ((data.duration == 1 && data.timedurationuntil != original.timedurationuntil) || + (data.duration == 2 && data.timedurationminutes != original.timedurationminutes)) { + return true; + } + + if (data.repeat && data.repeats != original.repeats) { + return true; + } + + return false; + } } diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 0caa80266..2ed0480e9 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -87,13 +87,26 @@ "addon.calendar.calendarevents": "Calendar events", "addon.calendar.calendarreminders": "Calendar reminders", "addon.calendar.defaultnotificationtime": "Default notification time", + "addon.calendar.durationminutes": "Duration in minutes", + "addon.calendar.durationnone": "Without duration", + "addon.calendar.durationuntil": "Until", + "addon.calendar.editevent": "Editing event", "addon.calendar.errorloadevent": "Error loading event.", "addon.calendar.errorloadevents": "Error loading events.", + "addon.calendar.eventduration": "Duration", "addon.calendar.eventendtime": "End time", + "addon.calendar.eventname": "Event title", "addon.calendar.eventstarttime": "Start time", + "addon.calendar.eventtype": "Event type", "addon.calendar.gotoactivity": "Go to activity", + "addon.calendar.invalidtimedurationminutes": "The duration in minutes you have entered is invalid. Please enter the duration in minutes greater than 0 or select no duration.", + "addon.calendar.invalidtimedurationuntil": "The date and time you selected for duration until is before the start time of the event. Please correct this before proceeding.", + "addon.calendar.newevent": "New event", "addon.calendar.noevents": "There are no events", + "addon.calendar.nopermissiontoupdatecalendar": "Sorry, but you do not have permission to update the calendar event", "addon.calendar.reminders": "Reminders", + "addon.calendar.repeatevent": "Repeat this event", + "addon.calendar.repeatweeksl": "Repeat weekly, creating altogether", "addon.calendar.setnewreminder": "Set a new reminder", "addon.calendar.typecategory": "Category event", "addon.calendar.typeclose": "Close event", @@ -1328,6 +1341,7 @@ "core.course.warningmanualcompletionmodified": "The manual completion of an activity was modified on the site.", "core.course.warningofflinemanualcompletiondeleted": "Some offline manual completion of course '{{name}}' has been deleted. {{error}}", "core.coursedetails": "Course details", + "core.coursenogroups": "This course doesn't have any group.", "core.courses.addtofavourites": "Star this course", "core.courses.allowguests": "This course allows guest users to enter", "core.courses.availablecourses": "Available courses", @@ -1457,6 +1471,7 @@ "core.grades.range": "Range", "core.grades.rank": "Rank", "core.grades.weight": "Weight", + "core.group": "Group", "core.groupsseparate": "Separate groups", "core.groupsvisible": "Visible groups", "core.hasdatatosync": "This {{$a}} has offline data to be synchronised.", @@ -1624,6 +1639,7 @@ "core.nopermissionerror": "Sorry, but you do not currently have permissions to do that", "core.nopermissions": "Sorry, but you do not currently have permissions to do that ({{$a}}).", "core.noresults": "No results", + "core.noselection": "No selection", "core.notapplicable": "n/a", "core.notenrolledprofile": "This profile is not available because this user is not enrolled in this course.", "core.notice": "Notice", @@ -1692,6 +1708,9 @@ "core.sec": "sec", "core.secs": "secs", "core.seemoredetail": "Click here to see more detail", + "core.selectacategory": "Please select a category", + "core.selectacourse": "Select a course", + "core.selectagroup": "Select a group", "core.send": "Send", "core.sending": "Sending", "core.serverconnection": "Error connecting to the server", @@ -1760,6 +1779,7 @@ "core.sharedfiles.sharedfiles": "Shared files", "core.sharedfiles.successstorefile": "File successfully stored. Select the file to upload to your private files or use in an activity.", "core.show": "Show", + "core.showless": "Show less...", "core.showmore": "Show more...", "core.site": "Site", "core.sitehome.sitehome": "Site home", @@ -1808,6 +1828,7 @@ "core.unlimited": "Unlimited", "core.unzipping": "Unzipping", "core.upgraderunning": "Site is being upgraded, please retry later.", + "core.user": "User", "core.user.address": "Address", "core.user.city": "City/town", "core.user.contact": "Contact", diff --git a/src/lang/en.json b/src/lang/en.json index fe9102832..b5c79cb94 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -51,6 +51,7 @@ "copiedtoclipboard": "Text copied to clipboard", "course": "Course", "coursedetails": "Course details", + "coursenogroups": "This course doesn't have any group.", "currentdevice": "Current device", "datastoredoffline": "Data stored in the device because it couldn't be sent. It will be sent automatically later.", "date": "Date", @@ -104,6 +105,7 @@ "forcepasswordchangenotice": "You must change your password to proceed.", "fulllistofcourses": "All courses", "fullnameandsitename": "{{fullname}} ({{sitename}})", + "group": "Group", "groupsseparate": "Separate groups", "groupsvisible": "Visible groups", "hasdatatosync": "This {{$a}} has offline data to be synchronised.", @@ -174,6 +176,7 @@ "nopermissionerror": "Sorry, but you do not currently have permissions to do that", "nopermissions": "Sorry, but you do not currently have permissions to do that ({{$a}}).", "noresults": "No results", + "noselection": "No selection", "notapplicable": "n/a", "notenrolledprofile": "This profile is not available because this user is not enrolled in this course.", "notice": "Notice", @@ -214,10 +217,14 @@ "sec": "sec", "secs": "secs", "seemoredetail": "Click here to see more detail", + "selectacategory": "Please select a category", + "selectacourse": "Select a course", + "selectagroup": "Select a group", "send": "Send", "sending": "Sending", "serverconnection": "Error connecting to the server", "show": "Show", + "showless": "Show less...", "showmore": "Show more...", "site": "Site", "sitemaintenance": "The site is undergoing maintenance and is currently not available", @@ -264,6 +271,7 @@ "unlimited": "Unlimited", "unzipping": "Unzipping", "upgraderunning": "Site is being upgraded, please retry later.", + "user": "User", "userdeleted": "This user account has been deleted", "userdetails": "User details", "usernotfullysetup": "User not fully set-up", diff --git a/src/providers/utils/utils.ts b/src/providers/utils/utils.ts index e1595c75b..0f9f71b3b 100644 --- a/src/providers/utils/utils.ts +++ b/src/providers/utils/utils.ts @@ -376,13 +376,15 @@ export class CoreUtilsProvider { } /** - * Flatten an object, moving subobjects' properties to the first level using dot notation. E.g.: - * {a: {b: 1, c: 2}, d: 3} -> {'a.b': 1, 'a.c': 2, d: 3} + * Flatten an object, moving subobjects' properties to the first level. + * It supports 2 notations: dot notation and square brackets. + * E.g.: {a: {b: 1, c: 2}, d: 3} -> {'a.b': 1, 'a.c': 2, d: 3} * * @param {object} obj Object to flatten. - * @return {object} Flatten object. + * @param {boolean} [useDotNotation] Whether to use dot notation '.' or square brackets '['. + * @return {object} Flattened object. */ - flattenObject(obj: object): object { + flattenObject(obj: object, useDotNotation?: boolean): object { const toReturn = {}; for (const name in obj) { @@ -398,7 +400,8 @@ export class CoreUtilsProvider { continue; } - toReturn[name + '.' + subName] = flatObject[subName]; + const newName = useDotNotation ? name + '.' + subName : name + '[' + subName + ']'; + toReturn[newName] = flatObject[subName]; } } else { toReturn[name] = value; @@ -1051,6 +1054,37 @@ export class CoreUtilsProvider { return mapped; } + /** + * Convert an object to a format of GET param. E.g.: {a: 1, b: 2} -> a=1&b=2 + * + * @param {any} object Object to convert. + * @param {boolean} [removeEmpty=true] Whether to remove params whose value is empty/null/undefined. + * @return {string} GET params. + */ + objectToGetParams(object: any, removeEmpty: boolean = true): string { + // First of all, flatten the object so all properties are in the first level. + const flattened = this.flattenObject(object); + let result = '', + joinChar = ''; + + for (const name in flattened) { + let value = flattened[name]; + + if (removeEmpty && (value === null || typeof value == 'undefined' || value === '')) { + continue; + } + + if (typeof value == 'boolean') { + value = value ? 1 : 0; + } + + result += joinChar + name + '=' + value; + joinChar = '&'; + } + + return result; + } + /** * Add a prefix to all the keys in an object. *