diff --git a/src/addon/notes/components/components.module.ts b/src/addon/notes/components/components.module.ts new file mode 100644 index 000000000..4064545e6 --- /dev/null +++ b/src/addon/notes/components/components.module.ts @@ -0,0 +1,41 @@ +// (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 { AddonNotesTypesComponent } from './types/types'; + +@NgModule({ + declarations: [ + AddonNotesTypesComponent + ], + imports: [ + CommonModule, + IonicModule, + TranslateModule.forChild(), + CoreComponentsModule, + ], + providers: [ + ], + exports: [ + AddonNotesTypesComponent + ], + entryComponents: [ + AddonNotesTypesComponent + ] +}) +export class AddonNotesComponentsModule {} diff --git a/src/addon/notes/components/types/types.html b/src/addon/notes/components/types/types.html new file mode 100644 index 000000000..87c04392c --- /dev/null +++ b/src/addon/notes/components/types/types.html @@ -0,0 +1,15 @@ + + + + + {{ 'addon.notes.sitenotes' | translate }} + + + {{ 'addon.notes.coursenotes' | translate }} + + + {{ 'addon.notes.personalnotes' | translate }} + + + + diff --git a/src/addon/notes/components/types/types.ts b/src/addon/notes/components/types/types.ts new file mode 100644 index 000000000..813546b6d --- /dev/null +++ b/src/addon/notes/components/types/types.ts @@ -0,0 +1,62 @@ +// (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 { AfterViewInit, Component, Input, OnInit, ViewChild } from '@angular/core'; +import { CoreSplitViewComponent } from '@components/split-view/split-view'; +import { CoreSitesProvider } from '@providers/sites'; + +/** + * Component that displays the competencies of a course. + */ +@Component({ + selector: 'addon-notes-types', + templateUrl: 'types.html', +}) +export class AddonNotesTypesComponent implements AfterViewInit, OnInit { + @ViewChild(CoreSplitViewComponent) splitviewCtrl: CoreSplitViewComponent; + + @Input() courseId: number; + + protected type: string; + + constructor(private sitesProvider: CoreSitesProvider) { + } + + /** + * Properties initialized. + */ + ngOnInit(): void { + const siteHomeId = this.sitesProvider.getCurrentSite().getSiteHomeId(); + this.type = (this.courseId == siteHomeId ? 'site' : 'course'); + } + + /** + * View loaded. + */ + ngAfterViewInit(): void { + if (this.splitviewCtrl.isOn()) { + this.openList(this.type); + } + } + + /** + * Opens a list of notes. + * + * @param {string} type + */ + openList(type: string): void { + this.type = type; + this.splitviewCtrl.push('AddonNotesListPage', {courseId: this.courseId, type: type}); + } +} 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..4ea5dd0a8 --- /dev/null +++ b/src/addon/notes/notes.module.ts @@ -0,0 +1,67 @@ +// (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 { CoreCoursesProvider } from '@core/courses/providers/courses'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreUserDelegate } from '@core/user/providers/user-delegate'; +import { CoreUserProvider } from '@core/user/providers/user'; + +@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, eventsProvider: CoreEventsProvider, sitesProvider: CoreSitesProvider) { + // Register handlers. + courseOptionsDelegate.registerHandler(courseOptionHandler); + userDelegate.registerHandler(userHandler); + cronDelegate.register(syncHandler); + + eventsProvider.on(CoreEventsProvider.LOGOUT, () => { + courseOptionHandler.clearCoursesNavCache(); + }, sitesProvider.getCurrentSiteId()); + + eventsProvider.on(CoreCoursesProvider.EVENT_MY_COURSES_REFRESHED, () => { + courseOptionHandler.clearCoursesNavCache(); + }, sitesProvider.getCurrentSiteId()); + + eventsProvider.on(CoreUserProvider.PROFILE_REFRESHED, () => { + userHandler.clearAddNoteCache(); + }, sitesProvider.getCurrentSiteId()); + } +} diff --git a/src/addon/notes/pages/add/add.html b/src/addon/notes/pages/add/add.html new file mode 100644 index 000000000..59b0bb32d --- /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/pages/list/list.html b/src/addon/notes/pages/list/list.html new file mode 100644 index 000000000..02d5f6084 --- /dev/null +++ b/src/addon/notes/pages/list/list.html @@ -0,0 +1,42 @@ + + + {{ 'addon.notes.notes' | translate }} + + + + + + + + + + + + + + + + +

+ {{ 'core.thereisdatatosync' | translate:{$a: 'addon.notes.notes' | translate | lowercase } }} +

+ + + + + + + + + +

{{note.userfullname}}

+

{{note.lastmodified | coreDateDayOrTime}}

+

{{ 'core.notsent' | translate }}

+
+ + + +
+
+
+
diff --git a/src/addon/notes/pages/list/list.module.ts b/src/addon/notes/pages/list/list.module.ts new file mode 100644 index 000000000..56f649dee --- /dev/null +++ b/src/addon/notes/pages/list/list.module.ts @@ -0,0 +1,35 @@ +// (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 { CorePipesModule } from '@pipes/pipes.module'; +import { AddonNotesListPage } from './list'; + +@NgModule({ + declarations: [ + AddonNotesListPage, + ], + imports: [ + CoreComponentsModule, + CoreDirectivesModule, + CorePipesModule, + IonicPageModule.forChild(AddonNotesListPage), + TranslateModule.forChild() + ], +}) +export class AddonNotesListPageModule {} diff --git a/src/addon/notes/pages/list/list.ts b/src/addon/notes/pages/list/list.ts new file mode 100644 index 000000000..fcebe9a34 --- /dev/null +++ b/src/addon/notes/pages/list/list.ts @@ -0,0 +1,173 @@ +// (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, OnDestroy, Optional, ViewChild } from '@angular/core'; +import { Content, IonicPage, NavController, NavParams } 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 { CoreSplitViewComponent } from '@components/split-view/split-view'; +import { AddonNotesProvider } from '../../providers/notes'; +import { AddonNotesSyncProvider } from '../../providers/notes-sync'; + +/** + * Page that displays the list of notes. + */ +@IonicPage({ segment: 'addon-notes-list' }) +@Component({ + selector: 'page-addon-notes-list', + templateUrl: 'list.html', +}) +export class AddonNotesListPage implements OnDestroy { + @ViewChild(Content) content: Content; + + protected courseId = 0; + protected syncObserver: any; + + type = ''; + refreshIcon = 'spinner'; + syncIcon = 'spinner'; + notes: any[]; + hasOffline = false; + notesLoaded = false; + + constructor(navParams: NavParams, private navCtrl: NavController, @Optional() private svComponent: CoreSplitViewComponent, + private domUtils: CoreDomUtilsProvider, private textUtils: CoreTextUtilsProvider, + sitesProvider: CoreSitesProvider, eventsProvider: CoreEventsProvider, + private notesProvider: AddonNotesProvider, private notesSync: AddonNotesSyncProvider) { + this.courseId = navParams.get('courseId') || sitesProvider.getCurrentSiteHomeId(); + this.type = navParams.get('type'); + // 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()); + } + + /** + * View loaded. + */ + ionViewDidLoad(): 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 = this.notesProvider.hasOfflineNote(notes); + + 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 = 'loop'; + }); + } + + /** + * 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(); + } + }); + }); + } + + /** + * Tries to syncrhonize 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); + } + } + + /** + * Opens the profile of a user. + * + * @param {number} userId + */ + openUserProfile(userId: number): void { + // Decide which navCtrl to use. If this page is inside a split view, use the split view's master nav. + const navCtrl = this.svComponent ? this.svComponent.getMasterNav() : this.navCtrl; + navCtrl.push('CoreUserProfilePage', {userId, courseId: this.courseId}); + } + + /** + * Page destroyed. + */ + ngOnDestroy(): void { + this.syncObserver && this.syncObserver.off(); + } +} 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..f71f9e097 --- /dev/null +++ b/src/addon/notes/providers/course-option-handler.ts @@ -0,0 +1,90 @@ +// (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 { AddonNotesTypesComponent } from '../components/types/types'; + +/** + * Handler to inject an option into the course main menu. + */ +@Injectable() +export class AddonNotesCourseOptionHandler implements CoreCourseOptionsHandler { + name = 'AddonNotes'; + priority = 200; + protected coursesNavEnabledCache = {}; + + constructor(private notesProvider: AddonNotesProvider) { + } + + /** + * Clear courses nav cache. + */ + clearCoursesNavCache(): void { + this.coursesNavEnabledCache = {}; + } + + /** + * 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.isPluginViewNotesEnabled(); + } + + /** + * 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; + } + + if (typeof this.coursesNavEnabledCache[courseId] != 'undefined') { + return this.coursesNavEnabledCache[courseId]; + } + + return this.notesProvider.isPluginViewNotesEnabledForCourse(courseId).then((enabled) => { + this.coursesNavEnabledCache[courseId] = enabled; + + return enabled; + }); + } + + /** + * 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: AddonNotesTypesComponent, + }; + } +} diff --git a/src/addon/notes/providers/notes-offline.ts b/src/addon/notes/providers/notes-offline.ts new file mode 100644 index 000000000..c285c7428 --- /dev/null +++ b/src/addon/notes/providers/notes-offline.ts @@ -0,0 +1,232 @@ +// (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 + }; + const conditions = { + userid: userId, + content: content, + created: now + }; + + return site.getDb().insertOrUpdateRecord(this.NOTES_TABLE, data, conditions).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..1f6e0199d --- /dev/null +++ b/src/addon/notes/providers/notes.ts @@ -0,0 +1,367 @@ +// (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 utilsProvider: 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((data) => { + if (data.wserror) { + // It's a WebService error, the user cannot add the note so don't store it. + return Promise.reject(data.error); + } else { + // 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. Reject param is an object with: + * - error: The error message. + * - wserror: True if it's an error returned by the WebService, false 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).catch((error) => { + return Promise.reject({ + error: error, + wserror: this.utilsProvider.isWebServiceError(error) + }); + }).then((response) => { + if (response && response[0] && response[0].noteid === -1) { + // There was an error, and it should be translated already. + return Promise.reject({ + error: response[0].errormessage, + wserror: true + }); + } + + // 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 add note 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. + */ + isPluginAddNoteEnabled(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + if (!site.canUseAdvancedFeature('enablenotes')) { + return false; + } else if (!site.wsAvailable('core_notes_create_notes')) { + return false; + } + + return true; + }); + } + + /** + * 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 site.read('core_notes_create_notes', data).then(() => { + // User can add notes. + return true; + }).catch(() => { + return false; + }); + }); + } + + /** + * Returns whether or not the read notes plugin is enabled for the current 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. + */ + isPluginViewNotesEnabled(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + if (!site.canUseAdvancedFeature('enablenotes')) { + return false; + } else if (!site.wsAvailable('core_notes_get_course_notes')) { + return false; + } + + return true; + }); + } + + /** + * 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.getNotes(courseId, false, true, siteId).then(() => { + return true; + }).catch(() => { + return false; + }); + } + + /** + * 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; + }); + } + + /** + * Given a list of notes, check if any of them is an offline note. + * + * @param {any[]} notes List of notes. + * @return {boolean} True if at least 1 note is offline, false otherwise. + */ + hasOfflineNote(notes: any[]): boolean { + if (!notes || !notes.length) { + return false; + } + + for (let i = 0, len = notes.length; i < len; i++) { + if (notes[i].offline) { + return true; + } + } + + return false; + } + + /** + * 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..97478efe1 --- /dev/null +++ b/src/addon/notes/providers/user-handler.ts @@ -0,0 +1,101 @@ +// (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 { 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) { + } + + /** + * Clear add note cache. + * If a courseId is specified, it will only delete the entry for that course. + * + * @param {number} [courseId] Course ID. + */ + 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.isPluginAddNoteEnabled(); + } + + /** + * 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/app/app.module.ts b/src/app/app.module.ts index eaf546c1a..a8b38ffc0 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -79,6 +79,7 @@ import { AddonModLabelModule } from '@addon/mod/label/label.module'; import { AddonModResourceModule } from '@addon/mod/resource/resource.module'; import { AddonModFolderModule } from '@addon/mod/folder/folder.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'; @@ -161,6 +162,7 @@ export const CORE_PROVIDERS: any[] = [ AddonModResourceModule, AddonModFolderModule, 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(); -} + } }