// (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 { CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader'; import { CoreFile } from '@services/file'; import { CoreSites } from '@services/sites'; import { CoreTextUtils } from '@services/utils/text'; import { CoreUtils } from '@services/utils/utils'; import { makeSingleton } from '@singletons'; import { AddonModDataAction, AddonModDataEntryWSField } from './data'; import { AddonModDataEntryDBRecord, DATA_ENTRY_TABLE } from './database/data'; /** * Service to handle Offline data. */ @Injectable({ providedIn: 'root' }) export class AddonModDataOfflineProvider { /** * Delete all the actions of an entry. * * @param dataId Database ID. * @param entryId Database entry ID. * @param siteId Site ID. If not defined, current site. * @return Promise resolved if deleted, rejected if failure. */ async deleteAllEntryActions(dataId: number, entryId: number, siteId?: string): Promise { const actions = await this.getEntryActions(dataId, entryId, siteId); const promises = actions.map((action) => { this.deleteEntry(dataId, entryId, action.action, siteId); }); await Promise.all(promises); } /** * Delete an stored entry. * * @param dataId Database ID. * @param entryId Database entry Id. * @param action Action to be done * @param siteId Site ID. If not defined, current site. * @return Promise resolved if deleted, rejected if failure. */ async deleteEntry(dataId: number, entryId: number, action: AddonModDataAction, siteId?: string): Promise { const site = await CoreSites.getSite(siteId); await this.deleteEntryFiles(dataId, entryId, action, site.id); await site.getDb().deleteRecords(DATA_ENTRY_TABLE, { dataid: dataId, entryid: entryId, action, }); } /** * Delete entry offline files. * * @param dataId Database ID. * @param entryId Database entry ID. * @param action Action to be done. * @param siteId Site ID. If not defined, current site. * @return Promise resolved if deleted, rejected if failure. */ protected async deleteEntryFiles(dataId: number, entryId: number, action: AddonModDataAction, siteId?: string): Promise { const site = await CoreSites.getSite(siteId); const entry = await CoreUtils.ignoreErrors(this.getEntry(dataId, entryId, action, site.id)); if (!entry || !entry.fields) { // Entry not found or no fields, ignore. return; } const promises: Promise[] = []; entry.fields.forEach((field) => { const value = CoreTextUtils.parseJSON(field.value, null); if (!value || !value.offline) { return; } const promise = this.getEntryFieldFolder(dataId, entryId, field.fieldid, site.id).then((folderPath) => CoreFileUploader.getStoredFiles(folderPath)).then((files) => CoreFileUploader.clearTmpFiles(files)).catch(() => { // Files not found, ignore. }); promises.push(promise); }); await Promise.all(promises); } /** * Get all the stored entry data from all the databases. * * @param siteId Site ID. If not defined, current site. * @return Promise resolved with entries. */ async getAllEntries(siteId?: string): Promise { const site = await CoreSites.getSite(siteId); const entries = await site.getDb().getAllRecords(DATA_ENTRY_TABLE); return entries.map(this.parseRecord.bind(this)); } /** * Get all the stored entry actions from a certain database, sorted by modification time. * * @param dataId Database ID. * @param siteId Site ID. If not defined, current site. * @return Promise resolved with entries. */ async getDatabaseEntries(dataId: number, siteId?: string): Promise { const site = await CoreSites.getSite(siteId); const entries = await site.getDb().getRecords( DATA_ENTRY_TABLE, { dataid: dataId }, 'timemodified', ); return entries.map(this.parseRecord.bind(this)); } /** * Get an stored entry data. * * @param dataId Database ID. * @param entryId Database entry Id. * @param action Action to be done * @param siteId Site ID. If not defined, current site. * @return Promise resolved with entry. */ async getEntry( dataId: number, entryId: number, action: AddonModDataAction, siteId?: string, ): Promise { const site = await CoreSites.getSite(siteId); const entry = await site.getDb().getRecord(DATA_ENTRY_TABLE, { dataid: dataId, entryid: entryId, action, }); return this.parseRecord(entry); } /** * Get an all stored entry actions data. * * @param dataId Database ID. * @param entryId Database entry Id. * @param siteId Site ID. If not defined, current site. * @return Promise resolved with entry actions. */ async getEntryActions(dataId: number, entryId: number, siteId?: string): Promise { const site = await CoreSites.getSite(siteId); const entries = await site.getDb().getRecords( DATA_ENTRY_TABLE, { dataid: dataId, entryid: entryId }, ); return entries.map(this.parseRecord.bind(this)); } /** * Check if there are offline entries to send. * * @param dataId Database ID. * @param siteId Site ID. If not defined, current site. * @return Promise resolved with boolean: true if has offline answers, false otherwise. */ async hasOfflineData(dataId: number, siteId?: string): Promise { const site = await CoreSites.getSite(siteId); return CoreUtils.promiseWorks( site.getDb().recordExists(DATA_ENTRY_TABLE, { dataid: dataId }), ); } /** * Get the path to the folder where to store files for offline files in a database. * * @param dataId Database ID. * @param siteId Site ID. If not defined, current site. * @return Promise resolved with the path. */ protected async getDatabaseFolder(dataId: number, siteId?: string): Promise { const site = await CoreSites.getSite(siteId); const siteFolderPath = CoreFile.getSiteFolder(site.getId()); const folderPath = 'offlinedatabase/' + dataId; return CoreTextUtils.concatenatePaths(siteFolderPath, folderPath); } /** * Get the path to the folder where to store files for a new offline entry. * * @param dataId Database ID. * @param entryId The ID of the entry. * @param fieldId Field ID. * @param siteId Site ID. If not defined, current site. * @return Promise resolved with the path. */ async getEntryFieldFolder(dataId: number, entryId: number, fieldId: number, siteId?: string): Promise { const folderPath = await this.getDatabaseFolder(dataId, siteId); return CoreTextUtils.concatenatePaths(folderPath, entryId + '_' + fieldId); } /** * Parse "fields" of an offline record. * * @param record Record object * @return Record object with columns parsed. */ protected parseRecord(record: AddonModDataEntryDBRecord): AddonModDataOfflineAction { return Object.assign(record, { fields: CoreTextUtils.parseJSON(record.fields), }); } /** * Save an entry data to be sent later. * * @param dataId Database ID. * @param entryId Database entry Id. If action is add entryId should be 0 and -timemodified will be used. * @param action Action to be done to the entry: [add, edit, delete, approve, disapprove] * @param courseId Course ID of the database. * @param groupId Group ID. Only provided when adding. * @param fields Array of field data of the entry if needed. * @param timemodified The time the entry was modified. If not defined, current time. * @param siteId Site ID. If not defined, current site. * @return Promise resolved if stored, rejected if failure. */ async saveEntry( dataId: number, entryId: number, action: AddonModDataAction, courseId: number, groupId = 0, fields?: AddonModDataEntryWSField[], timemodified?: number, siteId?: string, ): Promise { const site = await CoreSites.getSite(siteId); timemodified = timemodified || new Date().getTime(); entryId = entryId === undefined || entryId === null ? -timemodified : entryId; const entry: AddonModDataEntryDBRecord = { dataid: dataId, courseid: courseId, groupid: groupId, action, entryid: entryId, fields: JSON.stringify(fields || []), timemodified, }; await site.getDb().insertRecord(DATA_ENTRY_TABLE, entry); return entry; } } export const AddonModDataOffline = makeSingleton(AddonModDataOfflineProvider); /** * Entry action stored offline. */ export type AddonModDataOfflineAction = Omit & { fields: AddonModDataEntryWSField[]; };