MOBILE-3659 course: Implement log helper

main
Dani Palou 2021-01-14 09:13:37 +01:00
parent 8d752d2bf5
commit 2906210242
5 changed files with 438 additions and 12 deletions

View File

@ -17,15 +17,15 @@ import { NgModule } from '@angular/core';
import { CORE_SITE_SCHEMAS } from '@services/sites';
import { SITE_SCHEMA, OFFLINE_SITE_SCHEMA } from './services/database/course';
import { SITE_SCHEMA as LOG_SITE_SCHEMA } from './services/database/log';
@NgModule({
providers: [
{
provide: CORE_SITE_SCHEMAS,
useValue: [SITE_SCHEMA, OFFLINE_SITE_SCHEMA],
useValue: [SITE_SCHEMA, OFFLINE_SITE_SCHEMA, LOG_SITE_SCHEMA],
multi: true,
},
],
})
export class CoreCourseModule {
}
export class CoreCourseModule {}

View File

@ -47,8 +47,9 @@ export interface CoreCourseOptionsHandler extends CoreDelegateHandler {
* @param admOptions Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions.
* @return True or promise resolved with true if enabled.
*/
isEnabledForCourse(courseId: number,
accessData: CoreCourseAccessData,
isEnabledForCourse(
courseId: number,
accessData: CoreCourseAccess,
navOptions?: CoreCourseUserAdminOrNavOptionIndexed,
admOptions?: CoreCourseUserAdminOrNavOptionIndexed,
): boolean | Promise<boolean>;
@ -56,7 +57,7 @@ export interface CoreCourseOptionsHandler extends CoreDelegateHandler {
/**
* Returns the data needed to render the handler.
*
* @param course The course. // @todo: define type in the whole file.
* @param course The course.
* @return Data or promise resolved with the data.
*/
getDisplayData?(
@ -226,7 +227,7 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt
protected coursesHandlers: {
[courseId: number]: {
access: any;
access: CoreCourseAccess;
navOptions?: CoreCourseUserAdminOrNavOptionIndexed;
admOptions?: CoreCourseUserAdminOrNavOptionIndexed;
deferred: PromiseDefer<void>;
@ -320,7 +321,7 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt
protected async getHandlersForAccess(
courseId: number,
refresh: boolean,
accessData: any,
accessData: CoreCourseAccess,
navOptions?: CoreCourseUserAdminOrNavOptionIndexed,
admOptions?: CoreCourseUserAdminOrNavOptionIndexed,
): Promise<CoreCourseOptionsHandler[]> {
@ -618,7 +619,7 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt
*/
async updateHandlersForCourse(
courseId: number,
accessData: any,
accessData: CoreCourseAccess,
navOptions?: CoreCourseUserAdminOrNavOptionIndexed,
admOptions?: CoreCourseUserAdminOrNavOptionIndexed,
): Promise<void> {
@ -673,5 +674,6 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt
export class CoreCourseOptionsDelegate extends makeSingleton(CoreCourseOptionsDelegateService) {}
// @todo define
export type CoreCourseAccessData = any;
export type CoreCourseAccess = {
type: string; // Either CoreCourseProvider.ACCESS_GUEST or CoreCourseProvider.ACCESS_DEFAULT.
};

View File

@ -856,7 +856,6 @@ export class CoreCourseProvider {
* @param siteId Site ID. If not defined, current site.
* @param name Name of the course.
* @return Promise resolved when the WS call is successful.
* @todo use logHelper. Remove eslint disable when done.
*/
async logView(courseId: number, sectionNumber?: number, siteId?: string, name?: string): Promise<void> {
const params: CoreCourseViewCourseWSParams = {

View File

@ -0,0 +1,60 @@
// (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 { CoreSiteSchema } from '@services/sites';
/**
* Database variables for CoreCourse service.
*/
export const ACTIVITY_LOG_TABLE = 'course_activity_log';
export const SITE_SCHEMA: CoreSiteSchema = {
name: 'CoreCourseLogHelperProvider',
version: 1,
tables: [
{
name: ACTIVITY_LOG_TABLE,
columns: [
{
name: 'component',
type: 'TEXT',
},
{
name: 'componentid',
type: 'INTEGER',
},
{
name: 'ws',
type: 'TEXT',
},
{
name: 'data',
type: 'TEXT',
},
{
name: 'time',
type: 'INTEGER',
}
],
primaryKeys: ['component', 'componentid', 'ws', 'time'],
},
],
};
export type CoreCourseActivityLogDBRecord = {
component: string;
componentid: number;
ws: string;
time: number;
data?: string;
};

View File

@ -0,0 +1,365 @@
// (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.instance.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.instance.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.instance.getSite(siteId);
const conditions: Partial<CoreCourseActivityLogDBRecord> = {
ws,
data: CoreUtils.instance.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.instance.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.instance.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.instance.getSite(siteId);
if (!CoreApp.instance.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.instance.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.instance.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.instance.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.instance.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.instance.getSite(siteId);
const log: CoreCourseActivityLogDBRecord = {
component,
componentid: componentId,
ws,
data: CoreUtils.instance.sortAndStringify(data),
time: CoreTimeUtils.instance.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.instance.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.
*/
async syncActivity(component: string, componentId: number, siteId?: string): Promise<void> {
const site = await CoreSites.instance.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.instance.parseJSON<Record<string, unknown>>(log.data || '{}', {});
try {
await this.logOnline(log.ws, data, siteId);
} catch (error) {
if (CoreUtils.instance.isWebServiceError(error)) {
// The WebService has thrown an error, this means that responses cannot be submitted.
await CoreUtils.instance.ignoreErrors(this.deleteWSLogs(log.ws, data, siteId));
}
throw error;
}
await this.deleteWSLogsByComponent(log.component, log.componentid, log.ws, siteId);
}));
}
}
export class CoreCourseLogHelper extends makeSingleton(CoreCourseLogHelperProvider) {}