diff --git a/src/addon/calendar/providers/calendar.ts b/src/addon/calendar/providers/calendar.ts index 741160a88..318800396 100644 --- a/src/addon/calendar/providers/calendar.ts +++ b/src/addon/calendar/providers/calendar.ts @@ -514,7 +514,7 @@ export class AddonCalendarProvider { notificationtime: e.notificationtime || -1 }; - return db.insertOrUpdateRecord(this.EVENTS_TABLE, eventRecord, { id: eventRecord.id }); + return db.insertRecord(this.EVENTS_TABLE, eventRecord); })); }); @@ -539,7 +539,7 @@ export class AddonCalendarProvider { event.notificationtime = time; - return site.getDb().insertOrUpdateRecord(this.EVENTS_TABLE, event, { id: event.id }).then(() => { + return site.getDb().insertRecord(this.EVENTS_TABLE, event).then(() => { return this.scheduleEventNotification(event, time); }); }); diff --git a/src/addon/competency/competency.module.ts b/src/addon/competency/competency.module.ts index 83ae4bd28..7d31bae23 100644 --- a/src/addon/competency/competency.module.ts +++ b/src/addon/competency/competency.module.ts @@ -22,10 +22,6 @@ import { AddonCompetencyComponentsModule } from './components/components.module' import { CoreCourseOptionsDelegate } from '@core/course/providers/options-delegate'; import { CoreMainMenuDelegate } from '@core/mainmenu/providers/delegate'; import { CoreUserDelegate } from '@core/user/providers/user-delegate'; -import { CoreUserProvider } from '@core/user/providers/user'; -import { CoreEventsProvider } from '@providers/events'; -import { CoreSitesProvider } from '@providers/sites'; -import { CoreCoursesProvider } from '@core/courses/providers/courses'; @NgModule({ declarations: [ @@ -44,24 +40,10 @@ import { CoreCoursesProvider } from '@core/courses/providers/courses'; export class AddonCompetencyModule { constructor(mainMenuDelegate: CoreMainMenuDelegate, mainMenuHandler: AddonCompetencyMainMenuHandler, courseOptionsDelegate: CoreCourseOptionsDelegate, courseOptionHandler: AddonCompetencyCourseOptionHandler, - userDelegate: CoreUserDelegate, userHandler: AddonCompetencyUserHandler, - eventsProvider: CoreEventsProvider, sitesProvider: CoreSitesProvider) { + userDelegate: CoreUserDelegate, userHandler: AddonCompetencyUserHandler) { mainMenuDelegate.registerHandler(mainMenuHandler); courseOptionsDelegate.registerHandler(courseOptionHandler); userDelegate.registerHandler(userHandler); - - eventsProvider.on(CoreEventsProvider.LOGOUT, () => { - courseOptionHandler.clearCoursesNavCache(); - userHandler.clearUsersNavCache(); - }, sitesProvider.getCurrentSiteId()); - - eventsProvider.on(CoreCoursesProvider.EVENT_MY_COURSES_REFRESHED, () => { - courseOptionHandler.clearCoursesNavCache(); - }, sitesProvider.getCurrentSiteId()); - - eventsProvider.on(CoreUserProvider.PROFILE_REFRESHED, () => { - userHandler.clearUsersNavCache(); - }, sitesProvider.getCurrentSiteId()); } } diff --git a/src/addon/competency/providers/course-option-handler.ts b/src/addon/competency/providers/course-option-handler.ts index fe7bdd9d8..4aee5b3dc 100644 --- a/src/addon/competency/providers/course-option-handler.ts +++ b/src/addon/competency/providers/course-option-handler.ts @@ -26,17 +26,8 @@ export class AddonCompetencyCourseOptionHandler implements CoreCourseOptionsHand name = 'AddonCompetency'; priority = 700; - protected coursesNavEnabledCache = {}; - constructor(private competencyProvider: AddonCompetencyProvider) {} - /** - * Clear courses nav cache. - */ - clearCoursesNavCache(): void { - this.coursesNavEnabledCache = {}; - } - /** * Whether or not the handler is enabled ona site level. * @return {boolean|Promise} Whether or not the handler is enabled on a site level. @@ -63,15 +54,8 @@ export class AddonCompetencyCourseOptionHandler implements CoreCourseOptionsHand return navOptions.competencies; } - if (typeof this.coursesNavEnabledCache[courseId] != 'undefined') { - return this.coursesNavEnabledCache[courseId]; - } - return this.competencyProvider.isPluginForCourseEnabled(courseId).then((competencies) => { - const enabled = competencies ? !competencies.canmanagecoursecompetencies : false; - this.coursesNavEnabledCache[courseId] = enabled; - - return enabled; + return competencies ? !competencies.canmanagecoursecompetencies : false; }); } diff --git a/src/addon/competency/providers/user-handler.ts b/src/addon/competency/providers/user-handler.ts index a80b6fa57..ffc4a7c51 100644 --- a/src/addon/competency/providers/user-handler.ts +++ b/src/addon/competency/providers/user-handler.ts @@ -16,6 +16,8 @@ import { Injectable } from '@angular/core'; import { CoreUserDelegate, CoreUserProfileHandler, CoreUserProfileHandlerData } from '@core/user/providers/user-delegate'; import { CoreSitesProvider } from '@providers/sites'; import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreUserProvider } from '@core/user/providers/user'; import { AddonCompetencyProvider } from './competency'; /** @@ -30,13 +32,15 @@ export class AddonCompetencyUserHandler implements CoreUserProfileHandler { usersNavEnabledCache = {}; constructor(private linkHelper: CoreContentLinksHelperProvider, protected sitesProvider: CoreSitesProvider, - private competencyProvider: AddonCompetencyProvider) { + private competencyProvider: AddonCompetencyProvider, eventsProvider: CoreEventsProvider) { + eventsProvider.on(CoreEventsProvider.LOGOUT, this.clearUsersNavCache.bind(this)); + eventsProvider.on(CoreUserProvider.PROFILE_REFRESHED, this.clearUsersNavCache.bind(this)); } /** * Clear users nav cache. */ - clearUsersNavCache(): void { + private clearUsersNavCache(): void { this.participantsNavEnabledCache = {}; this.usersNavEnabledCache = {}; } diff --git a/src/addon/messages/providers/messages-offline.ts b/src/addon/messages/providers/messages-offline.ts index ee8c5a7dd..a2243f6b9 100644 --- a/src/addon/messages/providers/messages-offline.ts +++ b/src/addon/messages/providers/messages-offline.ts @@ -148,11 +148,7 @@ export class AddonMessagesOfflineProvider { deviceoffline: this.appProvider.isOnline() ? 0 : 1 }; - return site.getDb().insertOrUpdateRecord(this.MESSAGES_TABLE, entry, { - touserid: toUserId, - smallmessage: message, - timecreated: entry.timecreated - }).then(() => { + return site.getDb().insertRecord(this.MESSAGES_TABLE, entry).then(() => { return entry; }); }); @@ -173,11 +169,7 @@ export class AddonMessagesOfflineProvider { data = { deviceoffline: value ? 1 : 0 }; messages.forEach((message) => { - promises.push(db.insertOrUpdateRecord(this.MESSAGES_TABLE, data, { - touserid: message.touserid, - smallmessage: message.smallmessage, - timecreated: message.timecreated - })); + promises.push(db.insertRecord(this.MESSAGES_TABLE, data)); }); return Promise.all(promises); diff --git a/src/addon/notes/components/components.module.ts b/src/addon/notes/components/components.module.ts new file mode 100644 index 000000000..53a7a2457 --- /dev/null +++ b/src/addon/notes/components/components.module.ts @@ -0,0 +1,45 @@ +// (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 { CommonModule } from '@angular/common'; +import { IonicModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { CorePipesModule } from '@pipes/pipes.module'; +import { AddonNotesListComponent } from './list/list'; + +@NgModule({ + declarations: [ + AddonNotesListComponent + ], + imports: [ + CommonModule, + IonicModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule, + CorePipesModule + ], + providers: [ + ], + exports: [ + AddonNotesListComponent + ], + entryComponents: [ + AddonNotesListComponent + ] +}) +export class AddonNotesComponentsModule {} diff --git a/src/addon/notes/components/list/list.html b/src/addon/notes/components/list/list.html new file mode 100644 index 000000000..5b2fd8ebc --- /dev/null +++ b/src/addon/notes/components/list/list.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + {{ 'addon.notes.sitenotes' | translate }} + {{ 'addon.notes.coursenotes' | translate }} + {{ 'addon.notes.personalnotes' | translate }} + + +
+ + {{ 'core.thereisdatatosync' | translate:{$a: 'addon.notes.notes' | translate | lowercase } }} +
+ + + + + + + + + +

{{note.userfullname}}

+

{{note.lastmodified | coreDateDayOrTime}}

+

{{ 'core.notsent' | translate }}

+
+ + + +
+
+
+
diff --git a/src/addon/notes/components/list/list.ts b/src/addon/notes/components/list/list.ts new file mode 100644 index 000000000..f41af7dec --- /dev/null +++ b/src/addon/notes/components/list/list.ts @@ -0,0 +1,171 @@ +// (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, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { Content } from 'ionic-angular'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { AddonNotesProvider } from '../../providers/notes'; +import { AddonNotesSyncProvider } from '../../providers/notes-sync'; + +/** + * Component that displays the notes of a course. + */ +@Component({ + selector: 'addon-notes-list', + templateUrl: 'list.html', +}) +export class AddonNotesListComponent implements OnInit, OnDestroy { + @Input() courseId: number; + + @ViewChild(Content) content: Content; + + protected syncObserver: any; + + type = 'course'; + refreshIcon = 'spinner'; + syncIcon = 'spinner'; + notes: any[]; + hasOffline = false; + notesLoaded = false; + + constructor(private domUtils: CoreDomUtilsProvider, private textUtils: CoreTextUtilsProvider, + sitesProvider: CoreSitesProvider, eventsProvider: CoreEventsProvider, + private notesProvider: AddonNotesProvider, private notesSync: AddonNotesSyncProvider) { + // Refresh data if notes are synchronized automatically. + this.syncObserver = eventsProvider.on(AddonNotesSyncProvider.AUTO_SYNCED, (data) => { + if (data.courseId == this.courseId) { + // Show the sync warnings. + this.showSyncWarnings(data.warnings); + + // Refresh the data. + this.notesLoaded = false; + this.refreshIcon = 'spinner'; + this.syncIcon = 'spinner'; + + this.content.scrollToTop(); + this.fetchNotes(false); + } + }, sitesProvider.getCurrentSiteId()); + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + this.fetchNotes(true).then(() => { + this.notesProvider.logView(this.courseId); + }); + } + + /** + * Fetch notes. + * + * @param {boolean} sync When to resync notes. + * @param {boolean} [showErrors] When to display errors or not. + * @return {Promise} Promise with the notes. + */ + private fetchNotes(sync: boolean, showErrors?: boolean): Promise { + const promise = sync ? this.syncNotes(showErrors) : Promise.resolve(); + + return promise.catch(() => { + // Ignore errors. + }).then(() => { + return this.notesProvider.getNotes(this.courseId).then((notes) => { + notes = notes[this.type + 'notes'] || []; + + this.hasOffline = notes.some((note) => note.offline); + + return this.notesProvider.getNotesUserData(notes, this.courseId).then((notes) => { + this.notes = notes; + }); + }); + }).catch((message) => { + this.domUtils.showErrorModal(message); + }).finally(() => { + this.notesLoaded = true; + this.refreshIcon = 'refresh'; + this.syncIcon = 'sync'; + }); + } + + /** + * Refresh notes on PTR. + * + * @param {boolean} showErrors Whether to display errors or not. + * @param {any} refresher Refresher instance. + */ + refreshNotes(showErrors: boolean, refresher?: any): void { + this.refreshIcon = 'spinner'; + this.syncIcon = 'spinner'; + this.notesProvider.invalidateNotes(this.courseId).finally(() => { + this.fetchNotes(true, showErrors).finally(() => { + if (refresher) { + refresher.complete(); + } + }); + }); + } + + /** + * Function called when the type has changed. + */ + typeChanged(): void { + this.notesLoaded = false; + this.refreshIcon = 'spinner'; + this.syncIcon = 'spinner'; + this.fetchNotes(true).then(() => { + this.notesProvider.logView(this.courseId); + }); + } + + /** + * Tries to synchronize course notes. + * + * @param {boolean} showErrors Whether to display errors or not. + * @return {Promise} Promise resolved if sync is successful, rejected otherwise. + */ + private syncNotes(showErrors: boolean): Promise { + return this.notesSync.syncNotes(this.courseId).then((warnings) => { + this.showSyncWarnings(warnings); + }).catch((error) => { + if (showErrors) { + this.domUtils.showErrorModalDefault(error, 'core.errorsync', true); + } + + return Promise.reject(null); + }); + } + + /** + * Show sync warnings if any. + * + * @param {string[]} warnings the warnings + */ + private showSyncWarnings(warnings: string[]): void { + const message = this.textUtils.buildMessage(warnings); + if (message) { + this.domUtils.showErrorModal(message); + } + } + + /** + * Page destroyed. + */ + ngOnDestroy(): void { + this.syncObserver && this.syncObserver.off(); + } +} diff --git a/src/addon/notes/lang/en.json b/src/addon/notes/lang/en.json new file mode 100644 index 000000000..761417b79 --- /dev/null +++ b/src/addon/notes/lang/en.json @@ -0,0 +1,13 @@ +{ + "addnewnote": "Add a new note", + "coursenotes": "Course notes", + "eventnotecreated": "Note created", + "nonotes": "There are no notes of this type yet.", + "note": "Note", + "notes": "Notes", + "personalnotes": "Personal notes", + "publishstate": "Context", + "sitenotes": "Site notes", + "userwithid": "User with ID {{id}}", + "warningnotenotsent": "Couldn't add note(s) to course {{course}}. {{error}}" +} \ No newline at end of file diff --git a/src/addon/notes/notes.module.ts b/src/addon/notes/notes.module.ts new file mode 100644 index 000000000..7dd83dc35 --- /dev/null +++ b/src/addon/notes/notes.module.ts @@ -0,0 +1,51 @@ +// (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 { AddonNotesProvider } from './providers/notes'; +import { AddonNotesOfflineProvider } from './providers/notes-offline'; +import { AddonNotesSyncProvider } from './providers/notes-sync'; +import { AddonNotesCourseOptionHandler } from './providers/course-option-handler'; +import { AddonNotesSyncCronHandler } from './providers/sync-cron-handler'; +import { AddonNotesUserHandler } from './providers/user-handler'; +import { AddonNotesComponentsModule } from './components/components.module'; +import { CoreCourseOptionsDelegate } from '@core/course/providers/options-delegate'; +import { CoreCronDelegate } from '@providers/cron'; +import { CoreUserDelegate } from '@core/user/providers/user-delegate'; + +@NgModule({ + declarations: [ + ], + imports: [ + AddonNotesComponentsModule + ], + providers: [ + AddonNotesProvider, + AddonNotesOfflineProvider, + AddonNotesSyncProvider, + AddonNotesCourseOptionHandler, + AddonNotesSyncCronHandler, + AddonNotesUserHandler + ] +}) +export class AddonNotesModule { + constructor(courseOptionsDelegate: CoreCourseOptionsDelegate, courseOptionHandler: AddonNotesCourseOptionHandler, + userDelegate: CoreUserDelegate, userHandler: AddonNotesUserHandler, + cronDelegate: CoreCronDelegate, syncHandler: AddonNotesSyncCronHandler) { + // Register handlers. + courseOptionsDelegate.registerHandler(courseOptionHandler); + userDelegate.registerHandler(userHandler); + cronDelegate.register(syncHandler); + } +} diff --git a/src/addon/notes/pages/add/add.html b/src/addon/notes/pages/add/add.html new file mode 100644 index 000000000..e0a41c7b1 --- /dev/null +++ b/src/addon/notes/pages/add/add.html @@ -0,0 +1,28 @@ + + + {{ 'addon.notes.addnewnote' | translate }} + + + + + + +
+ + {{ 'addon.notes.publishstate' | translate }} + + {{ 'addon.notes.personalnotes' | translate }} + {{ 'addon.notes.coursenotes' | translate }} + {{ 'addon.notes.sitenotes' | translate }} + + + + + + +
+
diff --git a/src/addon/notes/pages/add/add.module.ts b/src/addon/notes/pages/add/add.module.ts new file mode 100644 index 000000000..404012014 --- /dev/null +++ b/src/addon/notes/pages/add/add.module.ts @@ -0,0 +1,31 @@ +// (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 { AddonNotesAddPage } from './add'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreDirectivesModule } from '@directives/directives.module'; + +@NgModule({ + declarations: [ + AddonNotesAddPage + ], + imports: [ + CoreDirectivesModule, + IonicPageModule.forChild(AddonNotesAddPage), + TranslateModule.forChild() + ] +}) +export class AddonNotesAddPageModule {} diff --git a/src/addon/notes/pages/add/add.ts b/src/addon/notes/pages/add/add.ts new file mode 100644 index 000000000..f31f44528 --- /dev/null +++ b/src/addon/notes/pages/add/add.ts @@ -0,0 +1,69 @@ +// (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 } from '@angular/core'; +import { IonicPage, ViewController, NavParams } from 'ionic-angular'; +import { CoreAppProvider } from '@providers/app'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { AddonNotesProvider } from '../../providers/notes'; + +/** + * Component that displays a text area for composing a note. + */ +@IonicPage({ segment: 'addon-notes-add' }) +@Component({ + selector: 'page-addon-notes-add', + templateUrl: 'add.html', +}) +export class AddonNotesAddPage { + userId: number; + courseId: number; + publishState = 'personal'; + text = ''; + processing = false; + + constructor(params: NavParams, private viewCtrl: ViewController, private appProvider: CoreAppProvider, + private domUtils: CoreDomUtilsProvider, private notesProvider: AddonNotesProvider) { + this.userId = params.get('userId'); + this.courseId = params.get('courseId'); + } + + /** + * Send the note or store it offline. + */ + addNote(): void { + this.appProvider.closeKeyboard(); + const loadingModal = this.domUtils.showModalLoading('core.sending', true); + // Freeze the add note button. + this.processing = true; + this.notesProvider.addNote(this.userId, this.courseId, this.publishState, this.text).then((sent) => { + this.viewCtrl.dismiss().finally(() => { + const message = sent ? 'addon.notes.eventnotecreated' : 'core.datastoredoffline'; + this.domUtils.showAlertTranslated('core.success', message); + }); + }).catch((error) => { + this.domUtils.showErrorModal(error); + this.processing = false; + }).finally(() => { + loadingModal.dismiss(); + }); + } + + /** + * Close modal. + */ + closeModal(): void { + this.viewCtrl.dismiss(); + } +} diff --git a/src/addon/notes/providers/course-option-handler.ts b/src/addon/notes/providers/course-option-handler.ts new file mode 100644 index 000000000..34500726a --- /dev/null +++ b/src/addon/notes/providers/course-option-handler.ts @@ -0,0 +1,74 @@ +// (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, Injector } from '@angular/core'; +import { AddonNotesProvider } from './notes'; +import { CoreCourseProvider } from '@core/course/providers/course'; +import { CoreCourseOptionsHandler, CoreCourseOptionsHandlerData } from '@core/course/providers/options-delegate'; +import { AddonNotesListComponent } from '../components/list/list'; + +/** + * Handler to inject an option into the course main menu. + */ +@Injectable() +export class AddonNotesCourseOptionHandler implements CoreCourseOptionsHandler { + name = 'AddonNotes'; + priority = 200; + + constructor(private notesProvider: AddonNotesProvider) { + } + + /** + * Whether or not the handler is enabled on a site level. + * @return {boolean|Promise} Whether or not the handler is enabled on a site level. + */ + isEnabled(): boolean | Promise { + return this.notesProvider.isPluginEnabled(); + } + + /** + * Whether or not the handler is enabled for a certain course. + * + * @param {number} courseId The course ID. + * @param {any} accessData Access type and data. Default, guest, ... + * @param {any} [navOptions] Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions. + * @param {any} [admOptions] Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions. + * @return {boolean|Promise} True or promise resolved with true if enabled. + */ + isEnabledForCourse(courseId: number, accessData: any, navOptions?: any, admOptions?: any): boolean | Promise { + if (accessData && accessData.type == CoreCourseProvider.ACCESS_GUEST) { + return false; // Not enabled for guests. + } + + if (navOptions && typeof navOptions.notes != 'undefined') { + return navOptions.notes; + } + + return this.notesProvider.isPluginViewNotesEnabledForCourse(courseId); + } + + /** + * Returns the data needed to render the handler. + * + * @param {number} courseId The course ID. + * @return {CoreCourseOptionsHandlerData} Data. + */ + getDisplayData?(injector: Injector, courseId: number): CoreCourseOptionsHandlerData { + return { + title: 'addon.notes.notes', + class: 'addon-notes-course-handler', + component: AddonNotesListComponent, + }; + } +} diff --git a/src/addon/notes/providers/notes-offline.ts b/src/addon/notes/providers/notes-offline.ts new file mode 100644 index 000000000..597458ef8 --- /dev/null +++ b/src/addon/notes/providers/notes-offline.ts @@ -0,0 +1,227 @@ +// (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 { CoreLoggerProvider } from '@providers/logger'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreTimeUtilsProvider } from '@providers/utils/time'; + +/** + * Service to handle offline notes. + */ +@Injectable() +export class AddonNotesOfflineProvider { + protected logger; + + // Variables for database. + protected NOTES_TABLE = 'addon_notes_offline_notes'; + protected tablesSchema = [ + { + name: this.NOTES_TABLE, + columns: [ + { + name: 'userid', + type: 'INTEGER' + }, + { + name: 'courseid', + type: 'INTEGER' + }, + { + name: 'publishstate', + type: 'TEXT', + }, + { + name: 'content', + type: 'TEXT' + }, + { + name: 'format', + type: 'INTEGER' + }, + { + name: 'created', + type: 'INTEGER' + }, + { + name: 'lastmodified', + type: 'INTEGER' + } + ], + primaryKeys: ['userid', 'content', 'created'] + } + ]; + + constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private timeUtils: CoreTimeUtilsProvider) { + this.logger = logger.getInstance('AddonNotesOfflineProvider'); + this.sitesProvider.createTablesFromSchema(this.tablesSchema); + } + + /** + * Delete a note. + * + * @param {number} userId User ID the note is about. + * @param {string} content The note content. + * @param {number} timecreated The time the note was created. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if deleted, rejected if failure. + */ + deleteNote(userId: number, content: string, timecreated: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().deleteRecords(this.NOTES_TABLE, { + userid: userId, + content: content, + created: timecreated + }); + }); + } + + /** + * Get all offline notes. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with notes. + */ + getAllNotes(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().getRecords(this.NOTES_TABLE); + }); + } + + /** + * Get an offline note. + * + * @param {number} userId User ID the note is about. + * @param {string} content The note content. + * @param {number} timecreated The time the note was created. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the notes. + */ + getNote(userId: number, content: string, timecreated: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().getRecord(this.NOTES_TABLE, { + userid: userId, + content: content, + created: timecreated + }); + }); + } + + /** + * Get offline notes for a certain course. + * + * @param {number} courseId Course ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with notes. + */ + getNotesForCourse(courseId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().getRecords(this.NOTES_TABLE, {courseid: courseId}); + }); + } + + /** + * Get offline notes for a certain user. + * + * @param {number} userId User ID the notes are about. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with notes. + */ + getNotesForUser(userId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().getRecords(this.NOTES_TABLE, {userid: userId}); + }); + } + + /** + * Get offline notes with a certain publish state (Personal, Site or Course). + * + * @param {string} state Publish state ('personal', 'site' or 'course'). + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with notes. + */ + getNotesWithPublishState(state: string, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().getRecords(this.NOTES_TABLE, {publishstate: state}); + }); + } + + /** + * Check if there are offline notes for a certain course. + * + * @param {number} courseId Course ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with boolean: true if has offline notes, false otherwise. + */ + hasNotesForCourse(courseId: number, siteId?: string): Promise { + return this.getNotesForCourse(courseId, siteId).then((notes) => { + return !!notes.length; + }); + } + + /** + * Check if there are offline notes for a certain user. + * + * @param {number} userId User ID the notes are about. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with boolean: true if has offline notes, false otherwise. + */ + hasNotesForUser(userId: number, siteId?: string): Promise { + return this.getNotesForUser(userId, siteId).then((notes) => { + return !!notes.length; + }); + } + + /** + * Check if there are offline notes with a certain publish state (Personal, Site or Course). + * + * @param {string} state Publish state ('personal', 'site' or 'course'). + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with boolean: true if has offline notes, false otherwise. + */ + hasNotesWithPublishState(state: string, siteId?: string): Promise { + return this.getNotesWithPublishState(state, siteId).then((notes) => { + return !!notes.length; + }); + } + + /** + * Save a note to be sent later. + * + * @param {number} userId User ID the note is about. + * @param {number} courseId Course ID. + * @param {string} state Publish state ('personal', 'site' or 'course'). + * @param {string} content The note content. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if stored, rejected if failure. + */ + saveNote(userId: number, courseId: number, state: string, content: string, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const now = this.timeUtils.timestamp(); + const data = { + userid: userId, + courseid: courseId, + publishstate: state, + content: content, + format: 1, + created: now, + lastmodified: now + }; + + return site.getDb().insertRecord(this.NOTES_TABLE, data).then(() => { + return data; + }); + }); + } +} diff --git a/src/addon/notes/providers/notes-sync.ts b/src/addon/notes/providers/notes-sync.ts new file mode 100644 index 000000000..d443385db --- /dev/null +++ b/src/addon/notes/providers/notes-sync.ts @@ -0,0 +1,198 @@ +// (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 { CoreLoggerProvider } from '@providers/logger'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreSyncBaseProvider } from '@classes/base-sync'; +import { CoreAppProvider } from '@providers/app'; +import { AddonNotesOfflineProvider } from './notes-offline'; +import { AddonNotesProvider } from './notes'; +import { CoreCoursesProvider } from '@core/courses/providers/courses'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { TranslateService } from '@ngx-translate/core'; +import { CoreSyncProvider } from '@providers/sync'; + +/** + * Service to sync notes. + */ +@Injectable() +export class AddonNotesSyncProvider extends CoreSyncBaseProvider { + + static AUTO_SYNCED = 'addon_notes_autom_synced'; + + constructor(protected sitesProvider: CoreSitesProvider, protected loggerProvider: CoreLoggerProvider, + protected appProvider: CoreAppProvider, private notesOffline: AddonNotesOfflineProvider, + private eventsProvider: CoreEventsProvider, private notesProvider: AddonNotesProvider, + private coursesProvider: CoreCoursesProvider, private translate: TranslateService, private utils: CoreUtilsProvider, + syncProvider: CoreSyncProvider, protected textUtils: CoreTextUtilsProvider) { + super('AddonNotesSync', sitesProvider, loggerProvider, appProvider, syncProvider, textUtils); + } + + /** + * Try to synchronize all the notes in a certain site or in all sites. + * + * @param {string} [siteId] Site ID to sync. If not defined, sync all sites. + * @return {Promise} Promise resolved if sync is successful, rejected if sync fails. + */ + syncAllNotes(siteId?: string): Promise { + return this.syncOnSites('all notes', this.syncAllNotesFunc.bind(this), [], siteId); + } + + /** + * Synchronize all the notes in a certain site + * + * @param {string} siteId Site ID to sync. + * @return {Promise} Promise resolved if sync is successful, rejected if sync fails. + */ + private syncAllNotesFunc(siteId: string): Promise { + return this.notesOffline.getAllNotes(siteId).then((notes) => { + // Get all the courses to be synced. + const courseIds = []; + notes.forEach((note) => { + if (courseIds.indexOf(note.courseid) == -1) { + courseIds.push(note.courseid); + } + }); + + // Sync all courses. + const promises = courseIds.map((courseId) => { + return this.syncNotesIfNeeded(courseId, siteId).then((warnings) => { + if (typeof warnings != 'undefined') { + // Sync successful, send event. + this.eventsProvider.trigger(AddonNotesSyncProvider.AUTO_SYNCED, { + courseId: courseId, + warnings: warnings + }, siteId); + } + }); + }); + + return Promise.all(promises); + }); + } + + /** + * Sync course notes only if a certain time has passed since the last time. + * + * @param {number} courseId Course ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the notes are synced or if they don't need to be synced. + */ + private syncNotesIfNeeded(courseId: number, siteId?: string): Promise { + return this.isSyncNeeded(courseId, siteId).then((needed) => { + if (needed) { + return this.syncNotes(courseId, siteId); + } + }); + } + + /** + * Synchronize notes of a course. + * + * @param {number} courseId Course ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if sync is successful, rejected otherwise. + */ + syncNotes(courseId: number, siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + if (this.isSyncing(courseId, siteId)) { + // There's already a sync ongoing for notes, return the promise. + return this.getOngoingSync(courseId, siteId); + } + + this.logger.debug('Try to sync notes for course ' + courseId); + + const warnings = []; + + // Get offline notes to be sent. + const syncPromise = this.notesOffline.getNotesForCourse(courseId, siteId).then((notes) => { + if (!notes.length) { + // Nothing to sync. + return; + } else if (!this.appProvider.isOnline()) { + // Cannot sync in offline. + return Promise.reject(this.translate.instant('core.networkerrormsg')); + } + + const errors = []; + + // Format the notes to be sent. + const notesToSend = notes.map((note) => { + return { + userid: note.userid, + publishstate: note.publishstate, + courseid: note.courseid, + text: note.content, + format: note.format + }; + }); + + // Send the notes. + return this.notesProvider.addNotesOnline(notesToSend, siteId).then((response) => { + // Search errors in the response. + response.forEach((entry) => { + if (entry.noteid === -1 && errors.indexOf(entry.errormessage) == -1) { + errors.push(entry.errormessage); + } + }); + + // Fetch the notes from server to be sure they're up to date. + return this.notesProvider.invalidateNotes(courseId, siteId).then(() => { + return this.notesProvider.getNotes(courseId, false, true, siteId); + }).catch(() => { + // Ignore errors. + }); + }).catch((error) => { + if (this.utils.isWebServiceError(error)) { + // It's a WebService error, this means the user cannot send notes. + errors.push(error); + } else { + // Not a WebService error, reject the synchronization to try again. + return Promise.reject(error); + } + }).then(() => { + // Notes were sent, delete them from local DB. + const promises = notes.map((note) => { + return this.notesOffline.deleteNote(note.userid, note.content, note.created, siteId); + }); + + return Promise.all(promises); + }).then(() => { + if (errors && errors.length) { + // At least an error occurred, get course name and add errors to warnings array. + return this.coursesProvider.getUserCourse(courseId, true, siteId).catch(() => { + // Ignore errors. + return {}; + }).then((course) => { + errors.forEach((error) => { + warnings.push(this.translate.instant('addon.notes.warningnotenotsent', { + course: course.fullname ? course.fullname : courseId, + error: error + })); + }); + }); + } + }); + }).then(() => { + // All done, return the warnings. + return warnings; + }); + + return this.addOngoingSync(courseId, syncPromise, siteId); + } +} diff --git a/src/addon/notes/providers/notes.ts b/src/addon/notes/providers/notes.ts new file mode 100644 index 000000000..9e065ca01 --- /dev/null +++ b/src/addon/notes/providers/notes.ts @@ -0,0 +1,301 @@ +// (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 { CoreAppProvider } from '@providers/app'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreSiteWSPreSets } from '@classes/site'; +import { TranslateService } from '@ngx-translate/core'; +import { CoreUserProvider } from '@core/user/providers/user'; +import { AddonNotesOfflineProvider } from './notes-offline'; + +/** + * Service to handle notes. + */ +@Injectable() +export class AddonNotesProvider { + + protected ROOT_CACHE_KEY = 'mmaNotes:'; + protected logger; + + constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private appProvider: CoreAppProvider, + private utils: CoreUtilsProvider, private translate: TranslateService, private userProvider: CoreUserProvider, + private notesOffline: AddonNotesOfflineProvider) { + this.logger = logger.getInstance('AddonNotesProvider'); + } + + /** + * Add a note. + * + * @param {number} userId User ID of the person to add the note. + * @param {number} courseId Course ID where the note belongs. + * @param {string} publishState Personal, Site or Course. + * @param {string} noteText The note text. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with boolean: true if note was sent to server, false if stored in device. + */ + addNote(userId: number, courseId: number, publishState: string, noteText: string, siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + // Convenience function to store a note to be synchronized later. + const storeOffline = (): Promise => { + return this.notesOffline.saveNote(userId, courseId, publishState, noteText, siteId).then(() => { + return false; + }); + }; + + if (!this.appProvider.isOnline()) { + // App is offline, store the note. + return storeOffline(); + } + + // Send note to server. + return this.addNoteOnline(userId, courseId, publishState, noteText, siteId).then(() => { + return true; + }).catch((error) => { + if (this.utils.isWebServiceError(error)) { + // It's a WebService error, the user cannot send the message so don't store it. + return Promise.reject(error); + } + + // Error sending note, store it to retry later. + return storeOffline(); + }); + } + + /** + * Add a note. It will fail if offline or cannot connect. + * + * @param {number} userId User ID of the person to add the note. + * @param {number} courseId Course ID where the note belongs. + * @param {string} publishState Personal, Site or Course. + * @param {string} noteText The note text. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when added, rejected otherwise. + */ + addNoteOnline(userId: number, courseId: number, publishState: string, noteText: string, siteId?: string): Promise { + const notes = [ + { + courseid: courseId, + format: 1, + publishstate: publishState, + text: noteText, + userid: userId + } + ]; + + return this.addNotesOnline(notes, siteId).then((response) => { + if (response && response[0] && response[0].noteid === -1) { + // There was an error, and it should be translated already. + return Promise.reject(this.utils.createFakeWSError(response[0].errormessage)); + } + + // A note was added, invalidate the course notes. + return this.invalidateNotes(courseId, siteId).catch(() => { + // Ignore errors. + }); + }); + } + + /** + * Add several notes. It will fail if offline or cannot connect. + * + * @param {any[]} notes Notes to save. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when added, rejected otherwise. Promise resolved doesn't mean that notes + * have been added, the resolve param can contain errors for notes not sent. + */ + addNotesOnline(notes: any[], siteId?: string): Promise { + if (!notes || !notes.length) { + return Promise.resolve(); + } + + return this.sitesProvider.getSite(siteId).then((site) => { + const data = { + notes: notes + }; + + return site.write('core_notes_create_notes', data); + }); + } + + /** + * Returns whether or not the notes plugin is enabled for a certain site. + * + * This method is called quite often and thus should only perform a quick + * check, we should not be calling WS from here. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with true if enabled, resolved with false or rejected otherwise. + */ + isPluginEnabled(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.canUseAdvancedFeature('enablenotes'); + }); + } + + /** + * Returns whether or not the add note plugin is enabled for a certain course. + * + * @param {number} courseId ID of the course. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with true if enabled, resolved with false or rejected otherwise. + */ + isPluginAddNoteEnabledForCourse(courseId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + // The only way to detect if it's enabled is to perform a WS call. + // We use an invalid user ID (-1) to avoid saving the note if the user has permissions. + const data = { + notes: [ + { + userid: -1, + publishstate: 'personal', + courseid: courseId, + text: '', + format: 1 + } + ] + }; + + /* Use .read to cache data and be able to check it in offline. This means that, if a user loses the capabilities + to add notes, he'll still see the option in the app. */ + return this.utils.promiseWorks(site.read('core_notes_create_notes', data)); + }); + } + + /** + * Returns whether or not the read notes plugin is enabled for a certain course. + * + * @param {number} courseId ID of the course. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with true if enabled, resolved with false or rejected otherwise. + */ + isPluginViewNotesEnabledForCourse(courseId: number, siteId?: string): Promise { + return this.utils.promiseWorks(this.getNotes(courseId, false, true, siteId)); + } + + /** + * Get the cache key for the get notes call. + * + * @param {number} courseId ID of the course to get the notes from. + * @return {string} Cache key. + */ + getNotesCacheKey(courseId: number): string { + return this.ROOT_CACHE_KEY + 'notes:' + courseId; + } + + /** + * Get users notes for a certain site, course and personal notes. + * + * @param {number} courseId ID of the course to get the notes from. + * @param {boolean} [ignoreCache] True when we should not get the value from the cache. + * @param {boolean} [onlyOnline] True to return only online notes, false to return both online and offline. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise to be resolved when the notes are retrieved. + */ + getNotes(courseId: number, ignoreCache?: boolean, onlyOnline?: boolean, siteId?: string): Promise { + this.logger.debug('Get notes for course ' + courseId); + + return this.sitesProvider.getSite(siteId).then((site) => { + const data = { + courseid: courseId + }; + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getNotesCacheKey(courseId) + }; + + if (ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } + + return site.read('core_notes_get_course_notes', data, preSets).then((notes) => { + if (onlyOnline) { + return notes; + } + + // Get offline notes and add them to the list. + return this.notesOffline.getNotesForCourse(courseId, siteId).then((offlineNotes) => { + offlineNotes.forEach((note) => { + const fieldName = note.publishstate + 'notes'; + if (!notes[fieldName]) { + notes[fieldName] = []; + } + note.offline = true; + // Add note to the start of array since last notes are shown first. + notes[fieldName].unshift(note); + }); + + return notes; + }); + }); + }); + } + + /** + * Get user data for notes since they only have userid. + * + * @param {any[]} notes Notes to get the data for. + * @param {number} courseId ID of the course the notes belong to. + * @return {Promise} Promise always resolved. Resolve param is the formatted notes. + */ + getNotesUserData(notes: any[], courseId: number): Promise { + const promises = notes.map((note) => { + // Get the user profile to retrieve the user image. + return this.userProvider.getProfile(note.userid, note.courseid, true).then((user) => { + note.userfullname = user.fullname; + note.userprofileimageurl = user.profileimageurl || null; + }).catch(() => { + note.userfullname = this.translate.instant('addon.notes.userwithid', {id: note.userid}); + }); + }); + + return Promise.all(promises).then(() => { + return notes; + }); + } + + /** + * Invalidate get notes WS call. + * + * @param {number} courseId Course ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when data is invalidated. + */ + invalidateNotes(courseId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKey(this.getNotesCacheKey(courseId)); + }); + } + + /** + * Report notes as being viewed. + * + * @param {number} courseId ID of the course. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the WS call is successful. + */ + logView(courseId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + courseid: courseId, + userid: 0 + }; + + return site.write('core_notes_view_notes', params); + }); + } +} diff --git a/src/addon/notes/providers/sync-cron-handler.ts b/src/addon/notes/providers/sync-cron-handler.ts new file mode 100644 index 000000000..b0e1e5da1 --- /dev/null +++ b/src/addon/notes/providers/sync-cron-handler.ts @@ -0,0 +1,47 @@ +// (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 { AddonNotesSyncProvider } from './notes-sync'; + +/** + * Synchronization cron handler. + */ +@Injectable() +export class AddonNotesSyncCronHandler implements CoreCronHandler { + name = 'AddonNotesSyncCronHandler'; + + constructor(private notesSync: AddonNotesSyncProvider) {} + + /** + * 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. + * @return {Promise} Promise resolved when done, rejected if failure. + */ + execute(siteId?: string): Promise { + return this.notesSync.syncAllNotes(siteId); + } + + /** + * Get the time between consecutive executions. + * + * @return {number} Time between consecutive executions (in ms). + */ + getInterval(): number { + return 600000; // 10 minutes. + } +} diff --git a/src/addon/notes/providers/user-handler.ts b/src/addon/notes/providers/user-handler.ts new file mode 100644 index 000000000..ad48c0f06 --- /dev/null +++ b/src/addon/notes/providers/user-handler.ts @@ -0,0 +1,107 @@ +// (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 { ModalController } from 'ionic-angular'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreUserProvider } from '@core/user/providers/user'; +import { CoreUserDelegate, CoreUserProfileHandler, CoreUserProfileHandlerData } from '@core/user/providers/user-delegate'; +import { CoreSitesProvider } from '@providers/sites'; +import { AddonNotesProvider } from './notes'; + +/** + * Profile notes handler. + */ +@Injectable() +export class AddonNotesUserHandler implements CoreUserProfileHandler { + name = 'AddonNotes'; + priority = 200; + type = CoreUserDelegate.TYPE_COMMUNICATION; + addNoteEnabledCache = {}; + + constructor(private modalCtrl: ModalController, private sitesProvider: CoreSitesProvider, + private notesProvider: AddonNotesProvider, eventsProvider: CoreEventsProvider) { + eventsProvider.on(CoreEventsProvider.LOGOUT, this.clearAddNoteCache.bind(this)); + eventsProvider.on(CoreUserProvider.PROFILE_REFRESHED, (data) => { + this.clearAddNoteCache(data.courseId); + }); + } + + /** + * Clear add note cache. + * If a courseId is specified, it will only delete the entry for that course. + * + * @param {number} [courseId] Course ID. + */ + private clearAddNoteCache(courseId?: number): void { + if (courseId) { + delete this.addNoteEnabledCache[courseId]; + } else { + this.addNoteEnabledCache = {}; + } + } + + /** + * Whether or not the handler is enabled on a site level. + * @return {boolean|Promise} Whether or not the handler is enabled on a site level. + */ + isEnabled(): boolean | Promise { + return this.notesProvider.isPluginEnabled(); + } + + /** + * Check if handler is enabled for this user in this context. + * + * @param {any} user User to check. + * @param {number} courseId Course ID. + * @param {any} [navOptions] Course navigation options for current user. See $mmCourses#getUserNavigationOptions. + * @param {any} [admOptions] Course admin options for current user. See $mmCourses#getUserAdministrationOptions. + * @return {boolean|Promise} Promise resolved with true if enabled, resolved with false otherwise. + */ + isEnabledForUser(user: any, courseId: number, navOptions?: any, admOptions?: any): boolean | Promise { + // Active course required. + if (!courseId || user.id == this.sitesProvider.getCurrentSiteUserId()) { + return Promise.resolve(false); + } + + if (typeof this.addNoteEnabledCache[courseId] != 'undefined') { + return this.addNoteEnabledCache[courseId]; + } + + return this.notesProvider.isPluginAddNoteEnabledForCourse(courseId).then((enabled) => { + this.addNoteEnabledCache[courseId] = enabled; + + return enabled; + }); + } + + /** + * Returns the data needed to render the handler. + * + * @return {CoreUserProfileHandlerData} Data needed to render the handler. + */ + getDisplayData(user: any, courseId: number): CoreUserProfileHandlerData { + return { + icon: 'list', + title: 'addon.notes.addnewnote', + class: 'addon-notes-handler', + action: (event, navCtrl, user, courseId): void => { + event.preventDefault(); + event.stopPropagation(); + const modal = this.modalCtrl.create('AddonNotesAddPage', { userId: user.id, courseId }); + modal.present(); + } + }; + } +} diff --git a/src/addon/pushnotifications/providers/pushnotifications.ts b/src/addon/pushnotifications/providers/pushnotifications.ts index 9482de788..eec83e6ff 100644 --- a/src/addon/pushnotifications/providers/pushnotifications.ts +++ b/src/addon/pushnotifications/providers/pushnotifications.ts @@ -326,8 +326,8 @@ export class AddonPushNotificationsProvider { this.onMessageReceived(notification); }); - pushObject.on('registration').subscribe((registrationId: any) => { - this.pushID = registrationId; + pushObject.on('registration').subscribe((data: any) => { + this.pushID = data.registrationId; this.registerDeviceOnMoodle().catch((error) => { this.logger.warn('Can\'t register device', error); }); @@ -402,7 +402,7 @@ export class AddonPushNotificationsProvider { number: value }; - return this.appDB.insertOrUpdateRecord(this.BADGE_TABLE, entry, {siteid: siteId, addon: addon}).then(() => { + return this.appDB.insertRecord(this.BADGE_TABLE, entry).then(() => { return value; }); } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 4ac415d9d..3314d6415 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -81,6 +81,7 @@ import { AddonModFolderModule } from '@addon/mod/folder/folder.module'; import { AddonModPageModule } from '@addon/mod/page/page.module'; import { AddonModUrlModule } from '@addon/mod/url/url.module'; import { AddonMessagesModule } from '@addon/messages/messages.module'; +import { AddonNotesModule } from '../addon/notes/notes.module'; import { AddonPushNotificationsModule } from '@addon/pushnotifications/pushnotifications.module'; import { AddonRemoteThemesModule } from '@addon/remotethemes/remotethemes.module'; @@ -165,6 +166,7 @@ export const CORE_PROVIDERS: any[] = [ AddonModPageModule, AddonModUrlModule, AddonMessagesModule, + AddonNotesModule, AddonPushNotificationsModule, AddonRemoteThemesModule ], diff --git a/src/classes/base-sync.ts b/src/classes/base-sync.ts index 654b3bba8..aa413dbcc 100644 --- a/src/classes/base-sync.ts +++ b/src/classes/base-sync.ts @@ -151,11 +151,11 @@ export class CoreSyncBaseProvider { /** * Check if a sync is needed: if a certain time has passed since the last time. * - * @param {string} id Unique sync identifier per component. + * @param {string | number} id Unique sync identifier per component. * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved with boolean: whether sync is needed. */ - isSyncNeeded(id: string, siteId?: string): Promise { + isSyncNeeded(id: string | number, siteId?: string): Promise { return this.getSyncTime(id, siteId).then((time) => { return Date.now() - this.syncInterval >= time; }); @@ -178,12 +178,12 @@ export class CoreSyncBaseProvider { /** * Set the synchronization warnings. * - * @param {string} id Unique sync identifier per component. + * @param {string | number} id Unique sync identifier per component. * @param {string[]} warnings Warnings to set. * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved when done. */ - setSyncWarnings(id: string, warnings: string[], siteId?: string): Promise { + setSyncWarnings(id: string | number, warnings: string[], siteId?: string): Promise { const warningsText = JSON.stringify(warnings || []); return this.syncProvider.insertOrUpdateSyncRecord(this.component, id, { warnings: warningsText }, siteId); @@ -245,5 +245,5 @@ export class CoreSyncBaseProvider { } return Promise.resolve(); -} + } } diff --git a/src/classes/site.ts b/src/classes/site.ts index 330b59ea3..3846ae746 100644 --- a/src/classes/site.ts +++ b/src/classes/site.ts @@ -780,7 +780,7 @@ export class CoreSite { entry.key = preSets.cacheKey; } - return this.db.insertOrUpdateRecord(this.WS_CACHE_TABLE, entry, { id: id }); + return this.db.insertRecord(this.WS_CACHE_TABLE, entry); }); } diff --git a/src/classes/sqlitedb.ts b/src/classes/sqlitedb.ts index efef5527d..fdf17f59b 100644 --- a/src/classes/sqlitedb.ts +++ b/src/classes/sqlitedb.ts @@ -625,7 +625,7 @@ export class SQLiteDB { questionMarks = ',?'.repeat(keys.length).substr(1); return [ - `INSERT INTO ${table} (${fields}) VALUES (${questionMarks})`, + `INSERT OR REPLACE INTO ${table} (${fields}) VALUES (${questionMarks})`, keys.map((key) => data[key]) ]; } @@ -644,24 +644,6 @@ export class SQLiteDB { }); } - /** - * Insert or update a record. - * - * @param {string} table The database table. - * @param {object} data An object with the fields to insert/update: fieldname=>fieldvalue. - * @param {object} conditions The conditions to check if the record already exists (and to update it). - * @return {Promise} Promise resolved with done. - */ - insertOrUpdateRecord(table: string, data: object, conditions: object): Promise { - return this.getRecord(table, conditions).then(() => { - // It exists, update it. - return this.updateRecords(table, data, conditions); - }).catch(() => { - // Doesn't exist, insert it. - return this.insertRecord(table, data); - }); - } - /** * Insert a record into a table and return the "rowId" field. * diff --git a/src/components/context-menu/context-menu.ts b/src/components/context-menu/context-menu.ts index 630b44f4c..3cf987667 100644 --- a/src/components/context-menu/context-menu.ts +++ b/src/components/context-menu/context-menu.ts @@ -12,12 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, Input, OnInit, OnDestroy, ElementRef } from '@angular/core'; +import { Component, Input, OnInit, OnDestroy, ElementRef, Optional } from '@angular/core'; import { PopoverController } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreContextMenuItemComponent } from './context-menu-item'; import { CoreContextMenuPopoverComponent } from './context-menu-popover'; +import { CoreTabComponent } from '@components/tabs/tab'; import { Subject } from 'rxjs'; /** @@ -40,7 +41,7 @@ export class CoreContextMenuComponent implements OnInit, OnDestroy { protected parentContextMenu: CoreContextMenuComponent; constructor(private translate: TranslateService, private popoverCtrl: PopoverController, elementRef: ElementRef, - private domUtils: CoreDomUtilsProvider) { + private domUtils: CoreDomUtilsProvider, @Optional() public coreTab: CoreTabComponent) { // Create the stream and subscribe to it. We ignore successive changes during 250ms. this.itemsChangedStream = new Subject(); this.itemsChangedStream.auditTime(250).subscribe(() => { diff --git a/src/components/navbar-buttons/navbar-buttons.ts b/src/components/navbar-buttons/navbar-buttons.ts index d682248a6..7f4ae239b 100644 --- a/src/components/navbar-buttons/navbar-buttons.ts +++ b/src/components/navbar-buttons/navbar-buttons.ts @@ -134,10 +134,12 @@ export class CoreNavBarButtonsComponent implements OnInit, OnDestroy { } // Both containers have a context menu. Merge them to prevent having 2 menus at the same time. - const mainContextMenuInstance = this.domUtils.getInstanceByElement(mainContextMenu), - secondaryContextMenuInstance = this.domUtils.getInstanceByElement(secondaryContextMenu); + const mainContextMenuInstance: CoreContextMenuComponent = this.domUtils.getInstanceByElement(mainContextMenu), + secondaryContextMenuInstance: CoreContextMenuComponent = this.domUtils.getInstanceByElement(secondaryContextMenu); - if (mainContextMenuInstance && secondaryContextMenuInstance) { + // Check that both context menus belong to the same core-tab. We shouldn't merge menus from different tabs. + if (mainContextMenuInstance && secondaryContextMenuInstance && + mainContextMenuInstance.coreTab === secondaryContextMenuInstance.coreTab) { this.mergedContextMenu = secondaryContextMenuInstance; this.mergedContextMenu.mergeContextMenus(mainContextMenuInstance); diff --git a/src/config.json b/src/config.json index 163458ae7..275d3c206 100644 --- a/src/config.json +++ b/src/config.json @@ -9,7 +9,7 @@ "languages": {"ar": "عربي", "bg": "Български", "ca": "Català", "cs": "Čeština", "da": "Dansk", "de": "Deutsch", "de-du": "Deutsch - Du", "el": "Ελληνικά", "en": "English", "es": "Español", "es-mx": "Español - México", "eu": "Euskara", "fa": "فارسی", "fr" : "Français", "he" : "עברית", "hu": "magyar", "it": "Italiano", "lt" : "Lietuvių", "ja": "日本語","nl": "Nederlands", "pl": "Polski", "pt-br": "Português - Brasil", "pt": "Português - Portugal", "ro": "Română", "ru": "Русский", "sr-cr": "Српски", "sr-lt": "Srpski", "sv": "Svenska", "tr" : "Türkçe", "uk" : "Українська", "zh-cn" : "简体中文", "zh-tw" : "正體中文"}, "wsservice" : "moodle_mobile_app", "wsextservice" : "local_mobile", - "demo_sites": {"student": {"url": "http://school.demo.moodle.net", "username": "student", "password": "moodle"}, "teacher": {"url": "http://school.demo.moodle.net", "username": "teacher", "password": "moodle"}}, + "demo_sites": {"student": {"url": "https://school.demo.moodle.net", "username": "student", "password": "moodle"}, "teacher": {"url": "https://school.demo.moodle.net", "username": "teacher", "password": "moodle"}}, "gcmpn": "694767596569", "customurlscheme": "moodlemobile", "siteurl": "", diff --git a/src/core/course/providers/course.ts b/src/core/course/providers/course.ts index 083ed039f..a491ab610 100644 --- a/src/core/course/providers/course.ts +++ b/src/core/course/providers/course.ts @@ -699,7 +699,7 @@ export class CoreCourseProvider { previousDownloadTime: previousDownloadTime }; - return site.getDb().insertOrUpdateRecord(this.COURSE_STATUS_TABLE, data, { id: courseId }); + return site.getDb().insertRecord(this.COURSE_STATUS_TABLE, data); } }).then(() => { // Success inserting, trigger event. diff --git a/src/core/course/providers/module-prefetch-delegate.ts b/src/core/course/providers/module-prefetch-delegate.ts index 84f4b0b15..0fac45323 100644 --- a/src/core/course/providers/module-prefetch-delegate.ts +++ b/src/core/course/providers/module-prefetch-delegate.ts @@ -417,7 +417,9 @@ export class CoreCourseModulePrefetchDelegate extends CoreDelegate { courseId: courseId, time: this.timeUtils.timestamp() }; - site.getDb().insertOrUpdateRecord(this.CHECK_UPDATES_TIMES_TABLE, entry, { courseId: courseId }); + site.getDb().insertRecord(this.CHECK_UPDATES_TIMES_TABLE, entry).catch(() => { + // Ignore errors. + }); return this.treatCheckUpdatesResult(data.toCheck, response, result); }).catch((error) => { diff --git a/src/core/emulator/providers/local-notifications.ts b/src/core/emulator/providers/local-notifications.ts index f59120623..50f9ea52a 100644 --- a/src/core/emulator/providers/local-notifications.ts +++ b/src/core/emulator/providers/local-notifications.ts @@ -671,7 +671,7 @@ export class LocalNotificationsMock extends LocalNotifications { notification = Object.assign({}, notification); // Clone the object. notification.triggered = !!triggered; - return this.appDB.insertOrUpdateRecord(this.DESKTOP_NOTIFS_TABLE, notification, { id: notification.id }); + return this.appDB.insertRecord(this.DESKTOP_NOTIFS_TABLE, notification); } /** diff --git a/src/core/user/providers/user.ts b/src/core/user/providers/user.ts index 128f9fc93..7dfc1ae0e 100644 --- a/src/core/user/providers/user.ts +++ b/src/core/user/providers/user.ts @@ -383,7 +383,7 @@ export class CoreUserProvider { profileimageurl: avatar }; - return site.getDb().insertOrUpdateRecord(this.USERS_TABLE, userRecord, { id: userId }); + return site.getDb().insertRecord(this.USERS_TABLE, userRecord); }); } diff --git a/src/providers/config.ts b/src/providers/config.ts index 554ee3b21..c34166080 100644 --- a/src/providers/config.ts +++ b/src/providers/config.ts @@ -81,6 +81,6 @@ export class CoreConfigProvider { * @return {Promise} Promise resolved when done. */ set(name: string, value: boolean | number | string): Promise { - return this.appDB.insertOrUpdateRecord(this.TABLE_NAME, { name: name, value: value }, { name: name }); + return this.appDB.insertRecord(this.TABLE_NAME, { name: name, value: value }); } } diff --git a/src/providers/cron.ts b/src/providers/cron.ts index 930d324a2..4aa968ded 100644 --- a/src/providers/cron.ts +++ b/src/providers/cron.ts @@ -459,7 +459,7 @@ export class CoreCronDelegate { value: time }; - return this.appDB.insertOrUpdateRecord(this.CRON_TABLE, entry, { id: id }); + return this.appDB.insertRecord(this.CRON_TABLE, entry); } /** diff --git a/src/providers/filepool.ts b/src/providers/filepool.ts index bb1ded4a5..115964a7c 100644 --- a/src/providers/filepool.ts +++ b/src/providers/filepool.ts @@ -477,7 +477,7 @@ export class CoreFilepoolProvider { componentId: componentId || '' }; - return db.insertOrUpdateRecord(this.LINKS_TABLE, newEntry, { fileId: fileId }); + return db.insertRecord(this.LINKS_TABLE, newEntry); }); } @@ -544,7 +544,7 @@ export class CoreFilepoolProvider { values.fileId = fileId; return this.sitesProvider.getSiteDb(siteId).then((db) => { - return db.insertOrUpdateRecord(this.FILES_TABLE, values, { fileId: fileId }); + return db.insertRecord(this.FILES_TABLE, values); }); } @@ -2766,7 +2766,7 @@ export class CoreFilepoolProvider { // The package already has this status, no need to change it. promise = Promise.resolve(); } else { - promise = site.getDb().insertOrUpdateRecord(this.PACKAGES_TABLE, packageEntry, { id: packageId }); + promise = site.getDb().insertRecord(this.PACKAGES_TABLE, packageEntry); } return promise.then(() => { diff --git a/src/providers/local-notifications.ts b/src/providers/local-notifications.ts index c3339dfcf..4d0541514 100644 --- a/src/providers/local-notifications.ts +++ b/src/providers/local-notifications.ts @@ -500,6 +500,6 @@ export class CoreLocalNotificationsProvider { at: parseInt(notification.at, 10) }; - return this.appDB.insertOrUpdateRecord(this.TRIGGERED_TABLE, entry, { id: notification.id }); + return this.appDB.insertRecord(this.TRIGGERED_TABLE, entry); } } diff --git a/src/providers/sites.ts b/src/providers/sites.ts index 53154c12a..ac30cd79f 100644 --- a/src/providers/sites.ts +++ b/src/providers/sites.ts @@ -623,7 +623,7 @@ export class CoreSitesProvider { loggedOut: 0 }; - return this.appDB.insertOrUpdateRecord(this.SITES_TABLE, entry, { id: id }); + return this.appDB.insertRecord(this.SITES_TABLE, entry); } /** @@ -895,7 +895,7 @@ export class CoreSitesProvider { siteId: siteId }; - return this.appDB.insertOrUpdateRecord(this.CURRENT_SITE_TABLE, entry, { id: 1 }).then(() => { + return this.appDB.insertRecord(this.CURRENT_SITE_TABLE, entry).then(() => { this.eventsProvider.trigger(CoreEventsProvider.LOGIN, {}, siteId); }); } diff --git a/src/providers/sync.ts b/src/providers/sync.ts index 2003b488d..9137e997f 100644 --- a/src/providers/sync.ts +++ b/src/providers/sync.ts @@ -142,7 +142,7 @@ export class CoreSyncProvider { data.component = component; data.id = id; - return db.insertOrUpdateRecord(this.SYNC_TABLE, data, { component: component, id: id }); + return db.insertRecord(this.SYNC_TABLE, data); }); }