From 010475b7901aa5e3a770f6de18aa1c1ac9e82c26 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 8 Apr 2021 16:30:00 +0200 Subject: [PATCH] MOBILE-3644 glossary: Migrate services --- src/addons/mod/glossary/glossary.module.ts | 69 + src/addons/mod/glossary/lang.json | 31 + .../glossary/services/database/glossary.ts | 121 ++ .../mod/glossary/services/glossary-helper.ts | 112 ++ .../mod/glossary/services/glossary-offline.ts | 258 +++ .../mod/glossary/services/glossary-sync.ts | 368 +++++ src/addons/mod/glossary/services/glossary.ts | 1465 +++++++++++++++++ .../glossary/services/handlers/edit-link.ts | 78 + .../glossary/services/handlers/entry-link.ts | 75 + .../glossary/services/handlers/index-link.ts | 33 + .../glossary/services/handlers/list-link.ts | 33 + .../mod/glossary/services/handlers/module.ts | 92 ++ .../glossary/services/handlers/prefetch.ts | 235 +++ .../glossary/services/handlers/sync-cron.ts | 44 + .../glossary/services/handlers/tag-area.ts | 53 + src/addons/mod/mod.module.ts | 2 + src/core/features/compile/services/compile.ts | 4 +- 17 files changed, 3071 insertions(+), 2 deletions(-) create mode 100644 src/addons/mod/glossary/glossary.module.ts create mode 100644 src/addons/mod/glossary/lang.json create mode 100644 src/addons/mod/glossary/services/database/glossary.ts create mode 100644 src/addons/mod/glossary/services/glossary-helper.ts create mode 100644 src/addons/mod/glossary/services/glossary-offline.ts create mode 100644 src/addons/mod/glossary/services/glossary-sync.ts create mode 100644 src/addons/mod/glossary/services/glossary.ts create mode 100644 src/addons/mod/glossary/services/handlers/edit-link.ts create mode 100644 src/addons/mod/glossary/services/handlers/entry-link.ts create mode 100644 src/addons/mod/glossary/services/handlers/index-link.ts create mode 100644 src/addons/mod/glossary/services/handlers/list-link.ts create mode 100644 src/addons/mod/glossary/services/handlers/module.ts create mode 100644 src/addons/mod/glossary/services/handlers/prefetch.ts create mode 100644 src/addons/mod/glossary/services/handlers/sync-cron.ts create mode 100644 src/addons/mod/glossary/services/handlers/tag-area.ts diff --git a/src/addons/mod/glossary/glossary.module.ts b/src/addons/mod/glossary/glossary.module.ts new file mode 100644 index 000000000..21b6a7afb --- /dev/null +++ b/src/addons/mod/glossary/glossary.module.ts @@ -0,0 +1,69 @@ +// (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 { APP_INITIALIZER, NgModule, Type } from '@angular/core'; +import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate'; +import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate'; +import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate'; +import { CoreTagAreaDelegate } from '@features/tag/services/tag-area-delegate'; +import { CoreCronDelegate } from '@services/cron'; +import { CORE_SITE_SCHEMAS } from '@services/sites'; +import { SITE_SCHEMA, OFFLINE_SITE_SCHEMA } from './services/database/glossary'; +import { AddonModGlossaryProvider } from './services/glossary'; +import { AddonModGlossaryHelperProvider } from './services/glossary-helper'; +import { AddonModGlossaryOfflineProvider } from './services/glossary-offline'; +import { AddonModGlossarySyncProvider } from './services/glossary-sync'; +import { AddonModGlossaryEditLinkHandler } from './services/handlers/edit-link'; +import { AddonModGlossaryEntryLinkHandler } from './services/handlers/entry-link'; +import { AddonModGlossaryIndexLinkHandler } from './services/handlers/index-link'; +import { AddonModGlossaryListLinkHandler } from './services/handlers/list-link'; +import { AddonModGlossaryModuleHandler } from './services/handlers/module'; +import { AddonModGlossaryPrefetchHandler } from './services/handlers/prefetch'; +import { AddonModGlossarySyncCronHandler } from './services/handlers/sync-cron'; +import { AddonModGlossaryTagAreaHandler } from './services/handlers/tag-area'; + +export const ADDON_MOD_GLOSSARY_SERVICES: Type[] = [ + AddonModGlossaryProvider, + AddonModGlossaryOfflineProvider, + AddonModGlossarySyncProvider, + AddonModGlossaryHelperProvider, +]; + +@NgModule({ + imports: [ + ], + providers: [ + { + provide: CORE_SITE_SCHEMAS, + useValue: [SITE_SCHEMA, OFFLINE_SITE_SCHEMA], + multi: true, + }, + { + provide: APP_INITIALIZER, + multi: true, + deps: [], + useFactory: () => () => { + CoreCourseModuleDelegate.registerHandler(AddonModGlossaryModuleHandler.instance); + CoreCourseModulePrefetchDelegate.registerHandler(AddonModGlossaryPrefetchHandler.instance); + CoreCronDelegate.register(AddonModGlossarySyncCronHandler.instance); + CoreContentLinksDelegate.registerHandler(AddonModGlossaryIndexLinkHandler.instance); + CoreContentLinksDelegate.registerHandler(AddonModGlossaryListLinkHandler.instance); + CoreContentLinksDelegate.registerHandler(AddonModGlossaryEditLinkHandler.instance); + CoreContentLinksDelegate.registerHandler(AddonModGlossaryEntryLinkHandler.instance); + CoreTagAreaDelegate.registerHandler(AddonModGlossaryTagAreaHandler.instance); + }, + }, + ], +}) +export class AddonModGlossaryModule {} diff --git a/src/addons/mod/glossary/lang.json b/src/addons/mod/glossary/lang.json new file mode 100644 index 000000000..ba4329f33 --- /dev/null +++ b/src/addons/mod/glossary/lang.json @@ -0,0 +1,31 @@ +{ + "addentry": "Add a new entry", + "aliases": "Keyword(s)", + "attachment": "Attachment", + "browsemode": "Browse entries", + "byalphabet": "Alphabetically", + "byauthor": "Group by author", + "bycategory": "Group by category", + "bynewestfirst": "Newest first", + "byrecentlyupdated": "Recently updated", + "bysearch": "Search", + "cannoteditentry": "Cannot edit entry", + "casesensitive": "This entry is case sensitive", + "categories": "Categories", + "concept": "Concept", + "definition": "Definition", + "entriestobesynced": "Entries to be synced", + "entrypendingapproval": "This entry is pending approval.", + "entryusedynalink": "This entry should be automatically linked", + "errconceptalreadyexists": "This concept already exists. No duplicates allowed in this glossary.", + "errorloadingentries": "An error occurred while loading entries.", + "errorloadingentry": "An error occurred while loading the entry.", + "errorloadingglossary": "An error occurred while loading the glossary.", + "fillfields": "Concept and definition are mandatory fields.", + "fullmatch": "Match whole words only", + "linking": "Auto-linking", + "modulenameplural": "Glossaries", + "noentriesfound": "No entries were found.", + "searchquery": "Search query", + "tagarea_glossary_entries": "Glossary entries" +} diff --git a/src/addons/mod/glossary/services/database/glossary.ts b/src/addons/mod/glossary/services/database/glossary.ts new file mode 100644 index 000000000..4149405c9 --- /dev/null +++ b/src/addons/mod/glossary/services/database/glossary.ts @@ -0,0 +1,121 @@ +// (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 AddonModGlossaryProvider. + */ +export const ENTRIES_TABLE_NAME = 'addon_mod_glossary_entry_glossaryid'; +export const SITE_SCHEMA: CoreSiteSchema = { + name: 'AddonModGlossaryProvider', + version: 1, + tables: [ + { + name: ENTRIES_TABLE_NAME, + columns: [ + { + name: 'entryid', + type: 'INTEGER', + primaryKey: true, + }, + { + name: 'glossaryid', + type: 'INTEGER', + }, + { + name: 'pagefrom', + type: 'INTEGER', + }, + ], + }, + ], +}; + +/** + * Database variables for AddonModGlossaryOfflineProvider. + */ +export const OFFLINE_ENTRIES_TABLE_NAME = 'addon_mod_glossary_entrues'; +export const OFFLINE_SITE_SCHEMA: CoreSiteSchema = { + name: 'AddonModGlossaryOfflineProvider', + version: 1, + tables: [ + { + name: OFFLINE_ENTRIES_TABLE_NAME, + columns: [ + { + name: 'glossaryid', + type: 'INTEGER', + }, + { + name: 'courseid', + type: 'INTEGER', + }, + { + name: 'concept', + type: 'TEXT', + }, + { + name: 'definition', + type: 'TEXT', + }, + { + name: 'definitionformat', + type: 'TEXT', + }, + { + name: 'userid', + type: 'INTEGER', + }, + { + name: 'timecreated', + type: 'INTEGER', + }, + { + name: 'options', + type: 'TEXT', + }, + { + name: 'attachments', + type: 'TEXT', + }, + ], + primaryKeys: ['glossaryid', 'concept', 'timecreated'], + }, + ], +}; + +/** + * Glossary entry to get glossaryid from entryid. + */ +export type AddonModGlossaryEntryDBRecord = { + entryid: number; + glossaryid: number; + pagefrom: number; +}; + +/** + * Glossary offline entry. + */ +export type AddonModGlossaryOfflineEntryDBRecord = { + glossaryid: number; + courseid: number; + concept: string; + definition: string; + definitionformat: string; + userid: number; + timecreated: number; + options: string; + attachments: string; +}; diff --git a/src/addons/mod/glossary/services/glossary-helper.ts b/src/addons/mod/glossary/services/glossary-helper.ts new file mode 100644 index 000000000..a970e3d77 --- /dev/null +++ b/src/addons/mod/glossary/services/glossary-helper.ts @@ -0,0 +1,112 @@ +// (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 { FileEntry } from '@ionic-native/file/ngx'; +import { CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader'; +import { CoreFile } from '@services/file'; +import { CoreUtils } from '@services/utils/utils'; +import { AddonModGlossaryOffline } from './glossary-offline'; +import { AddonModGlossaryNewEntry, AddonModGlossaryNewEntryWithFiles } from './glossary'; +import { makeSingleton } from '@singletons'; +import { CoreWSExternalFile } from '@services/ws'; + +/** + * Helper to gather some common functions for glossary. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModGlossaryHelperProvider { + + /** + * Delete stored attachment files for a new entry. + * + * @param glossaryId Glossary ID. + * @param entryName The name of the entry. + * @param timeCreated The time the entry was created. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when deleted. + */ + async deleteStoredFiles(glossaryId: number, entryName: string, timeCreated: number, siteId?: string): Promise { + const folderPath = await AddonModGlossaryOffline.getEntryFolder(glossaryId, entryName, timeCreated, siteId); + + await CoreUtils.ignoreErrors(CoreFile.removeDir(folderPath)); + } + + /** + * Get a list of stored attachment files for a new entry. See AddonModGlossaryHelperProvider#storeFiles. + * + * @param glossaryId lossary ID. + * @param entryName The name of the entry. + * @param timeCreated The time the entry was created. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with the files. + */ + async getStoredFiles(glossaryId: number, entryName: string, timeCreated: number, siteId?: string): Promise { + const folderPath = await AddonModGlossaryOffline.getEntryFolder(glossaryId, entryName, timeCreated, siteId); + + return CoreFileUploader.getStoredFiles(folderPath); + } + + /** + * Check if the data of an entry has changed. + * + * @param entry Current data. + * @param files Files attached. + * @param original Original content. + * @return True if data has changed, false otherwise. + */ + hasEntryDataChanged( + entry: AddonModGlossaryNewEntry, + files: (CoreWSExternalFile | FileEntry)[], + original?: AddonModGlossaryNewEntryWithFiles, + ): boolean { + if (!original || typeof original.concept == 'undefined') { + // There is no original data. + return !!(entry.definition || entry.concept || files.length > 0); + } + + if (original.definition != entry.definition || original.concept != entry.concept) { + return true; + } + + return CoreFileUploader.areFileListDifferent(files, original.files); + } + + /** + * Given a list of files (either online files or local files), store the local files in a local folder + * to be submitted later. + * + * @param glossaryId Glossary ID. + * @param entryName The name of the entry. + * @param timeCreated The time the entry was created. + * @param files List of files. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved if success, rejected otherwise. + */ + async storeFiles( + glossaryId: number, + entryName: string, + timeCreated: number, + files: (CoreWSExternalFile | FileEntry)[], + siteId?: string, + ): Promise { + // Get the folder where to store the files. + const folderPath = await AddonModGlossaryOffline.getEntryFolder(glossaryId, entryName, timeCreated, siteId); + + return CoreFileUploader.storeFilesToUpload(folderPath, files); + } + +} + +export const AddonModGlossaryHelper = makeSingleton(AddonModGlossaryHelperProvider); diff --git a/src/addons/mod/glossary/services/glossary-offline.ts b/src/addons/mod/glossary/services/glossary-offline.ts new file mode 100644 index 000000000..4c9d7216b --- /dev/null +++ b/src/addons/mod/glossary/services/glossary-offline.ts @@ -0,0 +1,258 @@ +// (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 { 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 { AddonModGlossaryOfflineEntryDBRecord, OFFLINE_ENTRIES_TABLE_NAME } from './database/glossary'; +import { AddonModGlossaryDiscardedEntry, AddonModGlossaryEntryOption } from './glossary'; + +/** + * Service to handle offline glossary. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModGlossaryOfflineProvider { + + /** + * Delete a new entry. + * + * @param glossaryId Glossary ID. + * @param concept Glossary entry concept. + * @param timeCreated The time the entry was created. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved if deleted, rejected if failure. + */ + async deleteNewEntry(glossaryId: number, concept: string, timeCreated: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + const conditions: Partial = { + glossaryid: glossaryId, + concept: concept, + timecreated: timeCreated, + }; + + await site.getDb().deleteRecords(OFFLINE_ENTRIES_TABLE_NAME, conditions); + } + + /** + * Get all the stored new entries from all the glossaries. + * + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with entries. + */ + async getAllNewEntries(siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + const records = await site.getDb().getRecords(OFFLINE_ENTRIES_TABLE_NAME); + + return records.map(record => this.parseRecord(record)); + } + + /** + * Get a stored new entry. + * + * @param glossaryId Glossary ID. + * @param concept Glossary entry concept. + * @param timeCreated The time the entry was created. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with entry. + */ + async getNewEntry( + glossaryId: number, + concept: string, + timeCreated: number, + siteId?: string, + ): Promise { + const site = await CoreSites.getSite(siteId); + + const conditions: Partial = { + glossaryid: glossaryId, + concept: concept, + timecreated: timeCreated, + }; + + const record = await site.getDb().getRecord(OFFLINE_ENTRIES_TABLE_NAME, conditions); + + return this.parseRecord(record); + } + + /** + * Get all the stored add entry data from a certain glossary. + * + * @param glossaryId Glossary ID. + * @param siteId Site ID. If not defined, current site. + * @param userId User the entries belong to. If not defined, current user in site. + * @return Promise resolved with entries. + */ + async getGlossaryNewEntries(glossaryId: number, siteId?: string, userId?: number): Promise { + const site = await CoreSites.getSite(siteId); + + const conditions: Partial = { + glossaryid: glossaryId, + userid: userId || site.getUserId(), + }; + + const records = await site.getDb().getRecords(OFFLINE_ENTRIES_TABLE_NAME, conditions); + + return records.map(record => this.parseRecord(record)); + } + + /** + * Check if a concept is used offline. + * + * @param glossaryId Glossary ID. + * @param concept Concept to check. + * @param timeCreated Time of the entry we are editing. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with true if concept is found, false otherwise. + */ + async isConceptUsed(glossaryId: number, concept: string, timeCreated?: number, siteId?: string): Promise { + try { + const site = await CoreSites.getSite(siteId); + + const conditions: Partial = { + glossaryid: glossaryId, + concept: concept, + }; + + const entries = + await site.getDb().getRecords(OFFLINE_ENTRIES_TABLE_NAME, conditions); + + if (!entries.length) { + return false; + } + + if (entries.length > 1 || !timeCreated) { + return true; + } + + // If there's only one entry, check that is not the one we are editing. + return CoreUtils.promiseFails(this.getNewEntry(glossaryId, concept, timeCreated, siteId)); + } catch { + // No offline data found, return false. + return false; + } + } + + /** + * Save a new entry to be sent later. + * + * @param glossaryId Glossary ID. + * @param concept Glossary entry concept. + * @param definition Glossary entry concept definition. + * @param courseId Course ID of the glossary. + * @param options Options for the entry. + * @param attachments Result of CoreFileUploaderProvider#storeFilesToUpload for attachments. + * @param timeCreated The time the entry was created. If not defined, current time. + * @param siteId Site ID. If not defined, current site. + * @param userId User the entry belong to. If not defined, current user in site. + * @param discardEntry The entry provided will be discarded if found. + * @return Promise resolved if stored, rejected if failure. + */ + async addNewEntry( + glossaryId: number, + concept: string, + definition: string, + courseId: number, + options?: Record, + attachments?: CoreFileUploaderStoreFilesResult, + timeCreated?: number, + siteId?: string, + userId?: number, + discardEntry?: AddonModGlossaryDiscardedEntry, + ): Promise { + const site = await CoreSites.getSite(siteId); + + const entry: AddonModGlossaryOfflineEntryDBRecord = { + glossaryid: glossaryId, + courseid: courseId, + concept: concept, + definition: definition, + definitionformat: 'html', + options: JSON.stringify(options || {}), + attachments: JSON.stringify(attachments), + userid: userId || site.getUserId(), + timecreated: timeCreated || Date.now(), + }; + + // If editing an offline entry, delete previous first. + if (discardEntry) { + await this.deleteNewEntry(glossaryId, discardEntry.concept, discardEntry.timecreated, site.getId()); + } + + await site.getDb().insertRecord(OFFLINE_ENTRIES_TABLE_NAME, entry); + + return false; + } + + /** + * Get the path to the folder where to store files for offline attachments in a glossary. + * + * @param glossaryId Glossary ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with the path. + */ + async getGlossaryFolder(glossaryId: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + const siteFolderPath = CoreFile.getSiteFolder(site.getId()); + const folderPath = 'offlineglossary/' + glossaryId; + + return CoreTextUtils.concatenatePaths(siteFolderPath, folderPath); + } + + /** + * Get the path to the folder where to store files for a new offline entry. + * + * @param glossaryId Glossary ID. + * @param concept The name of the entry. + * @param timeCreated Time to allow duplicated entries. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with the path. + */ + async getEntryFolder(glossaryId: number, concept: string, timeCreated: number, siteId?: string): Promise { + const folderPath = await this.getGlossaryFolder(glossaryId, siteId); + + return CoreTextUtils.concatenatePaths(folderPath, 'newentry_' + concept + '_' + timeCreated); + } + + /** + * Parse "options" and "attachments" columns of a fetched record. + * + * @param records Record object + * @return Record object with columns parsed. + */ + protected parseRecord(record: AddonModGlossaryOfflineEntryDBRecord): AddonModGlossaryOfflineEntry { + return Object.assign(record, { + options: > CoreTextUtils.parseJSON(record.options), + attachments: record.attachments ? + CoreTextUtils.parseJSON(record.attachments) : undefined, + }); + } + +} + +export const AddonModGlossaryOffline = makeSingleton(AddonModGlossaryOfflineProvider); + +/** + * Glossary offline entry with parsed data. + */ +export type AddonModGlossaryOfflineEntry = Omit & { + options: Record; + attachments?: CoreFileUploaderStoreFilesResult; +}; diff --git a/src/addons/mod/glossary/services/glossary-sync.ts b/src/addons/mod/glossary/services/glossary-sync.ts new file mode 100644 index 000000000..ca697c46d --- /dev/null +++ b/src/addons/mod/glossary/services/glossary-sync.ts @@ -0,0 +1,368 @@ +// (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 { ContextLevel } from '@/core/constants'; +import { Injectable } from '@angular/core'; +import { FileEntry } from '@ionic-native/file/ngx'; +import { CoreSyncBlockedError } from '@classes/base-sync'; +import { CoreNetworkError } from '@classes/errors/network-error'; +import { CoreCourseActivitySyncBaseProvider } from '@features/course/classes/activity-sync'; +import { CoreCourseLogHelper } from '@features/course/services/log-helper'; +import { CoreRatingSync } from '@features/rating/services/rating-sync'; +import { CoreApp } from '@services/app'; +import { CoreSites } from '@services/sites'; +import { CoreSync } from '@services/sync'; +import { CoreTextUtils } from '@services/utils/text'; +import { CoreUtils } from '@services/utils/utils'; +import { CoreWSExternalFile } from '@services/ws'; +import { makeSingleton, Translate } from '@singletons'; +import { CoreEvents } from '@singletons/events'; +import { AddonModGlossary, AddonModGlossaryProvider } from './glossary'; +import { AddonModGlossaryHelper } from './glossary-helper'; +import { AddonModGlossaryOffline, AddonModGlossaryOfflineEntry } from './glossary-offline'; +import { CoreFileUploader } from '@features/fileuploader/services/fileuploader'; + +/** + * Service to sync glossaries. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModGlossarySyncProvider extends CoreCourseActivitySyncBaseProvider { + + static readonly AUTO_SYNCED = 'addon_mod_glossary_autom_synced'; + + protected componentTranslatableString = 'glossary'; + + constructor() { + super('AddonModGlossarySyncProvider'); + } + + /** + * Try to synchronize all the glossaries 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. + */ + syncAllGlossaries(siteId?: string, force?: boolean): Promise { + return this.syncOnSites('all glossaries', this.syncAllGlossariesFunc.bind(this, !!force), siteId); + } + + /** + * Sync all glossaries on a 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 syncAllGlossariesFunc(force: boolean, siteId: string): Promise { + siteId = siteId || CoreSites.getCurrentSiteId(); + + await Promise.all([ + this.syncAllGlossariesEntries(force, siteId), + this.syncRatings(undefined, force, siteId), + ]); + } + + /** + * Sync entried of all glossaries on a 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 syncAllGlossariesEntries(force: boolean, siteId: string): Promise { + const entries = await AddonModGlossaryOffline.getAllNewEntries(siteId); + + // Do not sync same glossary twice. + const treated: Record = {}; + + await Promise.all(entries.map(async (entry) => { + if (treated[entry.glossaryid]) { + return; + } + + treated[entry.glossaryid] = true; + + const result = force ? + await this.syncGlossaryEntries(entry.glossaryid, entry.userid, siteId) : + await this.syncGlossaryEntriesIfNeeded(entry.glossaryid, entry.userid, siteId); + + if (result?.updated) { + // Sync successful, send event. + CoreEvents.trigger(AddonModGlossarySyncProvider.AUTO_SYNCED, { + glossaryId: entry.glossaryid, + userId: entry.userid, + warnings: result.warnings, + }, siteId); + } + })); + } + + /** + * Sync a glossary only if a certain time has passed since the last time. + * + * @param glossaryId Glossary ID. + * @param userId User the entry belong to. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the glossary is synced or if it doesn't need to be synced. + */ + async syncGlossaryEntriesIfNeeded( + glossaryId: number, + userId: number, + siteId?: string, + ): Promise { + siteId = siteId || CoreSites.getCurrentSiteId(); + + const syncId = this.getGlossarySyncId(glossaryId, userId); + + const needed = await this.isSyncNeeded(syncId, siteId); + + if (needed) { + return this.syncGlossaryEntries(glossaryId, userId, siteId); + } + } + + /** + * Synchronize all offline entries of a glossary. + * + * @param glossaryId Glossary ID to be synced. + * @param userId User the entries belong to. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved if sync is successful, rejected otherwise. + */ + syncGlossaryEntries(glossaryId: number, userId?: number, siteId?: string): Promise { + userId = userId || CoreSites.getCurrentSiteUserId(); + siteId = siteId || CoreSites.getCurrentSiteId(); + + const syncId = this.getGlossarySyncId(glossaryId, userId); + if (this.isSyncing(syncId, siteId)) { + // There's already a sync ongoing for this glossary, return the promise. + return this.getOngoingSync(syncId, siteId)!; + } + + // Verify that glossary isn't blocked. + if (CoreSync.isBlocked(AddonModGlossaryProvider.COMPONENT, syncId, siteId)) { + this.logger.debug('Cannot sync glossary ' + glossaryId + ' because it is blocked.'); + + throw new CoreSyncBlockedError(Translate.instant('core.errorsyncblocked', { $a: this.componentTranslate })); + } + + this.logger.debug('Try to sync glossary ' + glossaryId + ' for user ' + userId); + + const syncPromise = this.performSyncGlossaryEntries(glossaryId, userId, siteId); + + return this.addOngoingSync(syncId, syncPromise, siteId); + } + + protected async performSyncGlossaryEntries( + glossaryId: number, + userId: number, + siteId: string, + ): Promise { + const result: AddonModGlossarySyncResult = { + warnings: [], + updated: false, + }; + const syncId = this.getGlossarySyncId(glossaryId, userId); + + // Sync offline logs. + await CoreUtils.ignoreErrors(CoreCourseLogHelper.syncActivity(AddonModGlossaryProvider.COMPONENT, glossaryId, siteId)); + + // Get offline responses to be sent. + const entries = await CoreUtils.ignoreErrors( + AddonModGlossaryOffline.getGlossaryNewEntries(glossaryId, siteId, userId), + [], + ); + + if (!entries.length) { + // Nothing to sync. + await CoreUtils.ignoreErrors(this.setSyncTime(syncId, siteId)); + + return result; + } else if (!CoreApp.isOnline()) { + // Cannot sync in offline. + throw new CoreNetworkError(); + } + + let courseId: number | undefined; + + await Promise.all(entries.map(async (data) => { + courseId = courseId || data.courseid; + + try { + // First of all upload the attachments (if any). + const itemId = await this.uploadAttachments(glossaryId, data, siteId); + + // Now try to add the entry. + await AddonModGlossary.addEntryOnline(glossaryId, data.concept, data.definition, data.options, itemId, siteId); + + result.updated = true; + + await this.deleteAddEntry(glossaryId, data.concept, data.timecreated, siteId); + } catch (error) { + if (!CoreUtils.isWebServiceError(error)) { + // Couldn't connect to server, reject. + throw error; + } + + // The WebService has thrown an error, this means that responses cannot be submitted. Delete them. + result.updated = true; + + await this.deleteAddEntry(glossaryId, data.concept, data.timecreated, siteId); + + // Responses deleted, add a warning. + result.warnings.push(Translate.instant('core.warningofflinedatadeleted', { + component: this.componentTranslate, + name: data.concept, + error: CoreTextUtils.getErrorMessageFromError(error), + })); + } + })); + + if (result.updated && courseId) { + // Data has been sent to server. Now invalidate the WS calls. + try { + const glossary = await AddonModGlossary.getGlossaryById(courseId, glossaryId); + + await AddonModGlossary.invalidateGlossaryEntries(glossary, true); + } catch { + // Ignore errors. + } + } + + // Sync finished, set sync time. + await CoreUtils.ignoreErrors(this.setSyncTime(syncId, siteId)); + + return result; + } + + /** + * Synchronize offline ratings. + * + * @param cmId Course module to be synced. If not defined, sync all glossaries. + * @param force Wether to force sync not depending on last execution. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved if sync is successful, rejected otherwise. + */ + async syncRatings(cmId?: number, force?: boolean, siteId?: string): Promise { + siteId = siteId || CoreSites.getCurrentSiteId(); + + const results = await CoreRatingSync.syncRatings('mod_glossary', 'entry', ContextLevel.MODULE, cmId, 0, force, siteId); + + let updated = false; + const warnings: string[] = []; + + await CoreUtils.allPromises(results.map(async (result) => { + if (result.updated.length) { + updated = true; + + // Invalidate entry of updated ratings. + await Promise.all(result.updated.map((itemId) => AddonModGlossary.invalidateEntry(itemId, siteId))); + } + + if (result.warnings.length) { + const glossary = await AddonModGlossary.getGlossary(result.itemSet.courseId, result.itemSet.instanceId, { siteId }); + + result.warnings.forEach((warning) => { + warnings.push(Translate.instant('core.warningofflinedatadeleted', { + component: this.componentTranslate, + name: glossary.name, + error: warning, + })); + }); + } + })); + + return { updated, warnings }; + } + + /** + * Delete a new entry. + * + * @param glossaryId Glossary ID. + * @param concept Glossary entry concept. + * @param timeCreated Time to allow duplicated entries. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when deleted. + */ + protected async deleteAddEntry(glossaryId: number, concept: string, timeCreated: number, siteId?: string): Promise { + await Promise.all([ + AddonModGlossaryOffline.deleteNewEntry(glossaryId, concept, timeCreated, siteId), + AddonModGlossaryHelper.deleteStoredFiles(glossaryId, concept, timeCreated, siteId), + ]); + } + + /** + * Upload attachments of an offline entry. + * + * @param glossaryId Glossary ID. + * @param entry Offline entry. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with draftid if uploaded, resolved with 0 if nothing to upload. + */ + protected async uploadAttachments(glossaryId: number, entry: AddonModGlossaryOfflineEntry, siteId?: string): Promise { + if (!entry.attachments) { + // No attachments. + return 0; + } + + // Has some attachments to sync. + let files: (CoreWSExternalFile | FileEntry)[] = entry.attachments.online || []; + + if (entry.attachments.offline) { + // Has offline files. + const storedFiles = await CoreUtils.ignoreErrors( + AddonModGlossaryHelper.getStoredFiles(glossaryId, entry.concept, entry.timecreated, siteId), + [], // Folder not found, no files to add. + ); + + files = files.concat(storedFiles); + } + + return CoreFileUploader.uploadOrReuploadFiles(files, AddonModGlossaryProvider.COMPONENT, glossaryId, siteId); + } + + /** + * Get the ID of a glossary sync. + * + * @param glossaryId Glossary ID. + * @param userId User the entries belong to.. If not defined, current user. + * @return Sync ID. + */ + protected getGlossarySyncId(glossaryId: number, userId?: number): string { + userId = userId || CoreSites.getCurrentSiteUserId(); + + return 'glossary#' + glossaryId + '#' + userId; + } + +} + +export const AddonModGlossarySync = makeSingleton(AddonModGlossarySyncProvider); + +/** + * Data returned by a glossary sync. + */ +export type AddonModGlossarySyncResult = { + warnings: string[]; // List of warnings. + updated: boolean; // Whether some data was sent to the server or offline data was updated. +}; + +/** + * Data passed to AUTO_SYNCED event. + */ +export type AddonModGlossaryAutoSyncData = { + glossaryId: number; + userId: number; + warnings: string[]; +}; diff --git a/src/addons/mod/glossary/services/glossary.ts b/src/addons/mod/glossary/services/glossary.ts new file mode 100644 index 000000000..b34767f4a --- /dev/null +++ b/src/addons/mod/glossary/services/glossary.ts @@ -0,0 +1,1465 @@ +// (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 { FileEntry } from '@ionic-native/file/ngx'; +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 { CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader'; +import { CoreRatingInfo } from '@features/rating/services/rating'; +import { CoreTagItem } from '@features/tag/services/tag'; +import { CoreApp } from '@services/app'; +import { CoreSites, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@services/sites'; +import { CoreTextUtils } from '@services/utils/text'; +import { CoreUtils } from '@services/utils/utils'; +import { CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws'; +import { makeSingleton, Translate } from '@singletons'; +import { AddonModGlossaryEntryDBRecord, ENTRIES_TABLE_NAME } from './database/glossary'; +import { AddonModGlossaryOffline } from './glossary-offline'; +import { AddonModGlossaryAutoSyncData, AddonModGlossarySyncProvider } from './glossary-sync'; + +const ROOT_CACHE_KEY = 'mmaModGlossary:'; + +/** + * Service that provides some features for glossaries. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModGlossaryProvider { + + static readonly COMPONENT = 'mmaModGlossary'; + static readonly LIMIT_ENTRIES = 25; + static readonly LIMIT_CATEGORIES = 10; + static readonly SHOW_ALL_CATEGORIES = 0; + static readonly SHOW_NOT_CATEGORISED = -1; + + static readonly ADD_ENTRY_EVENT = 'addon_mod_glossary_add_entry'; + + /** + * Get the course glossary cache key. + * + * @param courseId Course Id. + * @return Cache key. + */ + protected getCourseGlossariesCacheKey(courseId: number): string { + return ROOT_CACHE_KEY + 'courseGlossaries:' + courseId; + } + + /** + * Get all the glossaries in a course. + * + * @param courseId Course Id. + * @param options Other options. + * @return Resolved with the glossaries. + */ + async getCourseGlossaries(courseId: number, options: CoreSitesCommonWSOptions = {}): Promise { + const site = await CoreSites.getSite(options.siteId); + + const params: AddonModGlossaryGetGlossariesByCoursesWSParams = { + courseids: [courseId], + }; + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getCourseGlossariesCacheKey(courseId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModGlossaryProvider.COMPONENT, + ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; + + const result = await site.read( + 'mod_glossary_get_glossaries_by_courses', + params, + preSets, + ); + + return result.glossaries; + } + + /** + * Invalidate all glossaries in a course. + * + * @param courseId Course Id. + * @param siteId Site ID. If not defined, current site. + * @return Resolved when data is invalidated. + */ + async invalidateCourseGlossaries(courseId: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + const key = this.getCourseGlossariesCacheKey(courseId); + + await site.invalidateWsCacheForKey(key); + } + + /** + * Get the entries by author cache key. + * + * @param glossaryId Glossary Id. + * @param letter First letter of firstname or lastname, or either keywords: ALL or SPECIAL. + * @param field Search and order using: FIRSTNAME or LASTNAME + * @param sort The direction of the order: ASC or DESC + * @return Cache key. + */ + protected getEntriesByAuthorCacheKey(glossaryId: number, letter: string, field: string, sort: string): string { + return ROOT_CACHE_KEY + 'entriesByAuthor:' + glossaryId + ':' + letter + ':' + field + ':' + sort; + } + + /** + * Get entries by author. + * + * @param glossaryId Glossary Id. + * @param letter First letter of firstname or lastname, or either keywords: ALL or SPECIAL. + * @param field Search and order using: FIRSTNAME or LASTNAME + * @param sort The direction of the order: ASC or DESC + * @param options Other options. + * @return Resolved with the entries. + */ + async getEntriesByAuthor( + glossaryId: number, + letter: string, + field: string, + sort: string, + options: AddonModGlossaryGetEntriesOptions = {}, + ): Promise { + const site = await CoreSites.getSite(options.siteId); + + const params: AddonModGlossaryGetEntriesByAuthorWSParams = { + id: glossaryId, + letter: letter, + field: field, + sort: sort, + from: options.from || 0, + limit: options.limit || AddonModGlossaryProvider.LIMIT_ENTRIES, + }; + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getEntriesByAuthorCacheKey(glossaryId, letter, field, sort), + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + component: AddonModGlossaryProvider.COMPONENT, + componentId: options.cmId, + ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; + + return site.read('mod_glossary_get_entries_by_author', params, preSets); + } + + /** + * Invalidate cache of entries by author. + * + * @param glossaryId Glossary Id. + * @param letter First letter of firstname or lastname, or either keywords: ALL or SPECIAL. + * @param field Search and order using: FIRSTNAME or LASTNAME + * @param sort The direction of the order: ASC or DESC + * @param siteId Site ID. If not defined, current site. + * @return Resolved when data is invalidated. + */ + async invalidateEntriesByAuthor( + glossaryId: number, + letter: string, + field: string, + sort: string, + siteId?: string, + ): Promise { + const site = await CoreSites.getSite(siteId); + + const key = this.getEntriesByAuthorCacheKey(glossaryId, letter, field, sort); + + await site.invalidateWsCacheForKey(key); + } + + /** + * Get entries by category. + * + * @param glossaryId Glossary Id. + * @param categoryId The category ID. Use constant SHOW_ALL_CATEGORIES for all categories, or + * constant SHOW_NOT_CATEGORISED for uncategorised entries. + * @param options Other options. + * @return Resolved with the entries. + */ + async getEntriesByCategory( + glossaryId: number, + categoryId: number, + options: AddonModGlossaryGetEntriesOptions = {}, + ): Promise { + const site = await CoreSites.getSite(options.siteId); + + const params: AddonModGlossaryGetEntriesByCategoryWSParams = { + id: glossaryId, + categoryid: categoryId, + from: options.from || 0, + limit: options.limit || AddonModGlossaryProvider.LIMIT_ENTRIES, + }; + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getEntriesByCategoryCacheKey(glossaryId, categoryId), + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + component: AddonModGlossaryProvider.COMPONENT, + componentId: options.cmId, + ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; + + return site.read('mod_glossary_get_entries_by_category', params, preSets); + } + + /** + * Invalidate cache of entries by category. + * + * @param glossaryId Glossary Id. + * @param categoryId The category ID. Use constant SHOW_ALL_CATEGORIES for all categories, or + * constant SHOW_NOT_CATEGORISED for uncategorised entries. + * @param siteId Site ID. If not defined, current site. + * @return Resolved when data is invalidated. + */ + async invalidateEntriesByCategory(glossaryId: number, categoryId: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + const key = this.getEntriesByCategoryCacheKey(glossaryId, categoryId); + + await site.invalidateWsCacheForKey(key); + } + + /** + * Get the entries by category cache key. + * + * @param glossaryId Glossary Id. + * @param categoryId The category ID. Use constant SHOW_ALL_CATEGORIES for all categories, or + * constant SHOW_NOT_CATEGORISED for uncategorised entries. + * @return Cache key. + */ + getEntriesByCategoryCacheKey(glossaryId: number, categoryId: number): string { + return ROOT_CACHE_KEY + 'entriesByCategory:' + glossaryId + ':' + categoryId; + } + + /** + * Get the entries by date cache key. + * + * @param glossaryId Glossary Id. + * @param order The way to order the records. + * @param sort The direction of the order. + * @return Cache key. + */ + getEntriesByDateCacheKey(glossaryId: number, order: string, sort: string): string { + return ROOT_CACHE_KEY + 'entriesByDate:' + glossaryId + ':' + order + ':' + sort; + } + + /** + * Get entries by date. + * + * @param glossaryId Glossary Id. + * @param order The way to order the records. + * @param sort The direction of the order. + * @param options Other options. + * @return Resolved with the entries. + */ + async getEntriesByDate( + glossaryId: number, + order: string, + sort: string, + options: AddonModGlossaryGetEntriesOptions = {}, + ): Promise { + const site = await CoreSites.getSite(options.siteId); + + const params: AddonModGlossaryGetEntriesByDateWSParams = { + id: glossaryId, + order: order, + sort: sort, + from: options.from || 0, + limit: options.limit || AddonModGlossaryProvider.LIMIT_ENTRIES, + }; + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getEntriesByDateCacheKey(glossaryId, order, sort), + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + component: AddonModGlossaryProvider.COMPONENT, + componentId: options.cmId, + ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; + + return site.read('mod_glossary_get_entries_by_date', params, preSets); + } + + /** + * Invalidate cache of entries by date. + * + * @param glossaryId Glossary Id. + * @param order The way to order the records. + * @param sort The direction of the order. + * @param siteId Site ID. If not defined, current site. + * @return Resolved when data is invalidated. + */ + async invalidateEntriesByDate(glossaryId: number, order: string, sort: string, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + const key = this.getEntriesByDateCacheKey(glossaryId, order, sort); + + await site.invalidateWsCacheForKey(key); + } + + /** + * Get the entries by letter cache key. + * + * @param glossaryId Glossary Id. + * @param letter A letter, or a special keyword. + * @return Cache key. + */ + protected getEntriesByLetterCacheKey(glossaryId: number, letter: string): string { + return ROOT_CACHE_KEY + 'entriesByLetter:' + glossaryId + ':' + letter; + } + + /** + * Get entries by letter. + * + * @param glossaryId Glossary Id. + * @param letter A letter, or a special keyword. + * @param options Other options. + * @return Resolved with the entries. + */ + async getEntriesByLetter( + glossaryId: number, + letter: string, + options: AddonModGlossaryGetEntriesOptions = {}, + ): Promise { + options.from = options.from || 0; + options.limit = options.limit || AddonModGlossaryProvider.LIMIT_ENTRIES; + + const site = await CoreSites.getSite(options.siteId); + + const params: AddonModGlossaryGetEntriesByLetterWSParams = { + id: glossaryId, + letter: letter, + from: options.from, + limit: options.limit, + }; + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getEntriesByLetterCacheKey(glossaryId, letter), + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + component: AddonModGlossaryProvider.COMPONENT, + componentId: options.cmId, + ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; + + const result = await site.read( + 'mod_glossary_get_entries_by_letter', + params, + preSets, + ); + + if (options.limit == AddonModGlossaryProvider.LIMIT_ENTRIES) { + // Store entries in background, don't block the user for this. + CoreUtils.ignoreErrors(this.storeEntries(glossaryId, result.entries, options.from, site.getId())); + } + + return result; + } + + /** + * Invalidate cache of entries by letter. + * + * @param glossaryId Glossary Id. + * @param letter A letter, or a special keyword. + * @param siteId Site ID. If not defined, current site. + * @return Resolved when data is invalidated. + */ + async invalidateEntriesByLetter(glossaryId: number, letter: string, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + const key = this.getEntriesByLetterCacheKey(glossaryId, letter); + + return site.invalidateWsCacheForKey(key); + } + + /** + * Get the entries by search cache key. + * + * @param glossaryId Glossary Id. + * @param query The search query. + * @param fullSearch Whether or not full search is required. + * @param order The way to order the results. + * @param sort The direction of the order. + * @return Cache key. + */ + protected getEntriesBySearchCacheKey( + glossaryId: number, + query: string, + fullSearch: boolean, + order: string, + sort: string, + ): string { + return ROOT_CACHE_KEY + 'entriesBySearch:' + glossaryId + ':' + fullSearch + ':' + order + ':' + sort + ':' + query; + } + + /** + * Get entries by search. + * + * @param glossaryId Glossary Id. + * @param query The search query. + * @param fullSearch Whether or not full search is required. + * @param order The way to order the results. + * @param sort The direction of the order. + * @param from Start returning records from here. + * @param limit Number of records to return. + * @param omitExpires True to always get the value from cache. If data isn't cached, it will call the WS. + * @param forceOffline True to always get the value from cache. If data isn't cached, it won't call the WS. + * @param siteId Site ID. If not defined, current site. + * @return Resolved with the entries. + */ + async getEntriesBySearch( + glossaryId: number, + query: string, + fullSearch: boolean, + order: string, + sort: string, + options: AddonModGlossaryGetEntriesOptions = {}, + ): Promise { + const site = await CoreSites.getSite(options.siteId); + + const params: AddonModGlossaryGetEntriesBySearchWSParams = { + id: glossaryId, + query: query, + fullsearch: fullSearch, + order: order, + sort: sort, + from: options.from || 0, + limit: options.limit || AddonModGlossaryProvider.LIMIT_ENTRIES, + }; + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getEntriesBySearchCacheKey(glossaryId, query, fullSearch, order, sort), + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + component: AddonModGlossaryProvider.COMPONENT, + componentId: options.cmId, + ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; + + return site.read('mod_glossary_get_entries_by_search', params, preSets); + } + + /** + * Invalidate cache of entries by search. + * + * @param glossaryId Glossary Id. + * @param query The search query. + * @param fullSearch Whether or not full search is required. + * @param order The way to order the results. + * @param sort The direction of the order. + * @param siteId Site ID. If not defined, current site. + * @return Resolved when data is invalidated. + */ + async invalidateEntriesBySearch( + glossaryId: number, + query: string, + fullSearch: boolean, + order: string, + sort: string, + siteId?: string, + ): Promise { + const site = await CoreSites.getSite(siteId); + + const key = this.getEntriesBySearchCacheKey(glossaryId, query, fullSearch, order, sort); + + await site.invalidateWsCacheForKey(key); + } + + /** + * Get the glossary categories cache key. + * + * @param glossaryId Glossary Id. + * @return The cache key. + */ + protected getCategoriesCacheKey(glossaryId: number): string { + return ROOT_CACHE_KEY + 'categories:' + glossaryId; + } + + /** + * Get all the categories related to the glossary. + * + * @param glossaryId Glossary Id. + * @param options Other options. + * @return Promise resolved with the categories if supported or empty array if not. + */ + async getAllCategories(glossaryId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + const site = await CoreSites.getSite(options.siteId); + + return this.getCategories(glossaryId, [], site, options); + } + + /** + * Get the categories related to the glossary by sections. It's a recursive function see initial call values. + * + * @param glossaryId Glossary Id. + * @param categories Already fetched categories where to append the fetch. + * @param site Site object. + * @param options Other options. + * @return Promise resolved with the categories. + */ + protected async getCategories( + glossaryId: number, + categories: AddonModGlossaryCategory[], + site: CoreSite, + options: AddonModGlossaryGetCategoriesOptions = {}, + ): Promise { + options.from = options.from || 0; + options.limit = options.limit || AddonModGlossaryProvider.LIMIT_CATEGORIES; + + const params: AddonModGlossaryGetCategoriesWSParams = { + id: glossaryId, + from: options.from, + limit: options.limit, + }; + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getCategoriesCacheKey(glossaryId), + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + component: AddonModGlossaryProvider.COMPONENT, + componentId: options.cmId, + ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; + + const response = await site.read('mod_glossary_get_categories', params, preSets); + + categories = categories.concat(response.categories); + const canLoadMore = (options.from + options.limit) < response.count; + if (canLoadMore) { + options.from += options.limit; + + return this.getCategories(glossaryId, categories, site, options); + } + + return categories; + } + + /** + * Invalidate cache of categories by glossary id. + * + * @param glossaryId Glossary Id. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when categories data has been invalidated, + */ + async invalidateCategories(glossaryId: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + await site.invalidateWsCacheForKey(this.getCategoriesCacheKey(glossaryId)); + } + + /** + * Get an entry by ID cache key. + * + * @param entryId Entry Id. + * @return Cache key. + */ + protected getEntryCacheKey(entryId: number): string { + return ROOT_CACHE_KEY + 'getEntry:' + entryId; + } + + /** + * Get one entry by ID. + * + * @param entryId Entry ID. + * @param options Other options. + * @return Promise resolved with the entry. + */ + async getEntry(entryId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + const site = await CoreSites.getSite(options.siteId); + + const params: AddonModGlossaryGetEntryByIdWSParams = { + id: entryId, + }; + const preSets = { + cacheKey: this.getEntryCacheKey(entryId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModGlossaryProvider.COMPONENT, + componentId: options.cmId, + ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; + + try { + return await site.read('mod_glossary_get_entry_by_id', params, preSets); + } catch (error) { + // Entry not found. Search it in the list of entries. + try { + const data = await this.getStoredDataForEntry(entryId, site.getId()); + + if (typeof data.from != 'undefined') { + const response = await CoreUtils.ignoreErrors( + this.getEntryFromList(data.glossaryId, entryId, data.from, false, options), + ); + + if (response) { + return response; + } + } + + // Page not specified or entry not found in the page, search all pages. + return await this.getEntryFromList(data.glossaryId, entryId, 0, true, options); + } catch { + throw error; + } + } + } + + /** + * Get a glossary ID and the "from" of a given entry. + * + * @param entryId Entry ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with the glossary ID and the "from". + */ + async getStoredDataForEntry(entryId: number, siteId?: string): Promise<{glossaryId: number; from: number}> { + const site = await CoreSites.getSite(siteId); + + const conditions: Partial = { + entryid: entryId, + }; + + const record = await site.getDb().getRecord(ENTRIES_TABLE_NAME, conditions); + + return { + glossaryId: record.glossaryid, + from: record.pagefrom, + }; + } + + /** + * Get an entry from the list of entries. + * + * @param glossaryId Glossary ID. + * @param entryId Entry ID. + * @param from Page to get. + * @param loadNext Whether to load next pages if not found. + * @param options Options. + * @return Promise resolved with the entry data. + */ + protected async getEntryFromList( + glossaryId: number, + entryId: number, + from: number, + loadNext: boolean, + options: CoreCourseCommonModWSOptions = {}, + ): Promise { + // Get the entries from this "page" and check if the entry we're looking for is in it. + const result = await this.getEntriesByLetter(glossaryId, 'ALL', { + from: from, + readingStrategy: CoreSitesReadingStrategy.OnlyCache, + cmId: options.cmId, + siteId: options.siteId, + }); + + const entry = result.entries.find(entry => entry.id == entryId); + + if (entry) { + // Entry found, return it. + return { entry, from }; + } + + const nextFrom = from + result.entries.length; + if (nextFrom < result.count && loadNext) { + // Get the next "page". + return this.getEntryFromList(glossaryId, entryId, nextFrom, true, options); + } + + // No more pages and the entry wasn't found. Reject. + throw new CoreError('Entry not found.'); + }; + + /** + * Performs the whole fetch of the entries using the proper function and arguments. + * + * @param fetchFunction Function to fetch. + * @param fetchArguments Arguments to call the fetching. + * @param options Other options. + * @return Promise resolved with all entrries. + */ + fetchAllEntries( + fetchFunction: (options?: AddonModGlossaryGetEntriesOptions) => AddonModGlossaryGetEntriesWSResponse, + options: CoreCourseCommonModWSOptions = {}, + ): Promise { + options.siteId = options.siteId || CoreSites.getCurrentSiteId(); + + const entries: AddonModGlossaryEntry[] = []; + + const fetchMoreEntries = async (): Promise => { + const result = await fetchFunction({ + from: entries.length, + ...options, // Include all options. + }); + + Array.prototype.push.apply(entries, result.entries); + + return entries.length < result.count ? fetchMoreEntries() : entries; + }; + + return fetchMoreEntries(); + } + + /** + * Invalidate cache of entry by ID. + * + * @param entryId Entry Id. + * @param siteId Site ID. If not defined, current site. + * @return Resolved when data is invalidated. + */ + async invalidateEntry(entryId: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + await site.invalidateWsCacheForKey(this.getEntryCacheKey(entryId)); + } + + /** + * Invalidate cache of all entries in the array. + * + * @param entries Entry objects to invalidate. + * @param siteId Site ID. If not defined, current site. + * @return Resolved when data is invalidated. + */ + protected async invalidateEntries(entries: AddonModGlossaryEntry[], siteId?: string): Promise { + const keys: string[] = []; + entries.forEach((entry) => { + keys.push(this.getEntryCacheKey(entry.id)); + }); + + const site = await CoreSites.getSite(siteId); + + await site.invalidateMultipleWsCacheForKey(keys); + } + + /** + * Invalidate the prefetched content except files. + * To invalidate files, use AddonModGlossary#invalidateFiles. + * + * @param moduleId The module ID. + * @param courseId Course ID. + * @return Promise resolved when data is invalidated. + */ + async invalidateContent(moduleId: number, courseId: number): Promise { + const glossary = await this.getGlossary(courseId, moduleId); + + await CoreUtils.ignoreErrors(this.invalidateGlossaryEntries(glossary)); + + await CoreUtils.allPromises([ + this.invalidateCourseGlossaries(courseId), + this.invalidateCategories(glossary.id), + ]); + } + + /** + * Invalidate the prefetched content for a given glossary, except files. + * To invalidate files, use AddonModGlossaryProvider#invalidateFiles. + * + * @param glossary The glossary object. + * @param onlyEntriesList If true, entries won't be invalidated. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when data is invalidated. + */ + async invalidateGlossaryEntries(glossary: AddonModGlossaryGlossary, onlyEntriesList?: boolean, siteId?: string): Promise { + siteId = siteId || CoreSites.getCurrentSiteId(); + + const promises: Promise[] = []; + + if (!onlyEntriesList) { + promises.push(this.fetchAllEntries(this.getEntriesByLetter.bind(this, glossary.id, 'ALL'), { + cmId: glossary.coursemodule, + readingStrategy: CoreSitesReadingStrategy.PreferCache, + siteId, + }).then((entries) => this.invalidateEntries(entries, siteId))); + } + + glossary.browsemodes.forEach((mode) => { + switch (mode) { + case 'letter': + promises.push(this.invalidateEntriesByLetter(glossary.id, 'ALL', siteId)); + break; + case 'cat': + promises.push(this.invalidateEntriesByCategory( + glossary.id, + AddonModGlossaryProvider.SHOW_ALL_CATEGORIES, + siteId, + )); + break; + case 'date': + promises.push(this.invalidateEntriesByDate(glossary.id, 'CREATION', 'DESC', siteId)); + promises.push(this.invalidateEntriesByDate(glossary.id, 'UPDATE', 'DESC', siteId)); + break; + case 'author': + promises.push(this.invalidateEntriesByAuthor(glossary.id, 'ALL', 'LASTNAME', 'ASC', siteId)); + break; + default: + } + }); + + await CoreUtils.allPromises(promises); + } + + /** + * Get one glossary by cmid. + * + * @param courseId Course Id. + * @param cmId Course Module Id. + * @param options Other options. + * @return Promise resolved with the glossary. + */ + async getGlossary(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise { + const glossaries = await this.getCourseGlossaries(courseId, options); + + const glossary = glossaries.find((glossary) => glossary.coursemodule == cmId); + + if (glossary) { + return glossary; + } + + throw new CoreError('Glossary not found.'); + } + + /** + * Get one glossary by glossary ID. + * + * @param courseId Course Id. + * @param glossaryId Glossary Id. + * @param options Other options. + * @return Promise resolved with the glossary. + */ + async getGlossaryById( + courseId: number, + glossaryId: number, + options: CoreSitesCommonWSOptions = {}, + ): Promise { + const glossaries = await this.getCourseGlossaries(courseId, options); + + const glossary = glossaries.find((glossary) => glossary.id == glossaryId); + + if (glossary) { + return glossary; + } + + throw new CoreError('Glossary not found.'); + } + + /** + * Create a new entry on a glossary + * + * @param glossaryId Glossary ID. + * @param concept Glossary entry concept. + * @param definition Glossary entry concept definition. + * @param courseId Course ID of the glossary. + * @param entryOptions Options for the entry. + * @param attachments Attachments ID if sending online, result of CoreFileUploaderProvider#storeFilesToUpload otherwise. + * @param otherOptions Other options. + * @return Promise resolved with entry ID if entry was created in server, false if stored in device. + */ + async addEntry( + glossaryId: number, + concept: string, + definition: string, + courseId: number, + entryOptions: Record, + attachments?: number | CoreFileUploaderStoreFilesResult, + otherOptions: AddonModGlossaryAddEntryOptions = {}, + ): Promise { + otherOptions.siteId = otherOptions.siteId || CoreSites.getCurrentSiteId(); + + // Convenience function to store a new entry to be synchronized later. + const storeOffline = async (): Promise => { + const discardTime = otherOptions.discardEntry?.timecreated; + + if (otherOptions.checkDuplicates) { + // Check if the entry is duplicated in online or offline mode. + const conceptUsed = await this.isConceptUsed(glossaryId, concept, { + cmId: otherOptions.cmId, + timeCreated: discardTime, + siteId: otherOptions.siteId, + }); + + if (conceptUsed) { + throw new CoreError(Translate.instant('addon.mod_glossary.errconceptalreadyexists')); + } + } + + if (typeof attachments == 'number') { + // When storing in offline the attachments can't be a draft ID. + throw new CoreError('Error adding entry.'); + } + + await AddonModGlossaryOffline.addNewEntry( + glossaryId, + concept, + definition, + courseId, + entryOptions, + attachments, + otherOptions.timeCreated, + otherOptions.siteId, + undefined, + otherOptions.discardEntry, + ); + + return false; + }; + + if (!CoreApp.isOnline() && otherOptions.allowOffline) { + // App is offline, store the action. + return storeOffline(); + } + + // If we are editing an offline entry, discard previous first. + if (otherOptions.discardEntry) { + await AddonModGlossaryOffline.deleteNewEntry( + glossaryId, + otherOptions.discardEntry.concept, + otherOptions.discardEntry.timecreated, + otherOptions.siteId, + ); + } + + try { + // Try to add it in online. + return this.addEntryOnline(glossaryId, concept, definition, entryOptions, attachments, otherOptions.siteId); + } catch (error) { + if (otherOptions.allowOffline && !CoreUtils.isWebServiceError(error)) { + // Couldn't connect to server, store in offline. + return storeOffline(); + } + + // The WebService has thrown an error or offline not supported, reject. + throw error; + } + } + + /** + * Create a new entry on a glossary. It does not cache calls. It will fail if offline or cannot connect. + * + * @param glossaryId Glossary ID. + * @param concept Glossary entry concept. + * @param definition Glossary entry concept definition. + * @param options Options for the entry. + * @param attachId Attachments ID (if any attachment). + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with the entry ID if created, rejected otherwise. + */ + async addEntryOnline( + glossaryId: number, + concept: string, + definition: string, + options?: Record, + attachId?: number, + siteId?: string, + ): Promise { + const site = await CoreSites.getSite(siteId); + + const params: AddonModGlossaryAddEntryWSParams = { + glossaryid: glossaryId, + concept: concept, + definition: definition, + definitionformat: 1, + options: CoreUtils.objectToArrayOfObjects(options || {}, 'name', 'value'), + }; + + if (attachId) { + params.options!.push({ + name: 'attachmentsid', + value: String(attachId), + }); + } + + // Workaround for bug MDL-57737. + if (!site.isVersionGreaterEqualThan('3.2.2')) { + params.definition = CoreTextUtils.cleanTags(params.definition); + } + + const response = await site.write('mod_glossary_add_entry', params); + + return response.entryid; + } + + /** + * Check if a entry concept is already used. + * + * @param glossaryId Glossary ID. + * @param concept Concept to check. + * @param options Other options. + * @return Promise resolved with true if used, resolved with false if not used or error. + */ + async isConceptUsed(glossaryId: number, concept: string, options: AddonModGlossaryIsConceptUsedOptions = {}): Promise { + try { + // Check offline first. + const exists = await AddonModGlossaryOffline.isConceptUsed(glossaryId, concept, options.timeCreated, options.siteId); + + if (exists) { + return true; + } + + // If we get here, there's no offline entry with this name, check online. + // Get entries from the cache. + const entries = await this.fetchAllEntries(this.getEntriesByLetter.bind(glossaryId, 'ALL'), { + cmId: options.cmId, + readingStrategy: CoreSitesReadingStrategy.PreferCache, + siteId: options.siteId, + }); + + // Check if there's any entry with the same concept. + return entries.some((entry) => entry.concept == concept); + } catch { + // Error, assume not used. + return false; + } + } + + /** + * Return whether or not the plugin is enabled for editing in the current site. Plugin is enabled if the glossary WS are + * available. + * + * @return Whether the glossary editing is available or not. + */ + isPluginEnabledForEditing(): boolean { + return !!CoreSites.getCurrentSite()?.wsAvailable('mod_glossary_add_entry'); + } + + /** + * Report a glossary as being viewed. + * + * @param glossaryId Glossary ID. + * @param mode The mode in which the glossary was viewed. + * @param name Name of the glossary. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the WS call is successful. + */ + logView(glossaryId: number, mode: string, name?: string, siteId?: string): Promise { + const params: AddonModGlossaryViewGlossaryWSParams = { + id: glossaryId, + mode: mode, + }; + + return CoreCourseLogHelper.logSingle( + 'mod_glossary_view_glossary', + params, + AddonModGlossaryProvider.COMPONENT, + glossaryId, + name, + 'glossary', + { mode }, + siteId, + ); + } + + /** + * Report a glossary entry as being viewed. + * + * @param entryId Entry ID. + * @param glossaryId Glossary ID. + * @param name Name of the glossary. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the WS call is successful. + */ + logEntryView(entryId: number, glossaryId: number, name?: string, siteId?: string): Promise { + const params: AddonModGlossaryViewEntryWSParams = { + id: entryId, + }; + + return CoreCourseLogHelper.logSingle( + 'mod_glossary_view_entry', + params, + AddonModGlossaryProvider.COMPONENT, + glossaryId, + name, + 'glossary', + { entryid: entryId }, + siteId, + ); + } + + /** + * Store several entries so we can determine their glossaryId in offline. + * + * @param glossaryId Glossary ID the entries belongs to. + * @param entries Entries. + * @param from The "page" the entries belong to. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when done. + */ + protected async storeEntries( + glossaryId: number, + entries: AddonModGlossaryEntry[], + from: number, + siteId?: string, + ): Promise { + await Promise.all(entries.map((entry) => this.storeEntryId(glossaryId, entry.id, from, siteId))); + } + + /** + * Store an entry so we can determine its glossaryId in offline. + * + * @param glossaryId Glossary ID the entry belongs to. + * @param entryId Entry ID. + * @param from The "page" the entry belongs to. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when done. + */ + protected async storeEntryId(glossaryId: number, entryId: number, from: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + const entry: AddonModGlossaryEntryDBRecord = { + entryid: entryId, + glossaryid: glossaryId, + pagefrom: from, + }; + + await site.getDb().insertRecord(ENTRIES_TABLE_NAME, entry); + } + +} + +export const AddonModGlossary = makeSingleton(AddonModGlossaryProvider); + +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 { + [AddonModGlossaryProvider.ADD_ENTRY_EVENT]: AddonModGlossaryAddEntryEventData; + [AddonModGlossarySyncProvider.AUTO_SYNCED]: AddonModGlossaryAutoSyncData; + } + +} + +/** + * Data passed to ADD_ENTRY_EVENT. + */ +export type AddonModGlossaryAddEntryEventData = { + glossaryId: number; + entryId?: number; +}; + +/** + * Params of mod_glossary_get_glossaries_by_courses WS. + */ +export type AddonModGlossaryGetGlossariesByCoursesWSParams = { + courseids?: number[]; // Array of course IDs. +}; + +/** + * Data returned by mod_glossary_get_glossaries_by_courses WS. + */ +export type AddonModGlossaryGetGlossariesByCoursesWSResponse = { + glossaries: AddonModGlossaryGlossary[]; + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Data returned by mod_glossary_get_glossaries_by_courses WS. + */ +export type AddonModGlossaryGlossary = { + id: number; // Glossary id. + coursemodule: number; // Course module id. + course: number; // Course id. + name: string; // Glossary name. + intro: string; // The Glossary intro. + introformat: number; // Intro format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). + introfiles?: CoreWSExternalFile[]; + allowduplicatedentries: number; // If enabled, multiple entries can have the same concept name. + displayformat: string; // Display format type. + mainglossary: number; // If enabled this glossary is a main glossary. + showspecial: number; // If enabled, participants can browse the glossary by special characters, such as @ and #. + showalphabet: number; // If enabled, participants can browse the glossary by letters of the alphabet. + showall: number; // If enabled, participants can browse all entries at once. + allowcomments: number; // If enabled, all participants with permission will be able to add comments to glossary entries. + allowprintview: number; // If enabled, students are provided with a link to a printer-friendly version of the glossary. + usedynalink: number; // If enabled, the entry will be automatically linked. + defaultapproval: number; // If set to no, entries require approving by a teacher before they are viewable by everyone. + approvaldisplayformat: string; // When approving glossary items you may wish to use a different display format. + globalglossary: number; + entbypage: number; // Entries shown per page. + editalways: number; // Always allow editing. + rsstype: number; // RSS type. + rssarticles: number; // This setting specifies the number of glossary entry concepts to include in the RSS feed. + assessed: number; // Aggregate type. + assesstimestart: number; // Restrict rating to items created after this. + assesstimefinish: number; // Restrict rating to items created before this. + scale: number; // Scale ID. + timecreated: number; // Time created. + timemodified: number; // Time modified. + completionentries: number; // Number of entries to complete. + section: number; // Section. + visible: number; // Visible. + groupmode: number; // Group mode. + groupingid: number; // Grouping ID. + browsemodes: string[]; + canaddentry?: number; // Whether the user can add a new entry. +}; + +/** + * Common data passed to the get entries WebServices. + */ +export type AddonModGlossaryCommonGetEntriesWSParams = { + id: number; // Glossary entry ID. + from?: number; // Start returning records from here. + limit?: number; // Number of records to return. + options?: { + // When false, includes the non-approved entries created by the user. + // When true, also includes the ones that the user has the permission to approve. + includenotapproved?: boolean; + }; // An array of options. +}; + +/** + * Data returned by the different get entries WebServices. + */ +export type AddonModGlossaryGetEntriesWSResponse = { + count: number; // The total number of records matching the request. + entries: AddonModGlossaryEntry[]; + ratinginfo?: CoreRatingInfo; // Rating information. + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Params of mod_glossary_get_entries_by_author WS. + */ +export type AddonModGlossaryGetEntriesByAuthorWSParams = AddonModGlossaryCommonGetEntriesWSParams & { + letter: string; // First letter of firstname or lastname, or either keywords: 'ALL' or 'SPECIAL'. + field?: string; // Search and order using: 'FIRSTNAME' or 'LASTNAME'. + sort?: string; // The direction of the order: 'ASC' or 'DESC'. +}; + +/** + * Params of mod_glossary_get_entries_by_category WS. + */ +export type AddonModGlossaryGetEntriesByCategoryWSParams = AddonModGlossaryCommonGetEntriesWSParams & { + categoryid: number; // The category ID. Use '0' for all categories, or '-1' for uncategorised entries. +}; + +/** + * Data returned by mod_glossary_get_entries_by_category WS. + */ +export type AddonModGlossaryGetEntriesByCategoryWSResponse = Omit & { + entries: AddonModGlossaryEntryWithCategory[]; +}; + +/** + * Params of mod_glossary_get_entries_by_date WS. + */ +export type AddonModGlossaryGetEntriesByDateWSParams = AddonModGlossaryCommonGetEntriesWSParams & { + order?: string; // Order the records by: 'CREATION' or 'UPDATE'. + sort?: string; // The direction of the order: 'ASC' or 'DESC'. +}; + +/** + * Params of mod_glossary_get_entries_by_letter WS. + */ +export type AddonModGlossaryGetEntriesByLetterWSParams = AddonModGlossaryCommonGetEntriesWSParams & { + letter: string; // A letter, or either keywords: 'ALL' or 'SPECIAL'. +}; + +/** + * Params of mod_glossary_get_entries_by_search WS. + */ +export type AddonModGlossaryGetEntriesBySearchWSParams = AddonModGlossaryCommonGetEntriesWSParams & { + query: string; // The query string. + fullsearch?: boolean; // The query. + order?: string; // Order by: 'CONCEPT', 'CREATION' or 'UPDATE'. + sort?: string; // The direction of the order: 'ASC' or 'DESC'. +}; + +/** + * Entry data returned by several WS. + */ +export type AddonModGlossaryEntry = { + id: number; // The entry ID. + glossaryid: number; // The glossary ID. + userid: number; // Author ID. + userfullname: string; // Author full name. + userpictureurl: string; // Author picture. + concept: string; // The concept. + definition: string; // The definition. + definitionformat: number; // Definition format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). + definitiontrust: boolean; // The definition trust flag. + definitioninlinefiles?: CoreWSExternalFile[]; + attachment: boolean; // Whether or not the entry has attachments. + attachments?: CoreWSExternalFile[]; + timecreated: number; // Time created. + timemodified: number; // Time modified. + teacherentry: boolean; // The entry was created by a teacher, or equivalent. + sourceglossaryid: number; // The source glossary ID. + usedynalink: boolean; // Whether the concept should be automatically linked. + casesensitive: boolean; // When true, the matching is case sensitive. + fullmatch: boolean; // When true, the matching is done on full words only. + approved: boolean; // Whether the entry was approved. + tags?: CoreTagItem[]; +}; + +/** + * Entry data returned by several WS. + */ +export type AddonModGlossaryEntryWithCategory = AddonModGlossaryEntry & { + categoryid?: number; // The category ID. This may be '-1' when the entry is not categorised. + categoryname?: string; // The category name. May be empty when the entry is not categorised. +}; + +/** + * Params of mod_glossary_get_categories WS. + */ +export type AddonModGlossaryGetCategoriesWSParams = { + id: number; // The glossary ID. + from?: number; // Start returning records from here. + limit?: number; // Number of records to return. +}; + +/** + * Data returned by mod_glossary_get_categories WS. + */ +export type AddonModGlossaryGetCategoriesWSResponse = { + count: number; // The total number of records. + categories: AddonModGlossaryCategory[]; + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Data returned by mod_glossary_get_categories WS. + */ +export type AddonModGlossaryCategory = { + id: number; // The category ID. + glossaryid: number; // The glossary ID. + name: string; // The name of the category. + usedynalink: boolean; // Whether the category is automatically linked. +}; + +/** + * Params of mod_glossary_get_entry_by_id WS. + */ +export type AddonModGlossaryGetEntryByIdWSParams = { + id: number; // Glossary entry ID. +}; + +/** + * Data returned by mod_glossary_get_entry_by_id WS. + */ +export type AddonModGlossaryGetEntryByIdWSResponse = { + entry: AddonModGlossaryEntry; + ratinginfo?: CoreRatingInfo; // Rating information. + permissions?: { + candelete: boolean; // Whether the user can delete the entry. + canupdate: boolean; // Whether the user can update the entry. + }; // User permissions for the managing the entry. + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Data returned by mod_glossary_get_entry_by_id WS, with some calculated data if needed. + */ +export type AddonModGlossaryGetEntryByIdResponse = AddonModGlossaryGetEntryByIdWSResponse & { + from?: number; +}; + +/** + * Params of mod_glossary_add_entry WS. + */ +export type AddonModGlossaryAddEntryWSParams = { + glossaryid: number; // Glossary id. + concept: string; // Glossary concept. + definition: string; // Glossary concept definition. + definitionformat: number; // Definition format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). + options?: { // Optional settings. + name: string; // The allowed keys (value format) are: + // inlineattachmentsid (int); the draft file area id for inline attachments + // attachmentsid (int); the draft file area id for attachments + // categories (comma separated int); comma separated category ids + // aliases (comma separated str); comma separated aliases + // usedynalink (bool); whether the entry should be automatically linked. + // casesensitive (bool); whether the entry is case sensitive. + // fullmatch (bool); whether to match whole words only. + value: string | number; // The value of the option (validated inside the function). + }[]; +}; + +/** + * Data returned by mod_glossary_add_entry WS. + */ +export type AddonModGlossaryAddEntryWSResponse = { + entryid: number; // New glossary entry ID. + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Params of mod_glossary_view_glossary WS. + */ +export type AddonModGlossaryViewGlossaryWSParams = { + id: number; // Glossary instance ID. + mode: string; // The mode in which the glossary is viewed. +}; + +/** + * Params of mod_glossary_view_entry WS. + */ +export type AddonModGlossaryViewEntryWSParams = { + id: number; // Glossary entry ID. +}; + +/** + * Options to pass to add entry. + */ +export type AddonModGlossaryAddEntryOptions = { + timeCreated?: number; // The time the entry was created. If not defined, current time. + discardEntry?: AddonModGlossaryDiscardedEntry; // The entry provided will be discarded if found. + allowOffline?: boolean; // True if it can be stored in offline, false otherwise. + checkDuplicates?: boolean; // Check for duplicates before storing offline. Only used if allowOffline is true. + cmId?: number; // Module ID. + siteId?: string; // Site ID. If not defined, current site. +}; + +/** + * Entry to discard. + */ +export type AddonModGlossaryDiscardedEntry = { + concept: string; + timecreated: number; +}; + +/** + * Entry to be added. + */ +export type AddonModGlossaryNewEntry = { + concept: string; + definition: string; + timecreated: number; +}; + +/** + * Entry to be added, including attachments. + */ +export type AddonModGlossaryNewEntryWithFiles = AddonModGlossaryNewEntry & { + files: (CoreWSExternalFile | FileEntry)[]; +}; + +/** + * Options to pass to the different get entries functions. + */ +export type AddonModGlossaryGetEntriesOptions = CoreCourseCommonModWSOptions & { + from?: number; // Start returning records from here. Defaults to 0. + limit?: number; // Number of records to return. Defaults to AddonModGlossaryProvider.LIMIT_ENTRIES. +}; + +/** + * Options to pass to get categories. + */ +export type AddonModGlossaryGetCategoriesOptions = CoreCourseCommonModWSOptions & { + from?: number; // Start returning records from here. Defaults to 0. + limit?: number; // Number of records to return. Defaults to AddonModGlossaryProvider.LIMIT_CATEGORIES. +}; + +/** + * Options to pass to is concept used. + */ +export type AddonModGlossaryIsConceptUsedOptions = { + cmId?: number; // Module ID. + timeCreated?: number; // Timecreated to check that is not the timecreated we are editing. + siteId?: string; // Site ID. If not defined, current site. +}; + +/** + * Possible values for entry options. + */ +export type AddonModGlossaryEntryOption = string | number; diff --git a/src/addons/mod/glossary/services/handlers/edit-link.ts b/src/addons/mod/glossary/services/handlers/edit-link.ts new file mode 100644 index 000000000..089bfaf36 --- /dev/null +++ b/src/addons/mod/glossary/services/handlers/edit-link.ts @@ -0,0 +1,78 @@ +// (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 { CoreContentLinksHandlerBase } from '@features/contentlinks/classes/base-handler'; +import { CoreContentLinksAction } from '@features/contentlinks/services/contentlinks-delegate'; +import { CoreCourse } from '@features/course/services/course'; +import { CoreNavigator } from '@services/navigator'; +import { CoreDomUtils } from '@services/utils/dom'; +import { makeSingleton } from '@singletons'; +import { AddonModGlossaryModuleHandlerService } from './module'; + +/** + * Content links handler for glossary new entry. + * Match mod/glossary/edit.php?cmid=6 with a valid data. + * Currently it only supports new entry. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModGlossaryEditLinkHandlerService extends CoreContentLinksHandlerBase { + + name = 'AddonModGlossaryEditLinkHandler'; + featureName = 'CoreCourseModuleDelegate_AddonModGlossary'; + pattern = /\/mod\/glossary\/edit\.php.*([?&](cmid)=\d+)/; + + /** + * @inheritdoc + */ + getActions(siteIds: string[], url: string, params: Record): CoreContentLinksAction[] { + return [{ + action: async (siteId: string) => { + const modal = await CoreDomUtils.showModalLoading(); + + const cmId = Number(params.cmid); + + try { + const module = await CoreCourse.getModuleBasicInfo(cmId, siteId); + + await CoreNavigator.navigateToSitePath( + AddonModGlossaryModuleHandlerService.PAGE_NAME + '/edit/0', + { + params: { + cmId: module.id, + courseId: module.course, + }, + siteId, + }, + ); + } catch (error) { + CoreDomUtils.showErrorModalDefault(error, 'addon.mod_glossary.errorloadingglossary', true); + } finally { + // Just in case. In fact we need to dismiss the modal before showing a toast or error message. + modal.dismiss(); + } + }, + }]; + } + + /** + * @inheritdoc + */ + async isEnabled(siteId: string, url: string, params: Record): Promise { + return typeof params.cmid != 'undefined'; + } + +} + +export const AddonModGlossaryEditLinkHandler = makeSingleton(AddonModGlossaryEditLinkHandlerService); diff --git a/src/addons/mod/glossary/services/handlers/entry-link.ts b/src/addons/mod/glossary/services/handlers/entry-link.ts new file mode 100644 index 000000000..7ae25bb04 --- /dev/null +++ b/src/addons/mod/glossary/services/handlers/entry-link.ts @@ -0,0 +1,75 @@ +// (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 { CoreContentLinksHandlerBase } from '@features/contentlinks/classes/base-handler'; +import { CoreContentLinksAction } from '@features/contentlinks/services/contentlinks-delegate'; +import { CoreCourse } from '@features/course/services/course'; +import { CoreNavigator } from '@services/navigator'; +import { CoreDomUtils } from '@services/utils/dom'; +import { makeSingleton } from '@singletons'; +import { AddonModGlossary } from '../glossary'; +import { AddonModGlossaryModuleHandlerService } from './module'; + +/** + * Handler to treat links to glossary entries. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModGlossaryEntryLinkHandlerService extends CoreContentLinksHandlerBase { + + name = 'AddonModGlossaryEntryLinkHandler'; + featureName = 'CoreCourseModuleDelegate_AddonModGlossary'; + pattern = /\/mod\/glossary\/(showentry|view)\.php.*([&?](eid|g|mode|hook)=\d+)/; + + /** + * @inheritdoc + */ + getActions(siteIds: string[], url: string, params: Record): CoreContentLinksAction[] { + return [{ + action: async (siteId: string) => { + const modal = await CoreDomUtils.showModalLoading(); + + try { + const entryId = params.mode == 'entry' ? Number(params.hook) : Number(params.eid); + + const response = await AddonModGlossary.getEntry(entryId, { siteId }); + + const module = await CoreCourse.getModuleBasicInfoByInstance( + response.entry.glossaryid, + 'glossary', + siteId, + ); + + await CoreNavigator.navigateToSitePath( + AddonModGlossaryModuleHandlerService.PAGE_NAME + `/entry/${entryId}`, + { + params: { + cmId: module.id, + courseId: module.course, + }, + siteId, + }, + ); + } catch (error) { + CoreDomUtils.showErrorModalDefault(error, 'addon.mod_glossary.errorloadingentry', true); + } finally { + modal.dismiss(); + } + }, + }]; + } + +} + +export const AddonModGlossaryEntryLinkHandler = makeSingleton(AddonModGlossaryEntryLinkHandlerService); diff --git a/src/addons/mod/glossary/services/handlers/index-link.ts b/src/addons/mod/glossary/services/handlers/index-link.ts new file mode 100644 index 000000000..3658be016 --- /dev/null +++ b/src/addons/mod/glossary/services/handlers/index-link.ts @@ -0,0 +1,33 @@ +// (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 { CoreContentLinksModuleIndexHandler } from '@features/contentlinks/classes/module-index-handler'; +import { makeSingleton } from '@singletons'; + +/** + * Handler to treat links to glossary index. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModGlossaryIndexLinkHandlerService extends CoreContentLinksModuleIndexHandler { + + name = 'AddonModGlossaryIndexLinkHandler'; + + constructor() { + super('AddonModGlossary', 'glossary', 'g'); + } + +} + +export const AddonModGlossaryIndexLinkHandler = makeSingleton(AddonModGlossaryIndexLinkHandlerService); diff --git a/src/addons/mod/glossary/services/handlers/list-link.ts b/src/addons/mod/glossary/services/handlers/list-link.ts new file mode 100644 index 000000000..1386e0589 --- /dev/null +++ b/src/addons/mod/glossary/services/handlers/list-link.ts @@ -0,0 +1,33 @@ +// (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 { CoreContentLinksModuleListHandler } from '@features/contentlinks/classes/module-list-handler'; +import { makeSingleton } from '@singletons'; + +/** + * Handler to treat links to glossary list page. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModGlossaryListLinkHandlerService extends CoreContentLinksModuleListHandler { + + name = 'AddonModGlossaryListLinkHandler'; + + constructor() { + super('AddonModGlossary', 'glossary'); + } + +} + +export const AddonModGlossaryListLinkHandler = makeSingleton(AddonModGlossaryListLinkHandlerService); diff --git a/src/addons/mod/glossary/services/handlers/module.ts b/src/addons/mod/glossary/services/handlers/module.ts new file mode 100644 index 000000000..f49bfef4e --- /dev/null +++ b/src/addons/mod/glossary/services/handlers/module.ts @@ -0,0 +1,92 @@ +// (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 { CoreConstants } from '@/core/constants'; +import { Injectable, Type } from '@angular/core'; +import { CoreCourse, CoreCourseAnyModuleData } from '@features/course/services/course'; +import { CoreCourseModule } from '@features/course/services/course-helper'; +import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate'; +import { CoreNavigationOptions, CoreNavigator } from '@services/navigator'; +import { makeSingleton } from '@singletons'; +import { AddonModGlossaryIndexComponent } from '../../components/index/index'; + +/** + * Handler to support glossary modules. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModGlossaryModuleHandlerService implements CoreCourseModuleHandler { + + static readonly PAGE_NAME = 'mod_glossary'; + + name = 'AddonModGlossary'; + modName = 'glossary'; + + supportedFeatures = { + [CoreConstants.FEATURE_GROUPS]: false, + [CoreConstants.FEATURE_GROUPINGS]: false, + [CoreConstants.FEATURE_MOD_INTRO]: true, + [CoreConstants.FEATURE_COMPLETION_TRACKS_VIEWS]: true, + [CoreConstants.FEATURE_COMPLETION_HAS_RULES]: true, + [CoreConstants.FEATURE_GRADE_HAS_GRADE]: true, + [CoreConstants.FEATURE_GRADE_OUTCOMES]: true, + [CoreConstants.FEATURE_BACKUP_MOODLE2]: true, + [CoreConstants.FEATURE_SHOW_DESCRIPTION]: true, + [CoreConstants.FEATURE_RATE]: true, + [CoreConstants.FEATURE_PLAGIARISM]: true, + }; + + /** + * @inheritdoc + */ + async isEnabled(): Promise { + return true; + } + + /** + * @inheritdoc + */ + getData(module: CoreCourseAnyModuleData): CoreCourseModuleHandlerData { + return { + icon: CoreCourse.getModuleIconSrc(this.modName, 'modicon' in module ? module.modicon : undefined), + title: module.name, + class: 'addon-mod_glossary-handler', + showDownloadButton: true, + action: (event: Event, module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions) => { + options = options || {}; + options.params = options.params || {}; + Object.assign(options.params, { module }); + const routeParams = '/' + courseId + '/' + module.id; + + CoreNavigator.navigateToSitePath(AddonModGlossaryModuleHandlerService.PAGE_NAME + routeParams, options); + }, + }; + } + + /** + * @inheritdoc + */ + async getMainComponent(): Promise> { + return AddonModGlossaryIndexComponent; + } + + /** + * @inheritdoc + */ + displayRefresherInSingleActivity(): boolean { + return false; + } + +} + +export const AddonModGlossaryModuleHandler = makeSingleton(AddonModGlossaryModuleHandlerService); diff --git a/src/addons/mod/glossary/services/handlers/prefetch.ts b/src/addons/mod/glossary/services/handlers/prefetch.ts new file mode 100644 index 000000000..cc92048a0 --- /dev/null +++ b/src/addons/mod/glossary/services/handlers/prefetch.ts @@ -0,0 +1,235 @@ +// (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 { CoreComments } from '@features/comments/services/comments'; +import { CoreCourseActivityPrefetchHandlerBase } from '@features/course/classes/activity-prefetch-handler'; +import { CoreCourse, CoreCourseAnyModuleData } from '@features/course/services/course'; +import { CoreUser } from '@features/user/services/user'; +import { CoreFilepool } from '@services/filepool'; +import { CoreSites, CoreSitesReadingStrategy } from '@services/sites'; +import { CoreWSExternalFile } from '@services/ws'; +import { makeSingleton } from '@singletons'; +import { AddonModGlossary, AddonModGlossaryEntry, AddonModGlossaryGlossary, AddonModGlossaryProvider } from '../glossary'; +import { AddonModGlossarySync, AddonModGlossarySyncResult } from '../glossary-sync'; + +/** + * Handler to prefetch forums. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModGlossaryPrefetchHandlerService extends CoreCourseActivityPrefetchHandlerBase { + + name = 'AddonModGlossary'; + modName = 'glossary'; + component = AddonModGlossaryProvider.COMPONENT; + updatesNames = /^configuration$|^.*files$|^entries$/; + + /** + * @inheritdoc + */ + async getFiles(module: CoreCourseAnyModuleData, courseId: number): Promise { + try { + const glossary = await AddonModGlossary.getGlossary(courseId, module.id); + + const entries = await AddonModGlossary.fetchAllEntries( + AddonModGlossary.getEntriesByLetter.bind(AddonModGlossary.instance, glossary.id, 'ALL'), + { + cmId: module.id, + }, + ); + + return this.getFilesFromGlossaryAndEntries(module, glossary, entries); + } catch { + // Glossary not found, return empty list. + return []; + } + } + + /** + * Get the list of downloadable files. It includes entry embedded files. + * + * @param module Module to get the files. + * @param glossary Glossary + * @param entries Entries of the Glossary. + * @return List of Files. + */ + protected getFilesFromGlossaryAndEntries( + module: CoreCourseAnyModuleData, + glossary: AddonModGlossaryGlossary, + entries: AddonModGlossaryEntry[], + ): CoreWSExternalFile[] { + let files = this.getIntroFilesFromInstance(module, glossary); + + const getInlineFiles = CoreSites.getCurrentSite()?.isVersionGreaterEqualThan('3.2'); + + // Get entries files. + entries.forEach((entry) => { + files = files.concat(entry.attachments || []); + + if (getInlineFiles && entry.definitioninlinefiles && entry.definitioninlinefiles.length) { + files = files.concat(entry.definitioninlinefiles); + } else if (entry.definition && !getInlineFiles) { + files = files.concat(CoreFilepool.extractDownloadableFilesFromHtmlAsFakeFileObjects(entry.definition)); + } + }); + + return files; + } + + /** + * @inheritdoc + */ + invalidateContent(moduleId: number, courseId: number): Promise { + return AddonModGlossary.invalidateContent(moduleId, courseId); + } + + /** + * @inheritdoc + */ + prefetch(module: CoreCourseAnyModuleData, courseId?: number): Promise { + return this.prefetchPackage(module, courseId, this.prefetchGlossary.bind(this, module, courseId)); + } + + /** + * Prefetch a glossary. + * + * @param module The module object returned by WS. + * @param courseId Course ID the module belongs to. + * @param siteId Site ID. + * @return Promise resolved when done. + */ + protected async prefetchGlossary(module: CoreCourseAnyModuleData, courseId: number, siteId: string): Promise { + siteId = siteId || CoreSites.getCurrentSiteId(); + + const options = { + cmId: module.id, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }; + + // Prefetch the glossary data. + const glossary = await AddonModGlossary.getGlossary(courseId, module.id, { siteId }); + + const promises: Promise[] = []; + + glossary.browsemodes.forEach((mode) => { + switch (mode) { + case 'letter': // Always done. Look bellow. + break; + case 'cat': + promises.push(AddonModGlossary.fetchAllEntries( + AddonModGlossary.getEntriesByCategory.bind( + AddonModGlossary.instance, + glossary.id, + AddonModGlossaryProvider.SHOW_ALL_CATEGORIES, + ), + options, + )); + break; + case 'date': + promises.push(AddonModGlossary.fetchAllEntries( + AddonModGlossary.getEntriesByDate.bind( + AddonModGlossary.instance, + glossary.id, + 'CREATION', + 'DESC', + ), + options, + )); + promises.push(AddonModGlossary.fetchAllEntries( + AddonModGlossary.getEntriesByDate.bind( + AddonModGlossary.instance, + glossary.id, + 'UPDATE', + 'DESC', + ), + options, + )); + break; + case 'author': + promises.push(AddonModGlossary.fetchAllEntries( + AddonModGlossary.getEntriesByAuthor.bind( + AddonModGlossary.instance, + glossary.id, + 'ALL', + 'LASTNAME', + 'ASC', + ), + options, + )); + break; + default: + } + }); + + // Fetch all entries to get information from. + promises.push(AddonModGlossary.fetchAllEntries( + AddonModGlossary.getEntriesByLetter.bind(AddonModGlossary.instance, glossary.id, 'ALL'), + options, + ).then((entries) => { + const promises: Promise[] = []; + const commentsEnabled = !CoreComments.areCommentsDisabledInSite(); + + entries.forEach((entry) => { + // Don't fetch individual entries, it's too many WS calls. + if (glossary.allowcomments && commentsEnabled) { + promises.push(CoreComments.getComments( + 'module', + glossary.coursemodule, + 'mod_glossary', + entry.id, + 'glossary_entry', + 0, + siteId, + )); + } + }); + + const files = this.getFilesFromGlossaryAndEntries(module, glossary, entries); + promises.push(CoreFilepool.addFilesToQueue(siteId, files, this.component, module.id)); + + // Prefetch user avatars. + promises.push(CoreUser.prefetchUserAvatars(entries, 'userpictureurl', siteId)); + + return Promise.all(promises); + })); + + // Get all categories. + promises.push(AddonModGlossary.getAllCategories(glossary.id, options)); + + // Prefetch data for link handlers. + promises.push(CoreCourse.getModuleBasicInfo(module.id, siteId)); + promises.push(CoreCourse.getModuleBasicInfoByInstance(glossary.id, 'glossary', siteId)); + + await Promise.all(promises); + } + + /** + * @inheritdoc + */ + async sync(module: CoreCourseAnyModuleData, courseId: number, siteId?: string): Promise { + const results = await Promise.all([ + AddonModGlossarySync.syncGlossaryEntries(module.instance!, undefined, siteId), + AddonModGlossarySync.syncRatings(module.id, undefined, siteId), + ]); + + return { + updated: results[0].updated || results[1].updated, + warnings: results[0].warnings.concat(results[1].warnings), + }; + } + +} + +export const AddonModGlossaryPrefetchHandler = makeSingleton(AddonModGlossaryPrefetchHandlerService); diff --git a/src/addons/mod/glossary/services/handlers/sync-cron.ts b/src/addons/mod/glossary/services/handlers/sync-cron.ts new file mode 100644 index 000000000..236d67c71 --- /dev/null +++ b/src/addons/mod/glossary/services/handlers/sync-cron.ts @@ -0,0 +1,44 @@ +// (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 { CoreCronHandler } from '@services/cron'; +import { makeSingleton } from '@singletons'; +import { AddonModGlossarySync } from '../glossary-sync'; + +/** + * Synchronization cron handler. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModGlossarySyncCronHandlerService implements CoreCronHandler { + + name = 'AddonModGlossarySyncCronHandler'; + + /** + * @inheritdoc + */ + execute(siteId?: string, force?: boolean): Promise { + return AddonModGlossarySync.syncAllGlossaries(siteId, force); + } + + /** + * @inheritdoc + */ + getInterval(): number { + return AddonModGlossarySync.syncInterval; + } + +} + +export const AddonModGlossarySyncCronHandler = makeSingleton(AddonModGlossarySyncCronHandlerService); diff --git a/src/addons/mod/glossary/services/handlers/tag-area.ts b/src/addons/mod/glossary/services/handlers/tag-area.ts new file mode 100644 index 000000000..7d844e276 --- /dev/null +++ b/src/addons/mod/glossary/services/handlers/tag-area.ts @@ -0,0 +1,53 @@ +// (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, Type } from '@angular/core'; +import { CoreTagFeedComponent } from '@features/tag/components/feed/feed'; +import { CoreTagAreaHandler } from '@features/tag/services/tag-area-delegate'; +import { CoreTagFeedElement, CoreTagHelper } from '@features/tag/services/tag-helper'; +import { makeSingleton } from '@singletons'; + +/** + * Handler to support tags. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModGlossaryTagAreaHandlerService implements CoreTagAreaHandler { + + name = 'AddonModGlossaryTagAreaHandler'; + type = 'mod_glossary/glossary_entries'; + + /** + * @inheritdoc + */ + async isEnabled(): Promise { + return true; + } + + /** + * @inheritdoc + */ + parseContent(content: string): CoreTagFeedElement[] { + return CoreTagHelper.parseFeedContent(content); + } + + /** + * @inheritdoc + */ + getComponent(): Type { + return CoreTagFeedComponent; + } + +} + +export const AddonModGlossaryTagAreaHandler = makeSingleton(AddonModGlossaryTagAreaHandlerService); diff --git a/src/addons/mod/mod.module.ts b/src/addons/mod/mod.module.ts index 5aa0ad1bb..4ce3025a6 100644 --- a/src/addons/mod/mod.module.ts +++ b/src/addons/mod/mod.module.ts @@ -32,6 +32,7 @@ import { AddonModSurveyModule } from './survey/survey.module'; import { AddonModScormModule } from './scorm/scorm.module'; import { AddonModChoiceModule } from './choice/choice.module'; import { AddonModWikiModule } from './wiki/wiki.module'; +import { AddonModGlossaryModule } from './glossary/glossary.module'; @NgModule({ imports: [ @@ -53,6 +54,7 @@ import { AddonModWikiModule } from './wiki/wiki.module'; AddonModScormModule, AddonModChoiceModule, AddonModWikiModule, + AddonModGlossaryModule, ], }) export class AddonModModule { } diff --git a/src/core/features/compile/services/compile.ts b/src/core/features/compile/services/compile.ts index cad7df44a..80e733aca 100644 --- a/src/core/features/compile/services/compile.ts +++ b/src/core/features/compile/services/compile.ts @@ -130,7 +130,7 @@ import { ADDON_MOD_DATA_SERVICES } from '@addons/mod/data/data.module'; // @todo import { ADDON_MOD_FEEDBACK_SERVICES } from '@addons/mod/feedback/feedback.module'; import { ADDON_MOD_FOLDER_SERVICES } from '@addons/mod/folder/folder.module'; import { ADDON_MOD_FORUM_SERVICES } from '@addons/mod/forum/forum.module'; -// @todo import { ADDON_MOD_GLOSSARY_SERVICES } from '@addons/mod/glossary/glossary.module'; +import { ADDON_MOD_GLOSSARY_SERVICES } from '@addons/mod/glossary/glossary.module'; import { ADDON_MOD_H5P_ACTIVITY_SERVICES } from '@addons/mod/h5pactivity/h5pactivity.module'; import { ADDON_MOD_IMSCP_SERVICES } from '@addons/mod/imscp/imscp.module'; import { ADDON_MOD_LESSON_SERVICES } from '@addons/mod/lesson/lesson.module'; @@ -296,7 +296,7 @@ export class CoreCompileProvider { // @todo ...ADDON_MOD_FEEDBACK_SERVICES, ...ADDON_MOD_FOLDER_SERVICES, ...ADDON_MOD_FORUM_SERVICES, - // @todo ...ADDON_MOD_GLOSSARY_SERVICES, + ...ADDON_MOD_GLOSSARY_SERVICES, ...ADDON_MOD_H5P_ACTIVITY_SERVICES, ...ADDON_MOD_IMSCP_SERVICES, ...ADDON_MOD_LESSON_SERVICES,