// (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 { CoreError } from '@classes/errors/error'; import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; import { CoreCourseCommonModWSOptions } from '@features/course/services/course'; import { CoreCourseLogHelper } from '@features/course/services/log-helper'; import { CoreRatingInfo } from '@features/rating/services/rating'; import { CoreTagItem } from '@features/tag/services/tag'; import { CoreApp } from '@services/app'; import { CoreFileEntry } from '@services/file-helper'; import { CoreFilepool } from '@services/filepool'; import { CoreSites, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@services/sites'; import { CoreUtils } from '@services/utils/utils'; import { CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws'; import { makeSingleton } from '@singletons'; import { AddonModDataFieldsDelegate } from './data-fields-delegate'; import { AddonModDataOffline } from './data-offline'; import { AddonModDataAutoSyncData, AddonModDataSyncProvider } from './data-sync'; const ROOT_CACHE_KEY = 'mmaModData:'; 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 { [AddonModDataSyncProvider.AUTO_SYNCED]: AddonModDataAutoSyncData; [AddonModDataProvider.ENTRY_CHANGED]: AddonModDataEntryChangedEventData; } } export enum AddonModDataAction { ADD = 'add', EDIT = 'edit', DELETE = 'delete', APPROVE = 'approve', DISAPPROVE = 'disapprove', USER = 'user', USERPICTURE = 'userpicture', MORE = 'more', MOREURL = 'moreurl', COMMENTS = 'comments', TIMEADDED = 'timeadded', TIMEMODIFIED = 'timemodified', TAGS = 'tags', APPROVALSTATUS = 'approvalstatus', DELCHECK = 'delcheck', // Unused. EXPORT = 'export', // Unused. } export enum AddonModDataTemplateType { LIST_HEADER = 'listtemplateheader', LIST = 'listtemplate', LIST_FOOTER = 'listtemplatefooter', ADD = 'addtemplate', SEARCH = 'asearchtemplate', SINGLE = 'singletemplate', } export enum AddonModDataTemplateMode { LIST = 'list', EDIT = 'edit', SHOW = 'show', SEARCH = 'search', } /** * Service that provides some features for databases. */ @Injectable({ providedIn: 'root' }) export class AddonModDataProvider { static readonly COMPONENT = 'mmaModData'; static readonly PER_PAGE = 25; static readonly ENTRY_CHANGED = 'addon_mod_data_entry_changed'; /** * Adds a new entry to a database. * * @param dataId Data instance ID. * @param entryId EntryId or provisional entry ID when offline. * @param courseId Course ID. * @param contents The fields data to be created. * @param groupId Group id, 0 means that the function will determine the user group. * @param fields The fields that define the contents. * @param siteId Site ID. If not defined, current site. * @param forceOffline Force editing entry in offline. * @return Promise resolved when the action is done. */ async addEntry( dataId: number, entryId: number, courseId: number, contents: AddonModDataEntryWSField[], groupId: number = 0, fields: AddonModDataField[], siteId?: string, forceOffline: boolean = false, ): Promise { siteId = siteId || CoreSites.getCurrentSiteId(); // Convenience function to store a data to be synchronized later. const storeOffline = async (): Promise => { const entry = await AddonModDataOffline.saveEntry( dataId, entryId, AddonModDataAction.ADD, courseId, groupId, contents, undefined, siteId, ); return { // Return provissional entry Id. newentryid: entry.entryid, sent: false, }; }; // Checks to store offline. if (!CoreApp.isOnline() || forceOffline) { const notifications = this.checkFields(fields, contents); if (notifications.length > 0) { return { fieldnotifications: notifications }; } } // Remove unnecessary not synced actions. await this.deleteEntryOfflineAction(dataId, entryId, AddonModDataAction.ADD, siteId); // App is offline, store the action. if (!CoreApp.isOnline() || forceOffline) { return storeOffline(); } try { const result: AddonModDataAddEntryResult = await this.addEntryOnline(dataId, contents, groupId, siteId); result.sent = true; return result; } 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 storeOffline(); } } /** * Adds a new entry to a database. It does not cache calls. It will fail if offline or cannot connect. * * @param dataId Database ID. * @param data The fields data to be created. * @param groupId Group id, 0 means that the function will determine the user group. * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the action is done. */ async addEntryOnline( dataId: number, data: AddonModDataEntryWSField[], groupId?: number, siteId?: string, ): Promise { const site = await CoreSites.getSite(siteId); const params: AddonModDataAddEntryWSParams = { databaseid: dataId, data, }; if (typeof groupId !== 'undefined') { params.groupid = groupId; } return site.write('mod_data_add_entry', params); } /** * Approves or unapproves an entry. * * @param dataId Database ID. * @param entryId Entry ID. * @param approve Whether to approve (true) or unapprove the entry. * @param courseId Course ID. * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the action is done. */ async approveEntry( dataId: number, entryId: number, approve: boolean, courseId: number, siteId?: string, ): Promise { siteId = siteId || CoreSites.getCurrentSiteId(); // Convenience function to store a data to be synchronized later. const storeOffline = async (): Promise => { const action = approve ? AddonModDataAction.APPROVE : AddonModDataAction.DISAPPROVE; await AddonModDataOffline.saveEntry(dataId, entryId, action, courseId, undefined, undefined, undefined, siteId); return { sent: false, }; }; // Get if the opposite action is not synced. const oppositeAction = approve ? AddonModDataAction.DISAPPROVE : AddonModDataAction.APPROVE; const found = await this.deleteEntryOfflineAction(dataId, entryId, oppositeAction, siteId); if (found) { // Offline action has been found and deleted. Stop here. return; } if (!CoreApp.isOnline()) { // App is offline, store the action. return storeOffline(); } try { await this.approveEntryOnline(entryId, approve, siteId); return { sent: true, }; } 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 storeOffline(); } } /** * Approves or unapproves an entry. It does not cache calls. It will fail if offline or cannot connect. * * @param entryId Entry ID. * @param approve Whether to approve (true) or unapprove the entry. * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the action is done. */ async approveEntryOnline(entryId: number, approve: boolean, siteId?: string): Promise { const site = await CoreSites.getSite(siteId); const params: AddonModDataApproveEntryWSParams = { entryid: entryId, approve, }; await site.write('mod_data_approve_entry', params); } /** * Convenience function to check fields requeriments here named "notifications". * * @param fields The fields that define the contents. * @param contents The contents data of the fields. * @return Array of notifications if any or false. */ protected checkFields(fields: AddonModDataField[], contents: AddonModDataSubfieldData[]): AddonModDataFieldNotification[] { const notifications: AddonModDataFieldNotification[] = []; const contentsIndexed = CoreUtils.arrayToObjectMultiple(contents, 'fieldid'); // App is offline, check required fields. fields.forEach((field) => { const notification = AddonModDataFieldsDelegate.getFieldsNotifications(field, contentsIndexed[field.id]); if (notification) { notifications.push({ fieldname: field.name, notification, }); } }); return notifications; } /** * Deletes an entry. * * @param dataId Database ID. * @param entryId Entry ID. * @param courseId Course ID. * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the action is done. */ async deleteEntry(dataId: number, entryId: number, courseId: number, siteId?: string): Promise { siteId = siteId || CoreSites.getCurrentSiteId(); // Convenience function to store a data to be synchronized later. const storeOffline = async (): Promise => { await AddonModDataOffline.saveEntry( dataId, entryId, AddonModDataAction.DELETE, courseId, undefined, undefined, undefined, siteId, ); }; // Check if the opposite action is not synced and just delete it. const addedOffline = await this.deleteEntryOfflineAction(dataId, entryId, AddonModDataAction.ADD, siteId); if (addedOffline) { // Offline add action found and deleted. Stop here. return; } if (!CoreApp.isOnline()) { // App is offline, store the action. return storeOffline(); } try { await this.deleteEntryOnline(entryId, siteId); } 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 storeOffline(); } } /** * Deletes an entry. It does not cache calls. It will fail if offline or cannot connect. * * @param entryId Entry ID. * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the action is done. */ async deleteEntryOnline(entryId: number, siteId?: string): Promise { const site = await CoreSites.getSite(siteId); const params: AddonModDataDeleteEntryWSParams = { entryid: entryId, }; await site.write('mod_data_delete_entry', params); } /** * Delete entry offline action. * * @param dataId Database ID. * @param entryId Entry ID. * @param action Action name to delete. * @param siteId Site ID. * @return Resolved with true if the action has been found and deleted. */ protected async deleteEntryOfflineAction( dataId: number, entryId: number, action: AddonModDataAction, siteId: string, ): Promise { try { // Get other not not synced actions. await AddonModDataOffline.getEntry(dataId, entryId, action, siteId); await AddonModDataOffline.deleteEntry(dataId, entryId, action, siteId); return true; } catch { // Not found. return false; } } /** * Updates an existing entry. * * @param dataId Database ID. * @param entryId Entry ID. * @param courseId Course ID. * @param contents The contents data to be updated. * @param fields The fields that define the contents. * @param siteId Site ID. If not defined, current site. * @param forceOffline Force editing entry in offline. * @return Promise resolved when the action is done. */ async editEntry( dataId: number, entryId: number, courseId: number, contents: AddonModDataEntryWSField[], fields: AddonModDataField[], siteId?: string, forceOffline: boolean = false, ): Promise { siteId = siteId || CoreSites.getCurrentSiteId(); // Convenience function to store a data to be synchronized later. const storeOffline = async (): Promise => { await AddonModDataOffline.saveEntry( dataId, entryId, AddonModDataAction.EDIT, courseId, undefined, contents, undefined, siteId, ); return { updated: true, sent: false, }; }; if (!CoreApp.isOnline() || forceOffline) { const notifications = this.checkFields(fields, contents); if (notifications.length > 0) { return { fieldnotifications: notifications }; } } // Remove unnecessary not synced actions. await this.deleteEntryOfflineAction(dataId, entryId, AddonModDataAction.EDIT, siteId); if (!CoreApp.isOnline() || forceOffline) { // App is offline, store the action. return storeOffline(); } try { const result: AddonModDataEditEntryResult = await this.editEntryOnline(entryId, contents, siteId); result.sent = true; return result; } 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 storeOffline(); } } /** * Updates an existing entry. It does not cache calls. It will fail if offline or cannot connect. * * @param entryId Entry ID. * @param data The fields data to be updated. * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the action is done. */ async editEntryOnline( entryId: number, data: AddonModDataEntryWSField[], siteId?: string, ): Promise { const site = await CoreSites.getSite(siteId); const params: AddonModDataUpdateEntryWSParams = { entryid: entryId, data, }; return site.write('mod_data_update_entry', params); } /** * Performs the whole fetch of the entries in the database. * * @param dataId Data ID. * @param options Other options. * @return Promise resolved when done. */ fetchAllEntries(dataId: number, options: AddonModDataGetEntriesOptions = {}): Promise { options.siteId = options.siteId || CoreSites.getCurrentSiteId(); options = Object.assign({ page: 0, perPage: AddonModDataProvider.PER_PAGE, }, options); return this.fetchEntriesRecursive(dataId, [], options); } /** * Recursive call on fetch all entries. * * @param dataId Data ID. * @param entries Entries already fetch (just to concatenate them). * @param options Other options. * @return Promise resolved when done. */ protected async fetchEntriesRecursive( dataId: number, entries: AddonModDataEntry[], options: AddonModDataGetEntriesOptions, ): Promise { const result = await this.getEntries(dataId, options); entries = entries.concat(result.entries); const canLoadMore = options.perPage! > 0 && ((options.page! + 1) * options.perPage!) < result.totalcount; if (canLoadMore) { options.page!++; return this.fetchEntriesRecursive(dataId, entries, options); } return entries; } /** * Get cache key for data data WS calls. * * @param courseId Course ID. * @return Cache key. */ protected getDatabaseDataCacheKey(courseId: number): string { return ROOT_CACHE_KEY + 'data:' + courseId; } /** * Get prefix cache key for all database activity data WS calls. * * @param dataId Data ID. * @return Cache key. */ protected getDatabaseDataPrefixCacheKey(dataId: number): string { return ROOT_CACHE_KEY + dataId; } /** * Get a database data. If more than one is found, only the first will be returned. * * @param courseId Course ID. * @param key Name of the property to check. * @param value Value to search. * @param options Other options. * @return Promise resolved when the data is retrieved. */ protected async getDatabaseByKey( courseId: number, key: string, value: number, options: CoreSitesCommonWSOptions = {}, ): Promise { const site = await CoreSites.getSite(options.siteId); const params: AddonModDataGetDatabasesByCoursesWSParams = { courseids: [courseId], }; const preSets: CoreSiteWSPreSets = { cacheKey: this.getDatabaseDataCacheKey(courseId), updateFrequency: CoreSite.FREQUENCY_RARELY, component: AddonModDataProvider.COMPONENT, ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; const response = await site.read('mod_data_get_databases_by_courses', params, preSets); const currentData = response.databases.find((data) => data[key] == value); if (currentData) { return currentData; } throw new CoreError('Activity not found'); } /** * Get a data by course module ID. * * @param courseId Course ID. * @param cmId Course module ID. * @param options Other options. * @return Promise resolved when the data is retrieved. */ getDatabase(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise { return this.getDatabaseByKey(courseId, 'coursemodule', cmId, options); } /** * Get a data by ID. * * @param courseId Course ID. * @param id Data ID. * @param options Other options. * @return Promise resolved when the data is retrieved. */ getDatabaseById(courseId: number, id: number, options: CoreSitesCommonWSOptions = {}): Promise { return this.getDatabaseByKey(courseId, 'id', id, options); } /** * Get prefix cache key for all database access information data WS calls. * * @param dataId Data ID. * @return Cache key. */ protected getDatabaseAccessInformationDataPrefixCacheKey(dataId: number): string { return this.getDatabaseDataPrefixCacheKey(dataId) + ':access:'; } /** * Get cache key for database access information data WS calls. * * @param dataId Data ID. * @param groupId Group ID. * @return Cache key. */ protected getDatabaseAccessInformationDataCacheKey(dataId: number, groupId: number = 0): string { return this.getDatabaseAccessInformationDataPrefixCacheKey(dataId) + groupId; } /** * Get access information for a given database. * * @param dataId Data ID. * @param options Other options. * @return Promise resolved when the database is retrieved. */ async getDatabaseAccessInformation( dataId: number, options: AddonModDataAccessInfoOptions = {}, ): Promise { const site = await CoreSites.getSite(options.siteId); options.groupId = options.groupId || 0; const params: AddonModDataGetDataAccessInformationWSParams = { databaseid: dataId, groupid: options.groupId, }; const preSets: CoreSiteWSPreSets = { cacheKey: this.getDatabaseAccessInformationDataCacheKey(dataId, options.groupId), component: AddonModDataProvider.COMPONENT, componentId: options.cmId, ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; return site.read('mod_data_get_data_access_information', params, preSets); } /** * Get entries for a specific database and group. * * @param dataId Data ID. * @param options Other options. * @return Promise resolved when the database is retrieved. */ async getEntries(dataId: number, options: AddonModDataGetEntriesOptions = {}): Promise { options = Object.assign({ groupId: 0, sort: 0, order: 'DESC', page: 0, perPage: AddonModDataProvider.PER_PAGE, }, options); const site = await CoreSites.getSite(options.siteId); // Always use sort and order params to improve cache usage (entries are identified by params). const params: AddonModDataGetEntriesWSParams = { databaseid: dataId, returncontents: true, page: options.page, perpage: options.perPage, groupid: options.groupId, sort: options.sort, order: options.order, }; const preSets: CoreSiteWSPreSets = { cacheKey: this.getEntriesCacheKey(dataId, options.groupId), updateFrequency: CoreSite.FREQUENCY_SOMETIMES, component: AddonModDataProvider.COMPONENT, componentId: options.cmId, ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; const response = await site.read('mod_data_get_entries', params, preSets); const entriesFormatted = response.entries.map((entry) => this.formatEntryContents(entry)); return Object.assign(response, { entries: entriesFormatted, }); } /** * Get cache key for database entries data WS calls. * * @param dataId Data ID. * @param groupId Group ID. * @return Cache key. */ protected getEntriesCacheKey(dataId: number, groupId: number = 0): string { return this.getEntriesPrefixCacheKey(dataId) + groupId; } /** * Get prefix cache key for database all entries data WS calls. * * @param dataId Data ID. * @return Cache key. */ protected getEntriesPrefixCacheKey(dataId: number): string { return this.getDatabaseDataPrefixCacheKey(dataId) + ':entries:'; } /** * Get an entry of the database activity. * * @param dataId Data ID for caching purposes. * @param entryId Entry ID. * @param options Other options. * @return Promise resolved when the entry is retrieved. */ async getEntry( dataId: number, entryId: number, options: CoreCourseCommonModWSOptions = {}, ): Promise { const site = await CoreSites.getSite(options.siteId); const params: AddonModDataGetEntryWSParams = { entryid: entryId, returncontents: true, }; const preSets: CoreSiteWSPreSets = { cacheKey: this.getEntryCacheKey(dataId, entryId), updateFrequency: CoreSite.FREQUENCY_SOMETIMES, component: AddonModDataProvider.COMPONENT, componentId: options.cmId, ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; const response = await site.read('mod_data_get_entry', params, preSets); return Object.assign(response, { entry: this.formatEntryContents(response.entry), }); } /** * Formats the contents of an entry. * * @param entry Original WS entry. * @returns Entry with contents formatted. */ protected formatEntryContents(entry: AddonModDataEntryWS): AddonModDataEntry { return Object.assign(entry, { contents: CoreUtils.arrayToObject(entry.contents, 'fieldid'), }); } /** * Get cache key for database entry data WS calls. * * @param dataId Data ID for caching purposes. * @param entryId Entry ID. * @return Cache key. */ protected getEntryCacheKey(dataId: number, entryId: number): string { return this.getDatabaseDataPrefixCacheKey(dataId) + ':entry:' + entryId; } /** * Get the list of configured fields for the given database. * * @param dataId Data ID. * @param options Other options. * @return Promise resolved when the fields are retrieved. */ async getFields(dataId: number, options: CoreCourseCommonModWSOptions = {}): Promise { const site = await CoreSites.getSite(options.siteId); const params: AddonModDataGetFieldsWSParams = { databaseid: dataId, }; const preSets: CoreSiteWSPreSets = { cacheKey: this.getFieldsCacheKey(dataId), updateFrequency: CoreSite.FREQUENCY_RARELY, component: AddonModDataProvider.COMPONENT, componentId: options.cmId, ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; const response = await site.read('mod_data_get_fields', params, preSets); if (response.fields) { return response.fields; } throw new CoreError('No fields were returned.'); } /** * Get cache key for database fields data WS calls. * * @param dataId Data ID. * @return Cache key. */ protected getFieldsCacheKey(dataId: number): string { return this.getDatabaseDataPrefixCacheKey(dataId) + ':fields'; } /** * Invalidate the prefetched content. * To invalidate files, use AddonModDataProvider#invalidateFiles. * * @param moduleId The module ID. * @param courseId Course ID of the module. * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the data is invalidated. */ async invalidateContent(moduleId: number, courseId: number, siteId?: string): Promise { siteId = siteId || CoreSites.getCurrentSiteId(); const promises: Promise[] = []; promises.push(this.getDatabase(courseId, moduleId).then(async (database) => { const ps: Promise[] = []; // Do not invalidate module data before getting module info, we need it! ps.push(this.invalidateDatabaseData(courseId, siteId)); ps.push(this.invalidateDatabaseWSData(database.id, siteId)); ps.push(this.invalidateFieldsData(database.id, siteId)); await Promise.all(ps); return; })); promises.push(this.invalidateFiles(moduleId, siteId)); await CoreUtils.allPromises(promises); } /** * Invalidates database access information data. * * @param dataId Data ID. * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the data is invalidated. */ async invalidateDatabaseAccessInformationData(dataId: number, siteId?: string): Promise { const site = await CoreSites.getSite(siteId); await site.invalidateWsCacheForKeyStartingWith(this.getDatabaseAccessInformationDataPrefixCacheKey(dataId)); } /** * Invalidates database entries data. * * @param dataId Data ID. * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the data is invalidated. */ async invalidateEntriesData(dataId: number, siteId?: string): Promise { const site = await CoreSites.getSite(siteId); await site.invalidateWsCacheForKeyStartingWith(this.getEntriesPrefixCacheKey(dataId)); } /** * Invalidates database fields data. * * @param dataId Data ID. * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the data is invalidated. */ async invalidateFieldsData(dataId: number, siteId?: string): Promise { const site = await CoreSites.getSite(siteId); await site.invalidateWsCacheForKey(this.getFieldsCacheKey(dataId)); } /** * Invalidate the prefetched files. * * @param moduleId The module ID. * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the files are invalidated. */ async invalidateFiles(moduleId: number, siteId?: string): Promise { await CoreFilepool.invalidateFilesByComponent(siteId, AddonModDataProvider.COMPONENT, moduleId); } /** * Invalidates database data. * * @param courseId Course ID. * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the data is invalidated. */ async invalidateDatabaseData(courseId: number, siteId?: string): Promise { const site = await CoreSites.getSite(siteId); await site.invalidateWsCacheForKey(this.getDatabaseDataCacheKey(courseId)); } /** * Invalidates database data except files and module info. * * @param databaseId Data ID. * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the data is invalidated. */ async invalidateDatabaseWSData(databaseId: number, siteId?: string): Promise { const site = await CoreSites.getSite(siteId); await site.invalidateWsCacheForKeyStartingWith(this.getDatabaseDataPrefixCacheKey(databaseId)); } /** * Invalidates database entry data. * * @param dataId Data ID for caching purposes. * @param entryId Entry ID. * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the data is invalidated. */ async invalidateEntryData(dataId: number, entryId: number, siteId?: string): Promise { const site = await CoreSites.getSite(siteId); await site.invalidateWsCacheForKey(this.getEntryCacheKey(dataId, entryId)); } /** * Return whether or not the plugin is enabled in a certain site. Plugin is enabled if the database WS are available. * * @param siteId Site ID. If not defined, current site. * @return Promise resolved with true if plugin is enabled, rejected or resolved with false otherwise. * @since 3.3 */ async isPluginEnabled(siteId?: string): Promise { const site = await CoreSites.getSite(siteId); return site.wsAvailable('mod_data_get_data_access_information'); } /** * Report the database as being viewed. * * @param id Module ID. * @param name Name of the data. * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the WS call is successful. */ async logView(id: number, name?: string, siteId?: string): Promise { const params: AddonModDataViewDatabaseWSParams = { databaseid: id, }; await CoreCourseLogHelper.logSingle( 'mod_data_view_database', params, AddonModDataProvider.COMPONENT, id, name, 'data', {}, siteId, ); } /** * Performs search over a database. * * @param dataId The data instance id. * @param options Other options. * @return Promise resolved when the action is done. */ async searchEntries(dataId: number, options: AddonModDataSearchEntriesOptions = {}): Promise { const site = await CoreSites.getSite(options.siteId); options.groupId = options.groupId || 0; options.sort = options.sort || 0; options.order || options.order || 'DESC'; options.page = options.page || 0; options.perPage = options.perPage || AddonModDataProvider.PER_PAGE; options.readingStrategy = options.readingStrategy || CoreSitesReadingStrategy.PreferNetwork; const params: AddonModDataSearchEntriesWSParams = { databaseid: dataId, groupid: options.groupId, returncontents: true, page: options.page, perpage: options.perPage, }; const preSets: CoreSiteWSPreSets = { component: AddonModDataProvider.COMPONENT, componentId: options.cmId, ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; if (typeof options.sort != 'undefined') { params.sort = options.sort; } if (typeof options.order !== 'undefined') { params.order = options.order; } if (typeof options.search !== 'undefined') { params.search = options.search; } if (typeof options.advSearch !== 'undefined') { params.advsearch = options.advSearch; } const response = await site.read('mod_data_search_entries', params, preSets); const entriesFormatted = response.entries.map((entry) => this.formatEntryContents(entry)); return Object.assign(response, { entries: entriesFormatted, }); } } export const AddonModData = makeSingleton(AddonModDataProvider); /** * Params of mod_data_view_database WS. */ type AddonModDataViewDatabaseWSParams = { databaseid: number; // Data instance id. }; /** * Params of mod_data_search_entries WS. */ type AddonModDataSearchEntriesWSParams = { databaseid: number; // Data instance id. groupid?: number; // Group id, 0 means that the function will determine the user group. returncontents?: boolean; // Whether to return contents or not. search?: string; // Search string (empty when using advanced). advsearch?: AddonModDataSearchEntriesAdvancedField[]; sort?: number; // Sort the records by this field id, reserved ids are: // 0: timeadded // -1: firstname // -2: lastname // -3: approved // -4: timemodified. // Empty for using the default database setting. order?: string; // The direction of the sorting: 'ASC' or 'DESC'. Empty for using the default database setting. page?: number; // The page of records to return. perpage?: number; // The number of records to return per page. }; /** * Data returned by mod_data_search_entries WS. */ export type AddonModDataSearchEntriesWSResponse = { entries: AddonModDataEntryWS[]; totalcount: number; // Total count of records returned by the search. maxcount?: number; // Total count of records that the user could see in the database (if all the search criterias were removed). listviewcontents?: string; // The list view contents as is rendered in the site. warnings?: CoreWSExternalWarning[]; }; /** * Options to pass to get access info. */ export type AddonModDataAccessInfoOptions = CoreCourseCommonModWSOptions & { groupId?: number; // Group Id. }; /** * Options to pass to get entries. */ export type AddonModDataGetEntriesOptions = CoreCourseCommonModWSOptions & { groupId?: number; // Group Id. sort?: number; // Sort the records by this field id, defaults to 0. Reserved ids are: // 0: timeadded // -1: firstname // -2: lastname // -3: approved // -4: timemodified order?: string; // The direction of the sorting: 'ASC' or 'DESC'. Defaults to 'DESC'. page?: number; // Page of records to return. Defaults to 0. perPage?: number; // Records per page to return. Defaults to AddonModDataProvider.PER_PAGE. }; /** * Options to pass to search entries. */ export type AddonModDataSearchEntriesOptions = AddonModDataGetEntriesOptions & { search?: string; // Search text. It will be used if advSearch is not defined. advSearch?: AddonModDataSearchEntriesAdvancedField[]; }; /** * Database entry (online or offline). */ export type AddonModDataEntry = Omit & { contents: AddonModDataEntryFields; // The record contents. tags?: CoreTagItem[]; // Tags. // Calculated data. deleted?: boolean; // Entry is deleted offline. hasOffline?: boolean; // Entry has offline actions. }; /** * Database entry data from WS. */ export type AddonModDataEntryWS = { id: number; // Record id. userid: number; // The id of the user who created the record. groupid: number; // The group id this record belongs to (0 for no groups). dataid: number; // The database id this record belongs to. timecreated: number; // Time the record was created. timemodified: number; // Last time the record was modified. approved: boolean; // Whether the entry has been approved (if the database is configured in that way). canmanageentry: boolean; // Whether the current user can manage this entry. fullname?: string; // The user who created the entry fullname. contents?: AddonModDataEntryField[]; tags?: CoreTagItem[]; // Tags. }; /** * Entry field content. */ export type AddonModDataEntryField = { id: number; // Content id. fieldid: number; // The field type of the content. recordid: number; // The record this content belongs to. content: string; // Contents. content1: string; // Contents. content2: string; // Contents. content3: string; // Contents. content4: string; // Contents. files: CoreFileEntry[]; }; /** * Entry contents indexed by field id. */ export type AddonModDataEntryFields = { [fieldid: number]: AddonModDataEntryField; }; /** * List of entries returned by web service and helper functions. */ export type AddonModDataEntries = { entries: AddonModDataEntry[]; // Online entries. totalcount: number; // Total count of online entries or found entries. maxcount?: number; // Total count of online entries. Only returned when searching. offlineEntries?: AddonModDataEntry[]; // Offline entries. hasOfflineActions?: boolean; // Whether the database has offline data. hasOfflineRatings?: boolean; // Whether the database has offline ratings. }; /** * Subfield form data. */ export type AddonModDataSubfieldData = { fieldid: number; subfield?: string; value?: unknown; // Value encoded in JSON. files?: CoreFileEntry[]; }; /** * Params of mod_data_get_data_access_information WS. */ type AddonModDataGetDataAccessInformationWSParams = { databaseid: number; // Database instance id. groupid?: number; // Group id, 0 means that the function will determine the user group. }; /** * Data returned by mod_data_get_data_access_information WS. */ export type AddonModDataGetDataAccessInformationWSResponse = { groupid: number; // User current group id (calculated). canaddentry: boolean; // Whether the user can add entries or not. canmanageentries: boolean; // Whether the user can manage entries or not. canapprove: boolean; // Whether the user can approve entries or not. timeavailable: boolean; // Whether the database is available or not by time restrictions. inreadonlyperiod: boolean; // Whether the database is in read mode only. numentries: number; // The number of entries the current user added. entrieslefttoadd: number; // The number of entries left to complete the activity. entrieslefttoview: number; // The number of entries left to view other users entries. warnings?: CoreWSExternalWarning[]; }; /** * Params of mod_data_get_databases_by_courses WS. */ type AddonModDataGetDatabasesByCoursesWSParams = { courseids?: number[]; // Array of course ids. }; /** * Data returned by mod_data_get_databases_by_courses WS. */ type AddonModDataGetDatabasesByCoursesWSResponse = { databases: AddonModDataData[]; warnings?: CoreWSExternalWarning[]; }; /** * Database data returned by mod_assign_get_assignments. */ export type AddonModDataData = { id: number; // Database id. course: number; // Course id. name: string; // Database name. intro: string; // The Database intro. introformat?: number; // Intro format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). comments: boolean; // Comments enabled. timeavailablefrom: number; // Timeavailablefrom field. timeavailableto: number; // Timeavailableto field. timeviewfrom: number; // Timeviewfrom field. timeviewto: number; // Timeviewto field. requiredentries: number; // Requiredentries field. requiredentriestoview: number; // Requiredentriestoview field. maxentries: number; // Maxentries field. rssarticles: number; // Rssarticles field. singletemplate: string; // Singletemplate field. listtemplate: string; // Listtemplate field. listtemplateheader: string; // Listtemplateheader field. listtemplatefooter: string; // Listtemplatefooter field. addtemplate: string; // Addtemplate field. rsstemplate: string; // Rsstemplate field. rsstitletemplate: string; // Rsstitletemplate field. csstemplate: string; // Csstemplate field. jstemplate: string; // Jstemplate field. asearchtemplate: string; // Asearchtemplate field. approval: boolean; // Approval field. manageapproved: boolean; // Manageapproved field. scale?: number; // Scale field. assessed?: number; // Assessed field. assesstimestart?: number; // Assesstimestart field. assesstimefinish?: number; // Assesstimefinish field. defaultsort: number; // Defaultsort field. defaultsortdir: number; // Defaultsortdir field. editany?: boolean; // Editany field (not used any more). notification?: number; // Notification field (not used any more). timemodified?: number; // Time modified. coursemodule: number; // Coursemodule. introfiles?: CoreWSExternalFile[]; }; /** * Params of mod_data_add_entry WS. */ type AddonModDataAddEntryWSParams = { databaseid: number; // Data instance id. groupid?: number; // Group id, 0 means that the function will determine the user group. data: AddonModDataEntryWSField[]; // The fields data to be created. }; /** * Data returned by mod_data_add_entry WS. */ export type AddonModDataAddEntryWSResponse = { newentryid: number; // True new created entry id. 0 if the entry was not created. generalnotifications: string[]; fieldnotifications: AddonModDataFieldNotification[]; warnings?: CoreWSExternalWarning[]; }; /** * Params of mod_data_approve_entry WS. */ type AddonModDataApproveEntryWSParams = { entryid: number; // Record entry id. approve?: boolean; // Whether to approve (true) or unapprove the entry. }; /** * Params of mod_data_delete_entry WS. */ type AddonModDataDeleteEntryWSParams = { entryid: number; // Record entry id. }; /** * Params of mod_data_update_entry WS. */ type AddonModDataUpdateEntryWSParams = { entryid: number; // The entry record id. data: AddonModDataEntryWSField[]; // The fields data to be updated. }; /** * Data returned by mod_data_update_entry WS. */ export type AddonModDataUpdateEntryWSResponse = { updated: boolean; // True if the entry was successfully updated, false other wise. generalnotifications: string[]; fieldnotifications: AddonModDataFieldNotification[]; warnings?: CoreWSExternalWarning[]; }; // The fields data to be created or updated. export type AddonModDataEntryWSField = { fieldid: number; // The field id. AddonModDataSubfieldData subfield?: string; // The subfield name (if required). value: string; // The contents for the field always JSON encoded. }; /** * Params of mod_data_get_entries WS. */ type AddonModDataGetEntriesWSParams = { databaseid: number; // Data instance id. groupid?: number; // Group id, 0 means that the function will determine the user group. returncontents?: boolean; // Whether to return contents or not. This will return each entry raw contents and the complete list // view(using the template). sort?: number; // Sort the records by this field id, reserved ids are: // 0: timeadded // -1: firstname // -2: lastname // -3: approved // -4: timemodified. // Empty for using the default database setting. order?: string; // The direction of the sorting: 'ASC' or 'DESC'. Empty for using the default database setting. page?: number; // The page of records to return. perpage?: number; // The number of records to return per page. }; /** * Data returned by mod_data_get_entries WS. */ export type AddonModDataGetEntriesWSResponse = { entries: AddonModDataEntryWS[]; totalcount: number; // Total count of records. totalfilesize: number; // Total size (bytes) of the files included in the records. listviewcontents?: string; // The list view contents as is rendered in the site. warnings?: CoreWSExternalWarning[]; }; /** * Params of mod_data_get_entry WS. */ type AddonModDataGetEntryWSParams = { entryid: number; // Record entry id. returncontents?: boolean; // Whether to return contents or not. }; /** * Data returned by mod_data_get_entry WS. */ type AddonModDataGetEntryWSResponse = { entry: AddonModDataEntryWS; entryviewcontents?: string; // The entry as is rendered in the site. ratinginfo?: CoreRatingInfo; // Rating information. warnings?: CoreWSExternalWarning[]; }; /** * Data returned by mod_data_get_entry WS. */ export type AddonModDataGetEntryFormatted = { entry: AddonModDataEntry; entryviewcontents?: string; // The entry as is rendered in the site. ratinginfo?: CoreRatingInfo; // Rating information. warnings?: CoreWSExternalWarning[]; }; export type AddonModDataFieldNotification = { fieldname: string; // The field name. notification: string; // The notification for the field. }; /** * Params of mod_data_get_fields WS. */ type AddonModDataGetFieldsWSParams = { databaseid: number; // Database instance id. }; /** * Data returned by mod_data_get_fields WS. */ type AddonModDataGetFieldsWSResponse = { fields: AddonModDataField[]; warnings?: CoreWSExternalWarning[]; }; /** * Field data returned by mod_data_get_fields WS. */ export type AddonModDataField = { id: number; // Field id. dataid: number; // The field type of the content. type: string; // The field type. name: string; // The field name. description: string; // The field description. required: boolean; // Whether is a field required or not. param1: string; // Field parameters. param2: string; // Field parameters. param3: string; // Field parameters. param4: string; // Field parameters. param5: string; // Field parameters. param6: string; // Field parameters. param7: string; // Field parameters. param8: string; // Field parameters. param9: string; // Field parameters. param10: string; // Field parameters. }; export type AddonModDataEntryChangedEventData = { dataId: number; entryId?: number; deleted?: boolean; }; /** * Advanced search field. */ export type AddonModDataSearchEntriesAdvancedField = { name: string; // Field key for search. Use fn or ln for first or last name. value: string; // JSON encoded value for search. }; /** * Advanced search field. */ export type AddonModDataSearchEntriesAdvancedFieldFormatted = { name: string; // Field key for search. Use fn or ln for first or last name. value: unknown; // JSON encoded value for search. }; export type AddonModDataAddEntryResult = Partial & { sent?: boolean; // True if sent, false if stored offline. }; export type AddonModDataApproveEntryResult = { sent?: boolean; // True if sent, false if stored offline. }; export type AddonModDataEditEntryResult = Partial & { sent?: boolean; // True if sent, false if stored offline. };