2021-03-03 09:46:35 +01:00

379 lines
13 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 { CoreApp } from '@services/app';
import { CoreSites } from '@services/sites';
import { CoreTextUtils } from '@services/utils/text';
import { CoreTimeUtils } from '@services/utils/time';
import { CoreUtils } from '@services/utils/utils';
import { CorePushNotifications } from '@features/pushnotifications/services/pushnotifications';
import { makeSingleton } from '@singletons';
import { ACTIVITY_LOG_TABLE, CoreCourseActivityLogDBRecord } from './database/log';
import { CoreStatusWithWarningsWSResponse } from '@services/ws';
import { CoreWSError } from '@classes/errors/wserror';
/**
* Helper to manage logging to Moodle.
*/
@Injectable({ providedIn: 'root' })
export class CoreCourseLogHelperProvider {
/**
* Delete the offline saved activity logs.
*
* @param component Component name.
* @param componentId Component ID.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when deleted, rejected if failure.
*/
protected async deleteLogs(component: string, componentId: number, siteId?: string): Promise<void> {
const site = await CoreSites.getSite(siteId);
const conditions: Partial<CoreCourseActivityLogDBRecord> = {
component,
componentid: componentId,
};
await site.getDb().deleteRecords(ACTIVITY_LOG_TABLE, conditions);
}
/**
* Delete a WS based log.
*
* @param component Component name.
* @param componentId Component ID.
* @param ws WS name.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when deleted, rejected if failure.
*/
protected async deleteWSLogsByComponent(component: string, componentId: number, ws: string, siteId?: string): Promise<void> {
const site = await CoreSites.getSite(siteId);
const conditions: Partial<CoreCourseActivityLogDBRecord> = {
component,
componentid: componentId,
ws,
};
await site.getDb().deleteRecords(ACTIVITY_LOG_TABLE, conditions);
}
/**
* Delete the offline saved activity logs using call data.
*
* @param ws WS name.
* @param data Data to send to the WS.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when deleted, rejected if failure.
*/
protected async deleteWSLogs(ws: string, data: Record<string, unknown>, siteId?: string): Promise<void> {
const site = await CoreSites.getSite(siteId);
const conditions: Partial<CoreCourseActivityLogDBRecord> = {
ws,
data: CoreUtils.sortAndStringify(data),
};
await site.getDb().deleteRecords(ACTIVITY_LOG_TABLE, conditions);
}
/**
* Get all the offline saved activity logs.
*
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the list of offline logs.
*/
protected async getAllLogs(siteId?: string): Promise<CoreCourseActivityLogDBRecord[]> {
const site = await CoreSites.getSite(siteId);
return site.getDb().getAllRecords<CoreCourseActivityLogDBRecord>(ACTIVITY_LOG_TABLE);
}
/**
* Get the offline saved activity logs.
*
* @param component Component name.
* @param componentId Component ID.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the list of offline logs.
*/
protected async getLogs(component: string, componentId: number, siteId?: string): Promise<CoreCourseActivityLogDBRecord[]> {
const site = await CoreSites.getSite(siteId);
const conditions: Partial<CoreCourseActivityLogDBRecord> = {
component,
componentid: componentId,
};
return site.getDb().getRecords<CoreCourseActivityLogDBRecord>(ACTIVITY_LOG_TABLE, conditions);
}
/**
* Perform log online. Data will be saved offline for syncing.
*
* @param ws WS name.
* @param data Data to send to the WS.
* @param component Component name.
* @param componentId Component ID.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
async log(ws: string, data: Record<string, unknown>, component: string, componentId: number, siteId?: string): Promise<void> {
const site = await CoreSites.getSite(siteId);
if (!CoreApp.isOnline()) {
// App is offline, store the action.
return this.storeOffline(ws, data, component, componentId, site.getId());
}
try {
await this.logOnline(ws, data, site.getId());
} catch (error) {
if (CoreUtils.isWebServiceError(error)) {
// The WebService has thrown an error, this means that responses cannot be submitted.
throw error;
}
// Couldn't connect to server, store in offline.
return this.storeOffline(ws, data, component, componentId, site.getId());
}
}
/**
* Perform the log online.
*
* @param ws WS name.
* @param data Data to send to the WS.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when log is successfully submitted. Rejected with object containing
* the error message (if any) and a boolean indicating if the error was returned by WS.
*/
protected async logOnline<T extends CoreStatusWithWarningsWSResponse>(
ws: string,
data: Record<string, unknown>,
siteId?: string,
): Promise<void> {
const site = await CoreSites.getSite(siteId);
// Clone to have an unmodified data object.
const wsData = Object.assign({}, data);
const response = await site.write<T>(ws, wsData);
if (!response.status) {
// Return the warning. If no warnings (shouldn't happen), create a fake one.
const warning = response.warnings?.[0] || {
warningcode: 'errorlog',
message: 'Error logging data.',
};
throw new CoreWSError(warning);
}
// Remove all the logs performed.
// TODO: Remove this lines when time is accepted in logs.
await this.deleteWSLogs(ws, data, siteId);
}
/**
* Perform log online. Data will be saved offline for syncing.
* It also triggers a Firebase view_item event.
*
* @param ws WS name.
* @param data Data to send to the WS.
* @param component Component name.
* @param componentId Component ID.
* @param name Name of the viewed item.
* @param category Category of the viewed item.
* @param eventData Data to pass to the Firebase event.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
logSingle(
ws: string,
data: Record<string, unknown>,
component: string,
componentId: number,
name?: string,
category?: string,
eventData?: Record<string, unknown>,
siteId?: string,
): Promise<void> {
CorePushNotifications.logViewEvent(componentId, name, category, ws, eventData, siteId);
return this.log(ws, data, component, componentId, siteId);
}
/**
* Perform log online. Data will be saved offline for syncing.
* It also triggers a Firebase view_item_list event.
*
* @param ws WS name.
* @param data Data to send to the WS.
* @param component Component name.
* @param componentId Component ID.
* @param category Category of the viewed item.
* @param eventData Data to pass to the Firebase event.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
logList(
ws: string,
data: Record<string, unknown>,
component: string,
componentId: number,
category: string,
eventData?: Record<string, unknown>,
siteId?: string,
): Promise<void> {
CorePushNotifications.logViewListEvent(category, ws, eventData, siteId);
return this.log(ws, data, component, componentId, siteId);
}
/**
* Save activity log for offline sync.
*
* @param ws WS name.
* @param data Data to send to the WS.
* @param component Component name.
* @param componentId Component ID.
* @param siteId Site ID. If not defined, current site.
* @return Resolved when done.
*/
protected async storeOffline(
ws: string,
data: Record<string, unknown>,
component: string,
componentId: number,
siteId?: string,
): Promise<void> {
const site = await CoreSites.getSite(siteId);
const log: CoreCourseActivityLogDBRecord = {
component,
componentid: componentId,
ws,
data: CoreUtils.sortAndStringify(data),
time: CoreTimeUtils.timestamp(),
};
await site.getDb().insertRecord(ACTIVITY_LOG_TABLE, log);
}
/**
* Sync all the offline saved activity logs.
*
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
async syncSite(siteId?: string): Promise<void> {
const site = await CoreSites.getSite(siteId);
siteId = site.getId();
const logs = await this.getAllLogs(siteId);
const unique: CoreCourseActivityLogDBRecord[] = [];
// TODO: When time is accepted on log, do not discard same logs.
logs.forEach((log) => {
// Just perform unique syncs.
const found = unique.find((doneLog) => log.component == doneLog.component && log.componentid == doneLog.componentid &&
log.ws == doneLog.ws && log.data == doneLog.data);
if (!found) {
unique.push(log);
}
});
return this.syncLogs(unique, siteId);
}
/**
* Sync the offline saved activity logs.
*
* @param component Component name.
* @param componentId Component ID.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
* @deprecated since 3.9.5. Please use syncActivity instead.
*/
syncIfNeeded(component: string, componentId: number, siteId?: string): Promise<void> {
return this.syncActivity(component, componentId, siteId);
}
/**
* Sync the offline saved activity logs.
*
* @param component Component name.
* @param componentId Component ID.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
async syncActivity(component: string, componentId: number, siteId?: string): Promise<void> {
const site = await CoreSites.getSite(siteId);
siteId = site.getId();
const logs = await this.getLogs(component, componentId, siteId);
const unique: CoreCourseActivityLogDBRecord[] = [];
// TODO: When time is accepted on log, do not discard same logs.
logs.forEach((log) => {
// Just perform unique syncs.
const found = unique.find((doneLog) => log.ws == doneLog.ws && log.data == doneLog.data);
if (!found) {
unique.push(log);
}
});
return this.syncLogs(unique, siteId);
}
/**
* Sync and delete given logs.
*
* @param logs Array of log objects.
* @param siteId Site Id.
* @return Promise resolved when done.
*/
protected async syncLogs(logs: CoreCourseActivityLogDBRecord[], siteId: string): Promise<void> {
await Promise.all(logs.map(async (log) => {
const data = CoreTextUtils.parseJSON<Record<string, unknown>>(log.data || '{}', {});
try {
await this.logOnline(log.ws, data, siteId);
} catch (error) {
if (CoreUtils.isWebServiceError(error)) {
// The WebService has thrown an error, this means that responses cannot be submitted.
await CoreUtils.ignoreErrors(this.deleteWSLogs(log.ws, data, siteId));
}
throw error;
}
await this.deleteWSLogsByComponent(log.component, log.componentid, log.ws, siteId);
}));
}
}
export const CoreCourseLogHelper = makeSingleton(CoreCourseLogHelperProvider);