2022-10-05 16:02:09 +02:00

270 lines
9.4 KiB
TypeScript

// (C) Copyright 2015 Moodle Pty Ltd.
//
// 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 { CoreSyncBaseProvider } from '@classes/base-sync';
import { CoreNetworkError } from '@classes/errors/network-error';
import { CoreCourses } from '@features/courses/services/courses';
import { CoreNetwork } from '@services/network';
import { CoreSites } from '@services/sites';
import { CoreUtils } from '@services/utils/utils';
import { Translate, makeSingleton } from '@singletons';
import { CoreEvents } from '@singletons/events';
import { AddonNotesDBRecord, AddonNotesDeletedDBRecord } from './database/notes';
import { AddonNotes, AddonNotesCreateNoteData } from './notes';
import { AddonNotesOffline } from './notes-offline';
/**
* Service to sync notes.
*/
@Injectable( { providedIn: 'root' } )
export class AddonNotesSyncProvider extends CoreSyncBaseProvider<AddonNotesSyncResult> {
static readonly AUTO_SYNCED = 'addon_notes_autom_synced';
constructor() {
super('AddonNotesSync');
}
/**
* Try to synchronize all the notes in a certain site or in all sites.
*
* @param siteId Site ID to sync. If not defined, sync all sites.
* @param force Wether to force sync not depending on last execution.
* @return Promise resolved if sync is successful, rejected if sync fails.
*/
syncAllNotes(siteId?: string, force?: boolean): Promise<void> {
return this.syncOnSites('all notes', (siteId) => this.syncAllNotesFunc(!!force, siteId), siteId);
}
/**
* Synchronize all the notes in a certain site
*
* @param force Wether to force sync not depending on last execution.
* @param siteId Site ID to sync.
* @return Promise resolved if sync is successful, rejected if sync fails.
*/
protected async syncAllNotesFunc(force: boolean, siteId: string): Promise<void> {
const notesArray = await Promise.all([
AddonNotesOffline.getAllNotes(siteId),
AddonNotesOffline.getAllDeletedNotes(siteId),
]);
// Get all the courses to be synced.
let courseIds: number[] = [];
notesArray.forEach((notes: (AddonNotesDeletedDBRecord | AddonNotesDBRecord)[]) => {
courseIds = courseIds.concat(notes.map((note) => note.courseid));
});
CoreUtils.uniqueArray(courseIds);
// Sync all courses.
const promises = courseIds.map(async (courseId) => {
const result = await (force
? this.syncNotes(courseId, siteId)
: this.syncNotesIfNeeded(courseId, siteId));
if (result !== undefined) {
// Sync successful, send event.
CoreEvents.trigger(AddonNotesSyncProvider.AUTO_SYNCED, {
courseId,
warnings: result.warnings,
}, siteId);
}
});
await Promise.all(promises);
}
/**
* Sync course notes only if a certain time has passed since the last time.
*
* @param courseId Course ID.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when the notes are synced or if they don't need to be synced.
*/
protected async syncNotesIfNeeded(courseId: number, siteId?: string): Promise<AddonNotesSyncResult | undefined> {
const needed = await this.isSyncNeeded(courseId, siteId);
if (needed) {
return this.syncNotes(courseId, siteId);
}
}
/**
* Synchronize notes of a course.
*
* @param courseId Course ID.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved if sync is successful, rejected otherwise.
*/
syncNotes(courseId: number, siteId?: string): Promise<AddonNotesSyncResult> {
siteId = siteId || CoreSites.getCurrentSiteId();
const currentSyncPromise = this.getOngoingSync(courseId, siteId);
if (currentSyncPromise) {
// There's already a sync ongoing for notes, return the promise.
return currentSyncPromise;
}
this.logger.debug('Try to sync notes for course ' + courseId);
const syncPromise = this.performSyncNotes(courseId, siteId);
return this.addOngoingSync(courseId, syncPromise, siteId);
}
/**
* Perform the synchronization of the notes of a course.
*
* @param courseId Course ID.
* @param siteId Site ID.
* @return Promise resolved if sync is successful, rejected otherwise.
*/
async performSyncNotes(courseId: number, siteId?: string): Promise<AddonNotesSyncResult> {
const result: AddonNotesSyncResult = {
warnings: [],
};
// Get offline notes to be sent and deleted.
const [offlineNotes, deletedNotes] = await Promise.all([
AddonNotesOffline.getAllNotes(siteId),
AddonNotesOffline.getAllDeletedNotes(siteId),
]);
if (!offlineNotes.length && !deletedNotes.length) {
// Nothing to sync.
return result;
}
if (!CoreNetwork.isOnline()) {
// Cannot sync in offline.
throw new CoreNetworkError();
}
const errors: string[] = [];
const promises: Promise<void>[] = [];
// Format the notes to be sent.
const notesToSend: AddonNotesCreateNoteData[] = offlineNotes.map((note) => ({
userid: note.userid,
publishstate: note.publishstate,
courseid: note.courseid,
text: note.content,
format: 1,
}));
// Send the notes.
promises.push(AddonNotes.addNotesOnline(notesToSend, siteId).then((response) => {
// Search errors in the response.
response.forEach((entry) => {
if (entry.noteid === -1 && entry.errormessage && errors.indexOf(entry.errormessage) == -1) {
errors.push(entry.errormessage);
}
});
return;
}).catch((error) => {
if (CoreUtils.isWebServiceError(error)) {
// It's a WebService error, this means the user cannot send notes.
errors.push(error);
return;
}
// Not a WebService error, reject the synchronization to try again.
throw error;
}).then(async () => {
// Notes were sent, delete them from local DB.
const promises: Promise<void>[] = offlineNotes.map((note) =>
AddonNotesOffline.deleteOfflineNote(note.userid, note.content, note.created, siteId));
await Promise.all(promises);
return;
}));
// Format the notes to be sent.
const notesToDelete = deletedNotes.map((note) => note.noteid);
// Delete the notes.
promises.push(AddonNotes.deleteNotesOnline(notesToDelete, courseId, siteId).catch((error) => {
if (CoreUtils.isWebServiceError(error)) {
// It's a WebService error, this means the user cannot send notes.
errors.push(error);
return;
}
// Not a WebService error, reject the synchronization to try again.
throw error;
}).then(async () => {
// Notes were sent, delete them from local DB.
const promises = notesToDelete.map((noteId) => AddonNotesOffline.undoDeleteNote(noteId, siteId));
await Promise.all(promises);
return;
}));
await Promise.all(promises);
// Fetch the notes from server to be sure they're up to date.
await CoreUtils.ignoreErrors(AddonNotes.invalidateNotes(courseId, undefined, siteId));
await CoreUtils.ignoreErrors(AddonNotes.getNotes(courseId, undefined, false, true, siteId));
if (errors && errors.length) {
// At least an error occurred, get course name and add errors to warnings array.
const course = await CoreUtils.ignoreErrors(CoreCourses.getUserCourse(courseId, true, siteId), {});
result.warnings = errors.map((error) =>
Translate.instant('addon.notes.warningnotenotsent', {
course: 'fullname' in course ? course.fullname : courseId,
error: error,
}));
}
// All done, return the warnings.
return result;
}
}
export const AddonNotesSync = makeSingleton(AddonNotesSyncProvider);
export type AddonNotesSyncResult = {
warnings: string[]; // List of warnings.
};
/**
* Data passed to AUTO_SYNCED event.
*/
export type AddonNotesSyncAutoSyncData = {
courseId: number;
warnings: string[];
};
declare module '@singletons/events' {
/**
* Augment CoreEventsData interface with events specific to this service.
*
* @see https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation
*/
export interface CoreEventsData {
[AddonNotesSyncProvider.AUTO_SYNCED]: AddonNotesSyncAutoSyncData;
}
}