From 5106e11e29e8ddf684c996a675b1d5081f8a568a Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Fri, 25 May 2018 15:35:03 +0200 Subject: [PATCH 1/7] MOBILE-2342 glossary: Implement glossary and offline providers --- src/addon/mod/glossary/glossary.module.ts | 30 + src/addon/mod/glossary/lang/en.json | 29 + src/addon/mod/glossary/providers/glossary.ts | 886 +++++++++++++++++++ src/addon/mod/glossary/providers/offline.ts | 280 ++++++ src/app/app.module.ts | 2 + 5 files changed, 1227 insertions(+) create mode 100644 src/addon/mod/glossary/glossary.module.ts create mode 100644 src/addon/mod/glossary/lang/en.json create mode 100644 src/addon/mod/glossary/providers/glossary.ts create mode 100644 src/addon/mod/glossary/providers/offline.ts diff --git a/src/addon/mod/glossary/glossary.module.ts b/src/addon/mod/glossary/glossary.module.ts new file mode 100644 index 000000000..0c12b9b7e --- /dev/null +++ b/src/addon/mod/glossary/glossary.module.ts @@ -0,0 +1,30 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { NgModule } from '@angular/core'; +import { AddonModGlossaryProvider } from './providers/glossary'; +import { AddonModGlossaryOfflineProvider } from './providers/offline'; + +@NgModule({ + declarations: [ + ], + imports: [ + ], + providers: [ + AddonModGlossaryProvider, + AddonModGlossaryOfflineProvider, + ] +}) +export class AddonModGlossaryModule { +} diff --git a/src/addon/mod/glossary/lang/en.json b/src/addon/mod/glossary/lang/en.json new file mode 100644 index 000000000..28a0078f5 --- /dev/null +++ b/src/addon/mod/glossary/lang/en.json @@ -0,0 +1,29 @@ +{ + "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", + "noentriesfound": "No entries were found.", + "searchquery": "Search query" +} diff --git a/src/addon/mod/glossary/providers/glossary.ts b/src/addon/mod/glossary/providers/glossary.ts new file mode 100644 index 000000000..02482f0e9 --- /dev/null +++ b/src/addon/mod/glossary/providers/glossary.ts @@ -0,0 +1,886 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { TranslateService } from '@ngx-translate/core'; +import { CoreSite } from '@classes/site'; +import { CoreAppProvider } from '@providers/app'; +import { CoreFilepoolProvider } from '@providers/filepool'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { AddonModGlossaryOfflineProvider } from './offline'; + +/** + * Service that provides some features for glossaries. + */ +@Injectable() +export class AddonModGlossaryProvider { + static COMPONENT = 'mmaModGlossary'; + static LIMIT_ENTRIES = 25; + static LIMIT_CATEGORIES = 10; + static SHOW_ALL_CATERGORIES = 0; + static SHOW_NOT_CATEGORISED = -1; + + static ADD_ENTRY_EVENT = 'addon_mod_glossary_add_entry'; + + protected ROOT_CACHE_KEY = 'mmaModGlossary:'; + + constructor(private appProvider: CoreAppProvider, + private sitesProvider: CoreSitesProvider, + private filepoolProvider: CoreFilepoolProvider, + private translate: TranslateService, + private textUtils: CoreTextUtilsProvider, + private utils: CoreUtilsProvider, + private glossaryOffline: AddonModGlossaryOfflineProvider) {} + + /** + * Get the course glossary cache key. + * + * @param {number} courseId Course Id. + * @return {string} Cache key. + */ + protected getCourseGlossariesCacheKey(courseId: number): string { + return this.ROOT_CACHE_KEY + 'courseGlossaries:' + courseId; + } + + /** + * Get all the glossaries in a course. + * + * @param {number} courseId Course Id. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Resolved with the glossaries. + */ + getCourseGlossaries(courseId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + courseids: [courseId] + }; + const preSets = { + cacheKey: this.getCourseGlossariesCacheKey(courseId) + }; + + return site.read('mod_glossary_get_glossaries_by_courses', params, preSets).then((result) => { + return result.glossaries; + }); + }); + } + + /** + * Invalidate all glossaries in a course. + * + * @param {number} courseId Course Id. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Resolved when data is invalidated. + */ + invalidateCourseGlossaries(courseId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const key = this.getCourseGlossariesCacheKey(courseId); + + return site.invalidateWsCacheForKey(key); + }); + } + + /** + * Get the entries by author cache key. + * + * @param {number} glossaryId Glossary Id. + * @param {string} letter First letter of firstname or lastname, or either keywords: ALL or SPECIAL. + * @param {string} field Search and order using: FIRSTNAME or LASTNAME + * @param {string} sort The direction of the order: ASC or DESC + * @return {string} Cache key. + */ + protected getEntriesByAuthorCacheKey(glossaryId: number, letter: string, field: string, sort: string): string { + return this.ROOT_CACHE_KEY + 'entriesByAuthor:' + glossaryId + ':' + letter + ':' + field + ':' + sort; + } + + /** + * Get entries by author. + * + * @param {number} glossaryId Glossary Id. + * @param {string} letter First letter of firstname or lastname, or either keywords: ALL or SPECIAL. + * @param {string} field Search and order using: FIRSTNAME or LASTNAME + * @param {string} sort The direction of the order: ASC or DESC + * @param {number} from Start returning records from here. + * @param {number} limit Number of records to return. + * @param {boolean} forceCache True to always get the value from cache, false otherwise. Default false. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Resolved with the entries. + */ + getEntriesByAuthor(glossaryId: number, letter: string, field: string, sort: string, from: number, limit: number, + forceCache: boolean, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + id: glossaryId, + letter: letter, + field: field, + sort: sort, + from: from, + limit: limit + }; + const preSets = { + cacheKey: this.getEntriesByAuthorCacheKey(glossaryId, letter, field, sort), + omitExpires: forceCache + }; + + return site.read('mod_glossary_get_entries_by_author', params, preSets); + }); + } + + /** + * Invalidate cache of entries by author. + * + * @param {number} glossaryId Glossary Id. + * @param {string} letter First letter of firstname or lastname, or either keywords: ALL or SPECIAL. + * @param {string} field Search and order using: FIRSTNAME or LASTNAME + * @param {string} sort The direction of the order: ASC or DESC + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Resolved when data is invalidated. + */ + invalidateEntriesByAuthor(glossaryId: number, letter: string, field: string, sort: string, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const key = this.getEntriesByAuthorCacheKey(glossaryId, letter, field, sort); + + return site.invalidateWsCacheForKey(key); + }); + } + + /** + * Get entries by category. + * + * @param {number} glossaryId Glossary Id. + * @param {string} categoryId The category ID. Use constant SHOW_ALL_CATERGORIES for all categories, or + * constant SHOW_NOT_CATEGORISED for uncategorised entries. + * @param {number} from Start returning records from here. + * @param {number} limit Number of records to return. + * @param {boolean} forceCache True to always get the value from cache, false otherwise. Default false. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Resolved with the entries. + */ + getEntriesByCategory(glossaryId: number, categoryId: number, from: number, limit: number, forceCache: boolean, + siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + id: glossaryId, + categoryid: categoryId, + from: from, + limit: limit + }; + const preSets = { + cacheKey: this.getEntriesByCategoryCacheKey(glossaryId, categoryId), + omitExpires: forceCache + }; + + return site.read('mod_glossary_get_entries_by_category', params, preSets); + }); + } + + /** + * Invalidate cache of entries by category. + * + * @param {number} glossaryId Glossary Id. + * @param {string} categoryId The category ID. Use constant SHOW_ALL_CATERGORIES for all categories, or + * constant SHOW_NOT_CATEGORISED for uncategorised entries. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Resolved when data is invalidated. + */ + invalidateEntriesByCategory(glossaryId: number, categoryId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const key = this.getEntriesByCategoryCacheKey(glossaryId, categoryId); + + return site.invalidateWsCacheForKey(key); + }); + } + + /** + * Get the entries by category cache key. + * + * @param {number} glossaryId Glossary Id. + * @param {string} categoryId The category ID. Use constant SHOW_ALL_CATERGORIES for all categories, or + * constant SHOW_NOT_CATEGORISED for uncategorised entries. + * @return {string} Cache key. + */ + getEntriesByCategoryCacheKey(glossaryId: number, categoryId: number): string { + return this.ROOT_CACHE_KEY + 'entriesByCategory:' + glossaryId + ':' + categoryId; + } + + /** + * Get the entries by date cache key. + * + * @param {number} glossaryId Glossary Id. + * @param {string} order The way to order the records. + * @param {string} sort The direction of the order. + * @return {string} Cache key. + */ + getEntriesByDateCacheKey(glossaryId: number, order: string, sort: string): string { + return this.ROOT_CACHE_KEY + 'entriesByDate:' + glossaryId + ':' + order + ':' + sort; + } + + /** + * Get entries by date. + * + * @param {number} glossaryId Glossary Id. + * @param {string} order The way to order the records. + * @param {string} sort The direction of the order. + * @param {number} from Start returning records from here. + * @param {number} limit Number of records to return. + * @param {boolean} forceCache True to always get the value from cache, false otherwise. Default false. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Resolved with the entries. + */ + getEntriesByDate(glossaryId: number, order: string, sort: string, from: number, limit: number, forceCache: boolean, + siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + id: glossaryId, + order: order, + sort: sort, + from: from, + limit: limit + }; + const preSets = { + cacheKey: this.getEntriesByDateCacheKey(glossaryId, order, sort), + omitExpires: forceCache + }; + + return site.read('mod_glossary_get_entries_by_date', params, preSets); + }); + } + + /** + * Invalidate cache of entries by date. + * + * @param {number} glossaryId Glossary Id. + * @param {string} order The way to order the records. + * @param {string} sort The direction of the order. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Resolved when data is invalidated. + */ + invalidateEntriesByDate(glossaryId: number, order: string, sort: string, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const key = this.getEntriesByDateCacheKey(glossaryId, order, sort); + + return site.invalidateWsCacheForKey(key); + }); + } + + /** + * Get the entries by letter cache key. + * + * @param {number} glossaryId Glossary Id. + * @param {string} letter A letter, or a special keyword. + * @return {string} Cache key. + */ + protected getEntriesByLetterCacheKey(glossaryId: number, letter: string): string { + return this.ROOT_CACHE_KEY + 'entriesByLetter:' + glossaryId + ':' + letter; + } + + /** + * Get entries by letter. + * + * @param {number} glossaryId Glossary Id. + * @param {string} letter A letter, or a special keyword. + * @param {number} from Start returning records from here. + * @param {number} limit Number of records to return. + * @param {boolean} forceCache True to always get the value from cache, false otherwise. Default false. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Resolved with the entries. + */ + getEntriesByLetter(glossaryId: number, letter: string, from: number, limit: number, forceCache: boolean, siteId?: string): + Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + id: glossaryId, + letter: letter, + from: from, + limit: limit + }; + const preSets = { + cacheKey: this.getEntriesByLetterCacheKey(glossaryId, letter), + omitExpires: forceCache + }; + + return site.read('mod_glossary_get_entries_by_letter', params, preSets); + }); + } + + /** + * Invalidate cache of entries by letter. + * + * @param {number} glossaryId Glossary Id. + * @param {string} letter A letter, or a special keyword. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Resolved when data is invalidated. + */ + invalidateEntriesByLetter(glossaryId: number, letter: string, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const key = this.getEntriesByLetterCacheKey(glossaryId, letter); + + return site.invalidateWsCacheForKey(key); + }); + } + + /** + * Get the entries by search cache key. + * + * @param {number} glossaryId Glossary Id. + * @param {string} query The search query. + * @param {boolean} fullSearch Whether or not full search is required. + * @param {string} order The way to order the results. + * @param {string} sort The direction of the order. + * @return {string} Cache key. + */ + protected getEntriesBySearchCacheKey(glossaryId: number, query: string, fullSearch: boolean, order: string, sort: string): + string { + return this.ROOT_CACHE_KEY + 'entriesBySearch:' + glossaryId + ':' + fullSearch + ':' + order + ':' + sort + ':' + query; + } + + /** + * Get entries by search. + * + * @param {number} glossaryId Glossary Id. + * @param {string} query The search query. + * @param {boolean} fullSearch Whether or not full search is required. + * @param {string} order The way to order the results. + * @param {string} sort The direction of the order. + * @param {number} from Start returning records from here. + * @param {number} limit Number of records to return. + * @param {boolean} forceCache True to always get the value from cache, false otherwise. Default false. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Resolved with the entries. + */ + getEntriesBySearch(glossaryId: number, query: string, fullSearch: boolean, order: string, sort: string, from: number, + limit: number, forceCache: boolean, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + id: glossaryId, + query: query, + fullsearch: fullSearch, + order: order, + sort: sort, + from: from, + limit: limit + }; + const preSets = { + cacheKey: this.getEntriesBySearchCacheKey(glossaryId, query, fullSearch, order, sort), + omitExpires: forceCache, + }; + + return site.read('mod_glossary_get_entries_by_search', params, preSets); + }); + } + + /** + * Invalidate cache of entries by search. + * + * @param {number} glossaryId Glossary Id. + * @param {string} query The search query. + * @param {boolean} fullSearch Whether or not full search is required. + * @param {string} order The way to order the results. + * @param {string} sort The direction of the order. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Resolved when data is invalidated. + */ + invalidateEntriesBySearch(glossaryId: number, query: string, fullSearch: boolean, order: string, sort: string, siteId?: string): + Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const key = this.getEntriesBySearchCacheKey(glossaryId, query, fullSearch, order, sort); + + return site.invalidateWsCacheForKey(key); + }); + } + + /** + * Get the glossary categories cache key. + * + * @param {number} glossaryId Glossary Id. + * @return {string} The cache key. + */ + protected getCategoriesCacheKey(glossaryId: number): string { + return this.ROOT_CACHE_KEY + 'categories:' + glossaryId; + } + + /** + * Get all the categories related to the glossary. + * + * @param {number} glossaryId Glossary Id. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the categories if supported or empty array if not. + */ + getAllCategories(glossaryId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return this.getCategories(glossaryId, 0, AddonModGlossaryProvider.LIMIT_CATEGORIES, [], site); + }); + } + + /** + * Get the categories related to the glossary by sections. It's a recursive function see initial call values. + * + * @param {number} glossaryId Glossary Id. + * @param {number} from Number of categories already fetched, so fetch will be done from this number. Initial value 0. + * @param {number} limit Number of categories to fetch. Initial value LIMIT_CATEGORIES. + * @param {any[]} categories Already fetched categories where to append the fetch. Initial value []. + * @param {any} site Site object. + * @return {Promise} Promise resolved with the categories. + */ + protected getCategories(glossaryId: number, from: number, limit: number, categories: any[], site: CoreSite): Promise { + const params = { + id: glossaryId, + from: from, + limit: limit + }; + const preSets = { + cacheKey: this.getCategoriesCacheKey(glossaryId) + }; + + return site.read('mod_glossary_get_categories', params, preSets).then((response) => { + categories = categories.concat(response.categories); + const canLoadMore = (from + limit) < response.count; + if (canLoadMore) { + from += limit; + + return this.getCategories(glossaryId, from, limit, categories, site); + } + + return categories; + }); + } + + /** + * Invalidate cache of categories by glossary id. + * + * @param {number} glossaryId Glossary Id. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when categories data has been invalidated, + */ + invalidateCategories(glossaryId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKey(this.getCategoriesCacheKey(glossaryId)); + }); + } + + /** + * Get an entry by ID cache key. + * + * @param {number} entryId Entry Id. + * @return {string} Cache key. + */ + protected getEntryCacheKey(entryId: number): string { + return this.ROOT_CACHE_KEY + 'getEntry:' + entryId; + } + + /** + * Get one entry by ID. + * + * @param {number} entryId Entry ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the entry. + */ + getEntry(entryId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + id: entryId + }; + const preSets = { + cacheKey: this.getEntryCacheKey(entryId) + }; + + return site.read('mod_glossary_get_entry_by_id', params, preSets).then((response) => { + if (response && response.entry) { + return response.entry; + } else { + return Promise.reject(null); + } + }); + }); + } + + /** + * Performs the fetch of the entries using the propper function and arguments. + * + * @param {Function} fetchFunction Function to fetch. + * @param {any[]} fetchArguments Arguments to call the fetching. + * @param {number} [limitFrom=0] Number of entries already fetched, so fetch will be done from this number. + * @param {number} [limitNum] Number of records to return. Defaults to LIMIT_ENTRIES. + * @param {boolean} [forceCache=false] True to always get the value from cache, false otherwise. Default false. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the response. + */ + fetchEntries(fetchFunction: Function, fetchArguments: any[], limitFrom: number = 0, limitNum?: number, + forceCache: boolean = false, siteId?: string): Promise { + limitNum = limitNum || AddonModGlossaryProvider.LIMIT_ENTRIES; + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + const args = fetchArguments.slice(); + args.push(limitFrom); + args.push(limitNum); + args.push(forceCache); + args.push(siteId); + + return fetchFunction.apply(this, args); + } + + /** + * Performs the whole fetch of the entries using the propper function and arguments. + * + * @param {Function} fetchFunction Function to fetch. + * @param {any[]} fetchArguments Arguments to call the fetching. + * @param {boolean} [forceCache=false] True to always get the value from cache, false otherwise. Default false. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with all entrries. + */ + fetchAllEntries(fetchFunction: Function, fetchArguments: any[], forceCache: boolean = false, siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + const entries = []; + const limitNum = AddonModGlossaryProvider.LIMIT_ENTRIES; + + const fetchMoreEntries = (): Promise => { + return this.fetchEntries(fetchFunction, fetchArguments, entries.length, limitNum, forceCache, siteId).then((result) => { + Array.prototype.push.apply(entries, result.entries); + + return entries.length < result.count ? fetchMoreEntries() : entries; + }); + }; + + return fetchMoreEntries(); + } + + /** + * Invalidate cache of entry by ID. + * + * @param {number} entryId Entry Id. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Resolved when data is invalidated. + */ + invalidateEntry(entryId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKey(this.getEntryCacheKey(entryId)); + }); + } + + /** + * Invalidate cache of all entries in the array. + * + * @param {any[]} entries Entry objects to invalidate. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Resolved when data is invalidated. + */ + protected invalidateEntries(entries: any[], siteId?: string): Promise { + const keys = []; + entries.forEach((entry) => { + keys.push(this.getEntryCacheKey(entry.id)); + }); + + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateMultipleWsCacheForKey(keys); + }); + } + + /** + * Invalidate the prefetched content except files. + * To invalidate files, use AddonModGlossary#invalidateFiles. + * + * @param {number} moduleId The module ID. + * @param {number} courseId Course ID. + * @return {Promise} Promise resolved when data is invalidated. + */ + invalidateContent(moduleId: number, courseId: number): Promise { + return this.getGlossary(courseId, moduleId).then((glossary) => { + return this.invalidateGlossaryEntries(glossary).finally(() => { + return this.utils.allPromises([ + this.invalidateCourseGlossaries(courseId), + this.invalidateCategories(glossary.id) + ]); + }); + }); + } + + /** + * Invalidate the prefetched content for a given glossary, except files. + * To invalidate files, use $mmaModGlossary#invalidateFiles. + * + * @param {any} glossary The glossary object. + * @param {boolean} [onlyEntriesList] If true, entries won't be invalidated. + * @return {Promise} Promise resolved when data is invalidated. + */ + invalidateGlossaryEntries(glossary: any, onlyEntriesList?: boolean): Promise { + const promises = []; + + if (!onlyEntriesList) { + promises.push(this.fetchAllEntries(this.getEntriesByLetter, [glossary.id, 'ALL'], true).then((entries) => { + return this.invalidateEntries(entries); + })); + } + + glossary.browsemodes.forEach((mode) => { + switch (mode) { + case 'letter': + promises.push(this.invalidateEntriesByLetter(glossary.id, 'ALL')); + break; + case 'cat': + promises.push(this.invalidateEntriesByCategory(glossary.id, AddonModGlossaryProvider.SHOW_ALL_CATERGORIES)); + break; + case 'date': + promises.push(this.invalidateEntriesByDate(glossary.id, 'CREATION', 'DESC')); + promises.push(this.invalidateEntriesByDate(glossary.id, 'UPDATE', 'DESC')); + break; + case 'author': + promises.push(this.invalidateEntriesByAuthor(glossary.id, 'ALL', 'LASTNAME', 'ASC')); + break; + default: + } + }); + + return this.utils.allPromises(promises); + } + + /** + * Invalidate the prefetched files. + * + * @param {number} moduleId The module ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the files are invalidated. + */ + protected invalidateFiles(moduleId: number, siteId?: string): Promise { + return this.filepoolProvider.invalidateFilesByComponent(siteId, AddonModGlossaryProvider.COMPONENT, moduleId); + } + + /** + * Get one glossary by cmid. + * + * @param {number} courseId Course Id. + * @param {number} cmId Course Module Id. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the glossary. + */ + getGlossary(courseId: number, cmId: number, siteId?: string): Promise { + return this.getCourseGlossaries(courseId, siteId).then((glossaries) => { + const glossary = glossaries.find((glossary) => glossary.coursemodule == cmId); + + if (glossary) { + return glossary; + } + + return Promise.reject(null); + }); + } + + /** + * Get one glossary by glossary ID. + * + * @param {number} courseId Course Id. + * @param {number} glossaryId Glossary Id. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the glossary. + */ + getGlossaryById(courseId: number, glossaryId: number, siteId?: string): Promise { + return this.getCourseGlossaries(courseId, siteId).then((glossaries) => { + const glossary = glossaries.find((glossary) => glossary.id == glossaryId); + + if (glossary) { + return glossary; + } + + return Promise.reject(null); + }); + } + + /** + * Create a new entry on a glossary + * + * @param {number} glossaryId Glossary ID. + * @param {string} concept Glossary entry concept. + * @param {string} definition Glossary entry concept definition. + * @param {number} courseId Course ID of the glossary. + * @param {any} [options] Array of options for the entry. + * @param {any} [attach] Attachments ID if sending online, result of $mmFileUploader#storeFilesToUpload otherwise. + * @param {number} [timeCreated] The time the entry was created. If not defined, current time. + * @param {string} [siteId] Site ID. If not defined, current site. + * @param {any} [discardEntry] The entry provided will be discarded if found. + * @param {boolean} [allowOffline] True if it can be stored in offline, false otherwise. + * @param {boolean} [checkDuplicates] Check for duplicates before storing offline. Only used if allowOffline is true. + * @return {Promise} Promise resolved with entry ID if entry was created in server, false if stored in device. + */ + addEntry(glossaryId: number, concept: string, definition: string, courseId: number, options: any, attach: any, + timeCreated: number, siteId?: string, discardEntry?: any, allowOffline?: boolean, checkDuplicates?: boolean): + Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + // Convenience function to store a new entry to be synchronized later. + const storeOffline = (): Promise => { + const discardTime = discardEntry && discardEntry.timecreated; + + let duplicatesPromise; + if (checkDuplicates) { + duplicatesPromise = this.isConceptUsed(glossaryId, concept, discardTime, siteId); + } else { + duplicatesPromise = Promise.resolve(false); + } + + // Check if the entry is duplicated in online or offline mode. + return duplicatesPromise.then((used) => { + if (used) { + return Promise.reject(this.translate.instant('addon.mod_glossary.errconceptalreadyexists')); + } + + return this.glossaryOffline.addNewEntry(glossaryId, concept, definition, courseId, attach, options, timeCreated, + siteId, undefined, discardEntry).then(() => { + return false; + }); + }); + }; + + if (!this.appProvider.isOnline() && allowOffline) { + // App is offline, store the action. + return storeOffline(); + } + + // If we are editing an offline entry, discard previous first. + let discardPromise; + if (discardEntry) { + discardPromise = this.glossaryOffline.deleteNewEntry( + glossaryId, discardEntry.concept, discardEntry.timecreated, siteId); + } else { + discardPromise = Promise.resolve(); + } + + return discardPromise.then(() => { + // Try to add it in online. + return this.addEntryOnline(glossaryId, concept, definition, options, attach, siteId).then((entryId) => { + return entryId; + }).catch((error) => { + if (allowOffline && !this.utils.isWebServiceError(error)) { + // Couldn't connect to server, store in offline. + return storeOffline(); + } else { + // The WebService has thrown an error or offline not supported, reject. + return Promise.reject(error); + } + }); + }); + } + + /** + * Create a new entry on a glossary. It does not cache calls. It will fail if offline or cannot connect. + * + * @param {number} glossaryId Glossary ID. + * @param {string} concept Glossary entry concept. + * @param {string} definition Glossary entry concept definition. + * @param {any} [options] Array of options for the entry. + * @param {number} [attachId] Attachments ID (if any attachment). + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the entry ID if created, rejected otherwise. + */ + addEntryOnline(glossaryId: number, concept: string, definition: string, options?: any, attachId?: number, siteId?: string): + Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + glossaryid: glossaryId, + concept: concept, + definition: definition, + definitionformat: 1, + options: this.utils.objectToArrayOfObjects(options || {}, 'name', 'value') + }; + + if (attachId) { + params.options.push({ + name: 'attachmentsid', + value: attachId + }); + } + + // Workaround for bug MDL-57737. + if (!site.isVersionGreaterEqualThan('3.2.2')) { + params.definition = this.textUtils.cleanTags(params.definition); + } + + return site.write('mod_glossary_add_entry', params).then((response) => { + if (response && response.entryid) { + return response.entryid; + } + + return this.utils.createFakeWSError(''); + }); + }); + } + + /** + * Check if a entry concept is already used. + * + * @param {number} glossaryId Glossary ID. + * @param {string} concept Concept to check. + * @param {number} [timeCreated] Timecreated to check that is not the timecreated we are editing. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with true if used, resolved with false if not used or error. + */ + isConceptUsed(glossaryId: number, concept: string, timeCreated?: number, siteId?: string): Promise { + // Check offline first. + return this.glossaryOffline.isConceptUsed(glossaryId, concept, timeCreated, siteId).then((exists) => { + if (exists) { + return true; + } + + // If we get here, there's no offline entry with this name, check online. + // Get entries from the cache. + return this.fetchAllEntries(this.getEntriesByLetter, [glossaryId, 'ALL'], true, siteId).then((entries) => { + // 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 {boolean} Whether the glossary editing is available or not. + */ + isPluginEnabledForEditing(): boolean { + return this.sitesProvider.getCurrentSite().wsAvailable('mod_glossary_add_entry'); + } + + /** + * Report a glossary as being viewed. + * + * @param {number} glossaryId Glossary ID. + * @param {string} mode The mode in which the glossary was viewed. + * @return {Promise} Promise resolved when the WS call is successful. + */ + logView(glossaryId: number, mode: string): Promise { + const params = { + id: glossaryId, + mode: mode + }; + + return this.sitesProvider.getCurrentSite().write('mod_glossary_view_glossary', params); + } + + /** + * Report a glossary entry as being viewed. + * + * @param {number} entryId Entry ID. + * @return {Promise} Promise resolved when the WS call is successful. + */ + logEntryView(entryId: number): Promise { + const params = { + id: entryId + }; + + return this.sitesProvider.getCurrentSite().write('mod_glossary_view_entry', params); + } +} diff --git a/src/addon/mod/glossary/providers/offline.ts b/src/addon/mod/glossary/providers/offline.ts new file mode 100644 index 000000000..d33af7131 --- /dev/null +++ b/src/addon/mod/glossary/providers/offline.ts @@ -0,0 +1,280 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { CoreFileProvider } from '@providers/file'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; + +/** + * Service to handle offline glossary. + */ +@Injectable() +export class AddonModGlossaryOfflineProvider { + + // Variables for database. + protected ENTRIES_TABLE = 'addon_mod_glossary_entrues'; + + protected tablesSchema = [ + { + name: this.ENTRIES_TABLE, + 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'] + } + ]; + + constructor(private fileProvider: CoreFileProvider, + private sitesProvider: CoreSitesProvider, + private textUtils: CoreTextUtilsProvider, + private utils: CoreUtilsProvider) { + this.sitesProvider.createTablesFromSchema(this.tablesSchema); + } + + /** + * Delete a new entry. + * + * @param {number} glossaryId Glossary ID. + * @param {string} concept Glossary entry concept. + * @param {number} timeCreated The time the entry was created. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if deleted, rejected if failure. + */ + deleteNewEntry(glossaryId: number, concept: string, timeCreated: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const conditions = { + glossaryid: glossaryId, + concept: concept, + timecreated: timeCreated, + }; + + return site.getDb().deleteRecords(this.ENTRIES_TABLE, conditions); + }); + } + + /** + * Get all the stored new entries from all the glossaries. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with entries. + */ + getAllNewEntries(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().getRecords(this.ENTRIES_TABLE).then((records: any[]) => { + return records.map(this.parseRecord.bind(this)); + }); + }); + } + + /** + * Get a stored new entry. + * + * @param {number} glossaryId Glossary ID. + * @param {string} concept Glossary entry concept. + * @param {number} timeCreated The time the entry was created. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with entry. + */ + getNewEntry(glossaryId: number, concept: string, timeCreated: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const conditions = { + glossaryid: glossaryId, + concept: concept, + timecreated: timeCreated, + }; + + return site.getDb().getRecord(this.ENTRIES_TABLE, conditions).then(this.parseRecord.bind(this)); + }); + } + + /** + * Get all the stored add entry data from a certain glossary. + * + * @param {number} glossaryId Glossary ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @param {number} [userId] User the entries belong to. If not defined, current user in site. + * @return {Promise} Promise resolved with entries. + */ + getGlossaryNewEntries(glossaryId: number, siteId?: string, userId?: number): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const conditions = { + glossaryid: glossaryId, + userId: userId || site.getUserId(), + }; + + return site.getDb().getRecords(this.ENTRIES_TABLE, conditions).then((records: any[]) => { + return records.map(this.parseRecord.bind(this)); + }); + }); + } + + /** + * Check if a concept is used offline. + * + * @param {number} glossaryId Glossary ID. + * @param {string} concept Concept to check. + * @param {number} [timeCreated] Time of the entry we are editing. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with true if concept is found, false otherwise. + */ + isConceptUsed(glossaryId: number, concept: string, timeCreated?: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const conditions = { + glossaryid: glossaryId, + concept: concept, + }; + + return site.getDb().getRecords(this.ENTRIES_TABLE, conditions).then((entries) => { + 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 this.utils.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 {number} glossaryId Glossary ID. + * @param {string} concept Glossary entry concept. + * @param {string} definition Glossary entry concept definition. + * @param {number} courseId Course ID of the glossary. + * @param {any} [options] Options for the entry. + * @param {any} [attachments] Result of CoreFileUploaderProvider#storeFilesToUpload for attachments. + * @param {number} [timeCreated] The time the entry was created. If not defined, current time. + * @param {string} [siteId] Site ID. If not defined, current site. + * @param {number} [userId] User the entry belong to. If not defined, current user in site. + * @param {any} [discardEntry] The entry provided will be discarded if found. + * @return {Promise} Promise resolved if stored, rejected if failure. + */ + addNewEntry(glossaryId: number, concept: string, definition: string, courseId: number, options?: any, attachments?: any, + timeCreated?: number, siteId?: string, userId?: number, discardEntry?: any): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const entry = { + glossaryid: glossaryId, + courseid: courseId, + concept: concept, + definition: definition, + definitionformat: 'html', + options: JSON.stringify(options), + attachments: JSON.stringify(attachments), + userid: userId || site.getUserId(), + timecreated: timeCreated || new Date().getTime() + }; + + // If editing an offline entry, delete previous first. + let discardPromise; + if (discardEntry) { + discardPromise = this.deleteNewEntry(glossaryId, discardEntry.concept, discardEntry.timecreated, site.getId()); + } else { + discardPromise = Promise.resolve(); + } + + return discardPromise.then(() => { + return site.getDb().insertRecord(this.ENTRIES_TABLE, entry).then(() => false); + }); + }); + } + + /** + * Get the path to the folder where to store files for offline attachments in a glossary. + * + * @param {number} glossaryId Glossary ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the path. + */ + getGlossaryFolder(glossaryId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const siteFolderPath = this.fileProvider.getSiteFolder(site.getId()); + const folderPath = 'offlineglossary/' + glossaryId; + + return this.textUtils.concatenatePaths(siteFolderPath, folderPath); + }); + } + + /** + * Get the path to the folder where to store files for a new offline entry. + * + * @param {number} glossaryId Glossary ID. + * @param {string} concept The name of the entry. + * @param {number} timeCreated Time to allow duplicated entries. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the path. + */ + getEntryFolder(glossaryId: number, concept: string, timeCreated: number, siteId?: string): Promise { + return this.getGlossaryFolder(glossaryId, siteId).then((folderPath) => { + return this.textUtils.concatenatePaths(folderPath, 'newentry_' + concept + '_' + timeCreated); + }); + } + + /** + * Parse "options" and "attachments" columns of a fetched record. + * + * @param {any} records Record object + * @return {any} Record object with columns parsed. + */ + protected parseRecord(record: any): any { + record.options = this.textUtils.parseJSON(record.options); + record.attachments = this.textUtils.parseJSON(record.attachments); + + return record; + } +} diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 7c565ae77..a58fa3881 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -87,6 +87,7 @@ import { AddonModResourceModule } from '@addon/mod/resource/resource.module'; import { AddonModFeedbackModule } from '@addon/mod/feedback/feedback.module'; import { AddonModFolderModule } from '@addon/mod/folder/folder.module'; import { AddonModForumModule } from '@addon/mod/forum/forum.module'; +import { AddonModGlossaryModule } from '@addon/mod/glossary/glossary.module'; import { AddonModPageModule } from '@addon/mod/page/page.module'; import { AddonModQuizModule } from '@addon/mod/quiz/quiz.module'; import { AddonModScormModule } from '@addon/mod/scorm/scorm.module'; @@ -188,6 +189,7 @@ export const CORE_PROVIDERS: any[] = [ AddonModFeedbackModule, AddonModFolderModule, AddonModForumModule, + AddonModGlossaryModule, AddonModLtiModule, AddonModPageModule, AddonModQuizModule, From 507fd96d5c8ed1abe6edb25dea4fdf04ba49c36b Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Fri, 25 May 2018 15:41:40 +0200 Subject: [PATCH 2/7] MOBILE-2342 glossary: Implement helper and sync providers --- src/addon/mod/glossary/providers/helper.ts | 121 +++++++ .../glossary/providers/sync-cron-handler.ts | 47 +++ src/addon/mod/glossary/providers/sync.ts | 301 ++++++++++++++++++ 3 files changed, 469 insertions(+) create mode 100644 src/addon/mod/glossary/providers/helper.ts create mode 100644 src/addon/mod/glossary/providers/sync-cron-handler.ts create mode 100644 src/addon/mod/glossary/providers/sync.ts diff --git a/src/addon/mod/glossary/providers/helper.ts b/src/addon/mod/glossary/providers/helper.ts new file mode 100644 index 000000000..55a36960b --- /dev/null +++ b/src/addon/mod/glossary/providers/helper.ts @@ -0,0 +1,121 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader'; +import { CoreFileProvider } from '@providers/file'; +import { AddonModGlossaryProvider } from './glossary'; +import { AddonModGlossaryOfflineProvider } from './offline'; + +/** + * Helper to gather some common functions for glossary. + */ +@Injectable() +export class AddonModGlossaryHelperProvider { + + constructor(private fileProvider: CoreFileProvider, + private uploaderProvider: CoreFileUploaderProvider, + private glossaryOffline: AddonModGlossaryOfflineProvider) {} + + /** + * Delete stored attachment files for a new discussion. + * + * @param {number} glossaryId Glossary ID. + * @param {string} entryName The name of the entry. + * @param {number} timeCreated The time the entry was created. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when deleted. + */ + deleteStoredFiles(glossaryId: number, entryName: string, timeCreated: number, siteId?: string): Promise { + return this.glossaryOffline.getEntryFolder(glossaryId, entryName, timeCreated, siteId).then((folderPath) => { + this.fileProvider.removeDir(folderPath).catch(() => { + // Ignore any errors, CoreFileProvider.removeDir fails if folder doesn't exists. + }); + }); + } + + /** + * Get a list of stored attachment files for a new entry. See AddonModGlossaryHelperProvider#storeFiles. + * + * @param {number} glossaryId lossary ID. + * @param {string} entryName The name of the entry. + * @param {number} [timeCreated] The time the entry was created. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the files. + */ + getStoredFiles(glossaryId: number, entryName: string, timeCreated: number, siteId?: string): Promise { + return this.glossaryOffline.getEntryFolder(glossaryId, entryName, timeCreated, siteId).then((folderPath) => { + return this.uploaderProvider.getStoredFiles(folderPath); + }); + } + + /** + * Check if the data of an entry has changed. + * + * @param {any} entry Current data. + * @param {any[]} files Files attached. + * @param {any} original Original content. + * @return {boolean} True if data has changed, false otherwise. + */ + hasEntryDataChanged(entry: any, files: any[], original: any): 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 this.uploaderProvider.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 {number} glossaryId Glossary ID. + * @param {string} entryName The name of the entry. + * @param {number} [timeCreated] The time the entry was created. + * @param {any[]} files List of files. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if success, rejected otherwise. + */ + storeFiles(glossaryId: number, entryName: string, timeCreated: number, files: any[], siteId?: string): Promise { + // Get the folder where to store the files. + return this.glossaryOffline.getEntryFolder(glossaryId, entryName, timeCreated, siteId).then((folderPath) => { + return this.uploaderProvider.storeFilesToUpload(folderPath, files); + }); + } + + /** + * Upload or store some files, depending if the user is offline or not. + * + * @param {number} glossaryId Glossary ID. + * @param {string} entryName The name of the entry. + * @param {number} [timeCreated] The time the entry was created. + * @param {any[]} files List of files. + * @param {boolean} offline True if files sould be stored for offline, false to upload them. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if success. + */ + uploadOrStoreFiles(glossaryId: number, entryName: string, timeCreated: number, files: any[], offline: boolean, + siteId?: string): Promise { + if (offline) { + return this.storeFiles(glossaryId, entryName, timeCreated, files, siteId); + } else { + return this.uploaderProvider.uploadOrReuploadFiles(files, AddonModGlossaryProvider.COMPONENT, glossaryId, siteId); + } + } +} diff --git a/src/addon/mod/glossary/providers/sync-cron-handler.ts b/src/addon/mod/glossary/providers/sync-cron-handler.ts new file mode 100644 index 000000000..fdc3d5921 --- /dev/null +++ b/src/addon/mod/glossary/providers/sync-cron-handler.ts @@ -0,0 +1,47 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 '@providers/cron'; +import { AddonModGlossarySyncProvider } from './sync'; + +/** + * Synchronization cron handler. + */ +@Injectable() +export class AddonModGlossarySyncCronHandler implements CoreCronHandler { + name = 'AddonModGlossarySyncCronHandler'; + + constructor(private glossarySync: AddonModGlossarySyncProvider) {} + + /** + * Execute the process. + * Receives the ID of the site affected, undefined for all sites. + * + * @param {string} [siteId] ID of the site affected, undefined for all sites. + * @return {Promise} Promise resolved when done, rejected if failure. + */ + execute(siteId?: string): Promise { + return this.glossarySync.syncAllGlossaries(siteId); + } + + /** + * Get the time between consecutive executions. + * + * @return {number} Time between consecutive executions (in ms). + */ + getInterval(): number { + return AddonModGlossarySyncProvider.SYNC_TIME; + } +} diff --git a/src/addon/mod/glossary/providers/sync.ts b/src/addon/mod/glossary/providers/sync.ts new file mode 100644 index 000000000..ea509a690 --- /dev/null +++ b/src/addon/mod/glossary/providers/sync.ts @@ -0,0 +1,301 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { TranslateService } from '@ngx-translate/core'; +import { CoreSyncBaseProvider } from '@classes/base-sync'; +import { CoreCourseProvider } from '@core/course/providers/course'; +import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader'; +import { CoreAppProvider } from '@providers/app'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreSyncProvider } from '@providers/sync'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { AddonModGlossaryProvider } from './glossary'; +import { AddonModGlossaryHelperProvider } from './helper'; +import { AddonModGlossaryOfflineProvider } from './offline'; + +/** + * Service to sync glossaries. + */ +@Injectable() +export class AddonModGlossarySyncProvider extends CoreSyncBaseProvider { + + static AUTO_SYNCED = 'addon_mod_glossary_autom_synced'; + static SYNC_TIME = 600000; // 10 minutes. + + protected componentTranslate: string; + + constructor(translate: TranslateService, + appProvider: CoreAppProvider, + courseProvider: CoreCourseProvider, + private eventsProvider: CoreEventsProvider, + loggerProvider: CoreLoggerProvider, + sitesProvider: CoreSitesProvider, + syncProvider: CoreSyncProvider, + textUtils: CoreTextUtilsProvider, + private uploaderProvider: CoreFileUploaderProvider, + private utils: CoreUtilsProvider, + private glossaruProvider: AddonModGlossaryProvider, + private glossaryHelper: AddonModGlossaryHelperProvider, + private glossaryOffline: AddonModGlossaryOfflineProvider) { + + super('AddonModGlossarySyncProvider', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate); + + this.componentTranslate = courseProvider.translateModuleName('glossary'); + } + + /** + * Try to synchronize all the glossaries in a certain site or in all sites. + * + * @param {string} [siteId] Site ID to sync. If not defined, sync all sites. + * @return {Promise} Promise resolved if sync is successful, rejected if sync fails. + */ + syncAllGlossaries(siteId?: string): Promise { + return this.syncOnSites('all glossaries', this.syncAllGlossariesFunc.bind(this), [], siteId); + } + + /** + * Sync all glossaries on a site. + * + * @param {string} [siteId] Site ID to sync. If not defined, sync all sites. + * @return {Promise} Promise resolved if sync is successful, rejected if sync fails. + */ + protected syncAllGlossariesFunc(siteId?: string): Promise { + // Sync all new entries + return this.glossaryOffline.getAllNewEntries(siteId).then((entries) => { + const promises = {}; + + // Do not sync same glossary twice. + for (const i in entries) { + const entry = entries[i]; + + if (typeof promises[entry.glossaryid] != 'undefined') { + continue; + } + + promises[entry.glossaryid] = this.syncGlossaryEntriesIfNeeded(entry.glossaryid, entry.userid, siteId) + .then((result) => { + if (result && result.updated) { + // Sync successful, send event. + this.eventsProvider.trigger(AddonModGlossarySyncProvider.AUTO_SYNCED, { + glossaryId: entry.glossaryid, + userId: entry.userid, + warnings: result.warnings + }, siteId); + } + }); + } + + // Promises will be an object so, convert to an array first; + return Promise.all(this.utils.objectToArray(promises)); + }); + } + + /** + * Sync a glossary only if a certain time has passed since the last time. + * + * @param {number} glossaryId Glossary ID. + * @param {number} userId User the entry belong to. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the glossary is synced or if it doesn't need to be synced. + */ + syncGlossaryEntriesIfNeeded(glossaryId: number, userId: number, siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + const syncId = this.getGlossarySyncId(glossaryId, userId); + + return this.isSyncNeeded(syncId, siteId).then((needed) => { + if (needed) { + return this.syncGlossaryEntries(glossaryId, userId, siteId); + } + }); + } + + /** + * Synchronize all offline entries of a glossary. + * + * @param {number} glossaryId Glossary ID to be synced. + * @param {number} [userId] User the entries belong to. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if sync is successful, rejected otherwise. + */ + syncGlossaryEntries(glossaryId: number, userId?: number, siteId?: string): Promise { + userId = userId || this.sitesProvider.getCurrentSiteUserId(); + siteId = siteId || this.sitesProvider.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 (this.syncProvider.isBlocked(AddonModGlossaryProvider.COMPONENT, syncId, siteId)) { + this.logger.debug('Cannot sync glossary ' + glossaryId + ' because it is blocked.'); + + return Promise.reject(this.translate.instant('core.errorsyncblocked', {$a: this.componentTranslate})); + } + + this.logger.debug('Try to sync glossary ' + glossaryId + ' for user ' + userId); + + let courseId; + const result = { + warnings: [], + updated: false + }; + + // Get offline responses to be sent. + const syncPromise = this.glossaryOffline.getGlossaryNewEntries(glossaryId, siteId, userId).catch(() => { + // No offline data found, return empty object. + return []; + }).then((entries) => { + if (!entries.length) { + // Nothing to sync. + return; + } else if (!this.appProvider.isOnline()) { + // Cannot sync in offline. + return Promise.reject(null); + } + + const promises = []; + + entries.forEach((data) => { + let promise; + + courseId = data.courseid; + + // First of all upload the attachments (if any). + promise = this.uploadAttachments(glossaryId, data, siteId).then((itemId) => { + // Now try to add the entry. + return this.glossaruProvider.addEntryOnline( + glossaryId, data.concept, data.definition, data.options, itemId, siteId); + }); + + promises.push(promise.then(() => { + result.updated = true; + + return this.deleteAddEntry(glossaryId, data.concept, data.timecreated, siteId); + }).catch((error) => { + if (this.utils.isWebServiceError(error)) { + // The WebService has thrown an error, this means that responses cannot be submitted. Delete them. + result.updated = true; + + return this.deleteAddEntry(glossaryId, data.concept, data.timecreated, siteId).then(() => { + // Responses deleted, add a warning. + result.warnings.push(this.translate.instant('core.warningofflinedatadeleted', { + component: this.componentTranslate, + name: data.concept, + error: error.error + })); + }); + } else { + // Couldn't connect to server, reject. + return Promise.reject(error); + } + })); + }); + + return Promise.all(promises); + }).then(() => { + if (result.updated && courseId) { + // Data has been sent to server. Now invalidate the WS calls. + return this.glossaruProvider.getGlossaryById(courseId, glossaryId).then((glossary) => { + return this.glossaruProvider.invalidateGlossaryEntries(glossary, true); + }).catch(() => { + // Ignore errors. + }); + } + }).then(() => { + // Sync finished, set sync time. + return this.setSyncTime(syncId, siteId).catch(() => { + // Ignore errors. + }); + }).then(() => { + // All done, return the warnings. + return result; + }); + + return this.addOngoingSync(syncId, syncPromise, siteId); + } + + /** + * Delete a new entry. + * + * @param {number} glossaryId Glossary ID. + * @param {string} concept Glossary entry concept. + * @param {number} timeCreated Time to allow duplicated entries. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when deleted. + */ + protected deleteAddEntry(glossaryId: number, concept: string, timeCreated: number, siteId?: string): Promise { + const promises = []; + + promises.push(this.glossaryOffline.deleteNewEntry(glossaryId, concept, timeCreated, siteId)); + promises.push(this.glossaryHelper.deleteStoredFiles(glossaryId, concept, timeCreated, siteId).catch(() => { + // Ignore errors, maybe there are no files. + })); + + return Promise.all(promises); + } + + /** + * Upload attachments of an offline entry. + * + * @param {number} glossaryId Glossary ID. + * @param {any} entry Offline entry. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with draftid if uploaded, resolved with 0 if nothing to upload. + */ + protected uploadAttachments(glossaryId: number, entry: any, siteId?: string): Promise { + if (entry.attachments) { + // Has some attachments to sync. + let files = entry.attachments.online || []; + let promise; + + if (entry.attachments.offline) { + // Has offline files. + promise = this.glossaryHelper.getStoredFiles(glossaryId, entry.concept, entry.timecreated, siteId).then((atts) => { + files = files.concat(atts); + }).catch(() => { + // Folder not found, no files to add. + }); + } else { + promise = Promise.resolve(0); + } + + return promise.then(() => { + return this.uploaderProvider.uploadOrReuploadFiles(files, AddonModGlossaryProvider.COMPONENT, glossaryId, siteId); + }); + } + + // No attachments, resolve. + return Promise.resolve(0); + } + + /** + * Get the ID of a glossary sync. + * + * @param {number} glossaryId Glossary ID. + * @param {number} [userId] User the entries belong to.. If not defined, current user. + * @return {string} Sync ID. + */ + protected getGlossarySyncId(glossaryId: number, userId?: number): string { + userId = userId || this.sitesProvider.getCurrentSiteUserId(); + + return 'glossary#' + glossaryId + '#' + userId; + } +} From c2da659be34087b2a6bda1e96d1da9b8ba879db2 Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Fri, 25 May 2018 15:52:01 +0200 Subject: [PATCH 3/7] MOBILE-2342 glossary: Implement index and mode-picker components --- .../glossary/components/components.module.ts | 51 +++ .../mod/glossary/components/index/index.html | 64 +++ .../mod/glossary/components/index/index.ts | 400 ++++++++++++++++++ .../components/mode-picker/mode-picker.html | 6 + .../components/mode-picker/mode-picker.ts | 69 +++ src/addon/mod/glossary/glossary.module.ts | 6 + 6 files changed, 596 insertions(+) create mode 100644 src/addon/mod/glossary/components/components.module.ts create mode 100644 src/addon/mod/glossary/components/index/index.html create mode 100644 src/addon/mod/glossary/components/index/index.ts create mode 100644 src/addon/mod/glossary/components/mode-picker/mode-picker.html create mode 100644 src/addon/mod/glossary/components/mode-picker/mode-picker.ts diff --git a/src/addon/mod/glossary/components/components.module.ts b/src/addon/mod/glossary/components/components.module.ts new file mode 100644 index 000000000..c37c35d36 --- /dev/null +++ b/src/addon/mod/glossary/components/components.module.ts @@ -0,0 +1,51 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { IonicModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { CorePipesModule } from '@pipes/pipes.module'; +import { CoreCourseComponentsModule } from '@core/course/components/components.module'; +import { AddonModGlossaryIndexComponent } from './index/index'; +import { AddonModGlossaryModePickerPopoverComponent } from './mode-picker/mode-picker'; + +@NgModule({ + declarations: [ + AddonModGlossaryIndexComponent, + AddonModGlossaryModePickerPopoverComponent + ], + imports: [ + CommonModule, + IonicModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule, + CorePipesModule, + CoreCourseComponentsModule + ], + providers: [ + ], + exports: [ + AddonModGlossaryIndexComponent, + AddonModGlossaryModePickerPopoverComponent + ], + entryComponents: [ + AddonModGlossaryIndexComponent, + AddonModGlossaryModePickerPopoverComponent + ] +}) +export class AddonModGlossaryComponentsModule {} diff --git a/src/addon/mod/glossary/components/index/index.html b/src/addon/mod/glossary/components/index/index.html new file mode 100644 index 000000000..3778a757a --- /dev/null +++ b/src/addon/mod/glossary/components/index/index.html @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + {{ 'core.hasdatatosync' | translate:{$a: moduleName} }} + + + + + + + + + {{ 'addon.mod_glossary.entriestobesynced' | translate }} + + +

{{entry.concept}}

+
+
+ + + + + + {{getDivider(entry)}} + + + +

{{entry.concept}}

+
+
+
+ + + + + + +
+
+
diff --git a/src/addon/mod/glossary/components/index/index.ts b/src/addon/mod/glossary/components/index/index.ts new file mode 100644 index 000000000..cb19029fa --- /dev/null +++ b/src/addon/mod/glossary/components/index/index.ts @@ -0,0 +1,400 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { Component, Injector, ViewChild } from '@angular/core'; +import { Content, PopoverController } from 'ionic-angular'; +import { CoreSplitViewComponent } from '@components/split-view/split-view'; +import { CoreCourseModuleMainActivityComponent } from '@core/course/classes/main-activity-component'; +import { AddonModGlossaryProvider } from '../../providers/glossary'; +import { AddonModGlossaryOfflineProvider } from '../../providers/offline'; +import { AddonModGlossarySyncProvider } from '../../providers/sync'; +import { AddonModGlossaryModePickerPopoverComponent } from '../mode-picker/mode-picker'; + +type FetchMode = 'author_all' | 'cat_all' | 'newest_first' | 'recently_updated' | 'search' | 'letter_all'; + +/** + * Component that displays a glossary entry page. + */ +@Component({ + selector: 'addon-mod-glossary-index', + templateUrl: 'index.html', +}) +export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivityComponent { + @ViewChild(CoreSplitViewComponent) splitviewCtrl: CoreSplitViewComponent; + @ViewChild(Content) content: Content; + + component = AddonModGlossaryProvider.COMPONENT; + moduleName = 'glossary'; + + fetchMode: FetchMode; + viewMode: string; + isSearch = false; + entries = []; + offlineEntries = []; + canAdd = false; + canLoadMore = false; + loadingMessage = this.translate.instant('core.loading'); + selectedEntry: number; + + protected syncEventName = AddonModGlossarySyncProvider.AUTO_SYNCED; + protected glossary: any; + protected fetchFunction: Function; + protected fetchInvalidate: Function; + protected fetchArguments: any[]; + protected showDivider: (entry: any, previous?: any) => boolean; + protected getDivider: (entry: any) => string; + protected addEntryObserver: any; + + constructor(injector: Injector, + private popoverCtrl: PopoverController, + private glossaryProvider: AddonModGlossaryProvider, + private glossaryOffline: AddonModGlossaryOfflineProvider, + private glossarySync: AddonModGlossarySyncProvider) { + super(injector); + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + super.ngOnInit(); + + // When an entry is added, we reload the data. + this.addEntryObserver = this.eventsProvider.on(AddonModGlossaryProvider.ADD_ENTRY_EVENT, this.eventReceived.bind(this)); + + this.loadContent(false, true).then(() => { + if (!this.glossary) { + return; + } + + if (this.splitviewCtrl.isOn()) { + // Load the first entry. + if (this.entries.length > 0) { + this.openEntry(this.entries[0].id); + } + } + + this.glossaryProvider.logView(this.glossary.id, this.viewMode).then(() => { + this.courseProvider.checkModuleCompletion(this.courseId, this.module.completionstatus); + }).catch((error) => { + // Ignore errors. + }); + }); + } + + /** + * Download the component contents. + * + * @param {boolean} [refresh=false] Whether we're refreshing data. + * @param {boolean} [sync=false] If the refresh needs syncing. + * @param {boolean} [showErrors=false] Wether to show errors to the user or hide them. + * @return {Promise} Promise resolved when done. + */ + protected fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise { + return this.glossaryProvider.getGlossary(this.courseId, this.module.id).then((glossary) => { + this.glossary = glossary; + + this.description = glossary.intro || this.description; + this.canAdd = (this.glossaryProvider.isPluginEnabledForEditing() && glossary.canaddentry) || false; + + if (!this.fetchMode) { + this.switchMode('letter_all'); + } + + if (sync) { + // Try to synchronize the glossary. + return this.syncActivity(showErrors); + } + }).then(() => { + + return this.fetchEntries().then(() => { + // Check if there are responses stored in offline. + return this.glossaryOffline.getGlossaryNewEntries(this.glossary.id).then((offlineEntries) => { + offlineEntries.sort((a, b) => a.concept.localeCompare(b.fullname)); + this.hasOffline = !!offlineEntries.length; + this.offlineEntries = offlineEntries || []; + }); + }); + }).then(() => { + // All data obtained, now fill the context menu. + this.fillContextMenu(refresh); + }); + } + + /** + * Convenience function to fetch entries. + * + * @param {boolean} [append=false] True if fetched entries are appended to exsiting ones. + * @return {Promise} Promise resolved when done. + */ + protected fetchEntries(append: boolean = false): Promise { + if (!this.fetchFunction || !this.fetchArguments) { + // This happens in search mode with an empty query. + return Promise.resolve({entries: [], count: 0}); + } + + const limitFrom = append ? this.entries.length : 0; + const limitNum = AddonModGlossaryProvider.LIMIT_ENTRIES; + + return this.glossaryProvider.fetchEntries(this.fetchFunction, this.fetchArguments, limitFrom, limitNum).then((result) => { + if (append) { + Array.prototype.push.apply(this.entries, result.entries); + } else { + this.entries = result.entries; + } + this.canLoadMore = this.entries.length < result.count; + }).catch((error) => { + this.canLoadMore = false; // Set to false to prevent infinite calls with infinite-loading. + + return Promise.reject(error); + }); + } + + /** + * Perform the invalidate content function. + * + * @return {Promise} Resolved when done. + */ + protected invalidateContent(): Promise { + const promises = []; + + if (this.fetchInvalidate && this.fetchArguments) { + promises.push(this.fetchInvalidate.apply(this.glossaryProvider, this.fetchArguments)); + } + + promises.push(this.glossaryProvider.invalidateCourseGlossaries(this.courseId)); + + if (this.glossary && this.glossary.id) { + promises.push(this.glossaryProvider.invalidateCategories(this.glossary.id)); + } + + return Promise.all(promises); + } + + /** + * Performs the sync of the activity. + * + * @return {Promise} Promise resolved when done. + */ + protected sync(): Promise { + return this.glossarySync.syncGlossaryEntries(this.glossary.id); + } + + /** + * Checks if sync has succeed from result sync data. + * + * @param {any} result Data returned on the sync function. + * @return {boolean} Whether it succeed or not. + */ + protected hasSyncSucceed(result: any): boolean { + return result.updated; + } + + /** + * Compares sync event data with current data to check if refresh content is needed. + * + * @param {any} syncEventData Data receiven on sync observer. + * @return {boolean} True if refresh is needed, false otherwise. + */ + protected isRefreshSyncNeeded(syncEventData: any): boolean { + return this.glossary && syncEventData.glossaryId == this.glossary.id && + syncEventData.userId == this.sitesProvider.getCurrentSiteUserId(); + } + + /** + * Change fetch mode. + * + * @param {FetchMode} mode New mode. + */ + protected switchMode(mode: FetchMode): void { + this.fetchMode = mode; + + switch (mode) { + case 'author_all': + // Browse by author. + this.viewMode = 'author'; + this.fetchFunction = this.glossaryProvider.getEntriesByAuthor; + this.fetchInvalidate = this.glossaryProvider.invalidateEntriesByAuthor; + this.fetchArguments = [this.glossary.id, 'ALL', 'LASTNAME', 'ASC']; + this.getDivider = (entry: any): string => entry.userfullname; + this.showDivider = (entry: any, previous?: any): boolean => { + return previous === 'undefined' || entry.userid != previous.userid; + }; + break; + case 'cat_all': + // Browse by category. + this.viewMode = 'cat'; + this.fetchFunction = this.glossaryProvider.getEntriesByCategory; + this.fetchInvalidate = this.glossaryProvider.invalidateEntriesByCategory; + this.fetchArguments = [this.glossary.id, AddonModGlossaryProvider.SHOW_ALL_CATERGORIES]; + this.getDivider = (entry: any): string => entry.categoryname; + this.showDivider = (entry?: any, previous?: any): boolean => { + return !previous || this.getDivider(entry) != this.getDivider(previous); + }; + break; + case 'newest_first': + // Newest first. + this.viewMode = 'date'; + this.fetchFunction = this.glossaryProvider.getEntriesByDate; + this.fetchInvalidate = this.glossaryProvider.invalidateEntriesByDate; + this.fetchArguments = [this.glossary.id, 'CREATION', 'DESC']; + this.getDivider = null; + this.showDivider = (): boolean => false; + break; + case 'recently_updated': + // Recently updated. + this.viewMode = 'date'; + this.fetchFunction = this.glossaryProvider.getEntriesByDate; + this.fetchInvalidate = this.glossaryProvider.invalidateEntriesByDate; + this.fetchArguments = [this.glossary.id, 'UPDATE', 'DESC']; + this.getDivider = null; + this.showDivider = (): boolean => false; + break; + case 'search': + // Search for entries. + this.viewMode = 'search'; + this.fetchFunction = this.glossaryProvider.getEntriesBySearch; + this.fetchInvalidate = this.glossaryProvider.invalidateEntriesBySearch; + this.fetchArguments = null; // Dynamically set later. + this.getDivider = null; + this.showDivider = (): boolean => false; + break; + case 'letter_all': + default: + // Consider it is 'letter_all'. + this.viewMode = 'letter'; + this.fetchMode = 'letter_all'; + this.fetchFunction = this.glossaryProvider.getEntriesByLetter; + this.fetchInvalidate = this.glossaryProvider.invalidateEntriesByLetter; + this.fetchArguments = [this.glossary.id, 'ALL']; + this.getDivider = (entry: any): string => entry.concept.substr(0, 1).toUpperCase(); + this.showDivider = (entry?: any, previous?: any): boolean => { + return !previous || this.getDivider(entry) != this.getDivider(previous); + }; + break; + } + } + + /** + * Convenience function to load more forum discussions. + * + * @return {Promise} Promise resolved when done. + */ + loadMoreEntries(): Promise { + return this.fetchEntries(true).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'addon.mod_glossary.errorloadingentries', true); + }); + } + + /** + * Show the mode picker menu. + * + * @param {MouseEvent} event Event. + */ + openModePicker(event: MouseEvent): void { + const popover = this.popoverCtrl.create(AddonModGlossaryModePickerPopoverComponent, { + glossary: this.glossary, + selectedMode: this.fetchMode + }); + + popover.onDidDismiss((newMode: FetchMode) => { + if (newMode === this.fetchMode) { + return; + } + + this.loadingMessage = this.translate.instant('core.loading'); + this.content.scrollToTop(); + this.switchMode(newMode); + + if (this.fetchMode === 'search') { + // If it's not an instant search, then we reset the values. + this.entries = []; + this.canLoadMore = false; + } else { + this.loaded = false; + this.loadContent(); + } + }); + + popover.present({ + ev: event + }); + } + + /** + * Opens an entry. + * + * @param {number} entryId Entry id. + */ + openEntry(entryId: number): void { + const params = { + courseId: this.courseId, + entryId: entryId, + }; + this.splitviewCtrl.push('AddonModGlossaryEntryPage', params); + this.selectedEntry = entryId; + } + + /** + * Opens new entry editor. + * + * @param {any} [entry] Offline entry to edit. + */ + openNewEntry(entry?: any): void { + const params = { + courseId: this.courseId, + module: this.module, + glossary: this.glossary, + entry: entry, + }; + this.splitviewCtrl.getMasterNav().push('AddonModGlossaryEditPage', params); + this.selectedEntry = 0; + } + + /** + * Search entries. + * + * @param {string} query Text entered on the search box. + */ + search(query: string): void { + this.loadingMessage = this.translate.instant('core.searching'); + this.fetchArguments = [this.glossary.id, query, 1, 'CONCEPT', 'ASC']; + this.loaded = false; + this.loadContent(); + } + + /** + * Function called when we receive an event of new entry. + * + * @param {any} data Event data. + */ + protected eventReceived(data: any): void { + if (this.glossary && this.glossary.id === data.glossaryId) { + this.loaded = false; + this.loadContent(); + + // Check completion since it could be configured to complete once the user adds a new discussion or replies. + this.courseProvider.checkModuleCompletion(this.courseId, this.module.completionstatus); + } + } + + /** + * Component being destroyed. + */ + ngOnDestroy(): void { + super.ngOnDestroy(); + + this.addEntryObserver && this.addEntryObserver.off(); + } +} diff --git a/src/addon/mod/glossary/components/mode-picker/mode-picker.html b/src/addon/mod/glossary/components/mode-picker/mode-picker.html new file mode 100644 index 000000000..df7d9e525 --- /dev/null +++ b/src/addon/mod/glossary/components/mode-picker/mode-picker.html @@ -0,0 +1,6 @@ + + + {{mode.langkey | translate}} + + + diff --git a/src/addon/mod/glossary/components/mode-picker/mode-picker.ts b/src/addon/mod/glossary/components/mode-picker/mode-picker.ts new file mode 100644 index 000000000..f85118cef --- /dev/null +++ b/src/addon/mod/glossary/components/mode-picker/mode-picker.ts @@ -0,0 +1,69 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { Component } from '@angular/core'; +import { NavParams, ViewController } from 'ionic-angular'; + +/** + * Component to display the mode picker. + */ +@Component({ + selector: 'addon-mod-glossary-mode-picker-popover', + templateUrl: 'mode-picker.html' +}) +export class AddonModGlossaryModePickerPopoverComponent { + modes = []; + selectedMode: string; + + constructor(navParams: NavParams, private viewCtrl: ViewController) { + this.selectedMode = navParams.get('selectedMode'); + const glossary = navParams.get('glossary'); + + // Preparing browse modes. + this.modes = [ + {key: 'search', langkey: 'addon.mod_glossary.bysearch'} + ]; + glossary.browsemodes.forEach((mode) => { + switch (mode) { + case 'letter' : + this.modes.push({key: 'letter_all', langkey: 'addon.mod_glossary.byalphabet'}); + break; + case 'cat' : + this.modes.push({key: 'cat_all', langkey: 'addon.mod_glossary.bycategory'}); + break; + case 'date' : + this.modes.push({key: 'newest_first', langkey: 'addon.mod_glossary.bynewestfirst'}); + this.modes.push({key: 'recently_updated', langkey: 'addon.mod_glossary.byrecentlyupdated'}); + break; + case 'author' : + this.modes.push({key: 'author_all', langkey: 'addon.mod_glossary.byauthor'}); + break; + default: + } + }); + } + + /** + * Function called when a mode is clicked. + * + * @param {Event} event Click event. + * @param {string} key Clicked mode key. + * @return {boolean} Return true if success, false if error. + */ + modePicked(event: Event, key: string): boolean { + this.viewCtrl.dismiss(key); + + return true; + } +} diff --git a/src/addon/mod/glossary/glossary.module.ts b/src/addon/mod/glossary/glossary.module.ts index 0c12b9b7e..6c984079d 100644 --- a/src/addon/mod/glossary/glossary.module.ts +++ b/src/addon/mod/glossary/glossary.module.ts @@ -15,15 +15,21 @@ import { NgModule } from '@angular/core'; import { AddonModGlossaryProvider } from './providers/glossary'; import { AddonModGlossaryOfflineProvider } from './providers/offline'; +import { AddonModGlossaryHelperProvider } from './providers/helper'; +import { AddonModGlossarySyncProvider } from './providers/sync'; +import { AddonModGlossaryComponentsModule } from './components/components.module'; @NgModule({ declarations: [ ], imports: [ + AddonModGlossaryComponentsModule, ], providers: [ AddonModGlossaryProvider, AddonModGlossaryOfflineProvider, + AddonModGlossaryHelperProvider, + AddonModGlossarySyncProvider, ] }) export class AddonModGlossaryModule { From 8c8028e2772bb23ad2c3efa72e215202d4e2411e Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Fri, 25 May 2018 15:56:15 +0200 Subject: [PATCH 4/7] MOBILE-2342 glossary: Implement index, entry and edit pages --- src/addon/mod/glossary/pages/edit/edit.html | 51 ++++ .../mod/glossary/pages/edit/edit.module.ts | 33 +++ src/addon/mod/glossary/pages/edit/edit.ts | 253 ++++++++++++++++++ src/addon/mod/glossary/pages/entry/entry.html | 45 ++++ .../mod/glossary/pages/entry/entry.module.ts | 35 +++ src/addon/mod/glossary/pages/entry/entry.ts | 110 ++++++++ src/addon/mod/glossary/pages/index/index.html | 11 + .../mod/glossary/pages/index/index.module.ts | 33 +++ src/addon/mod/glossary/pages/index/index.ts | 48 ++++ 9 files changed, 619 insertions(+) create mode 100644 src/addon/mod/glossary/pages/edit/edit.html create mode 100644 src/addon/mod/glossary/pages/edit/edit.module.ts create mode 100644 src/addon/mod/glossary/pages/edit/edit.ts create mode 100644 src/addon/mod/glossary/pages/entry/entry.html create mode 100644 src/addon/mod/glossary/pages/entry/entry.module.ts create mode 100644 src/addon/mod/glossary/pages/entry/entry.ts create mode 100644 src/addon/mod/glossary/pages/index/index.html create mode 100644 src/addon/mod/glossary/pages/index/index.module.ts create mode 100644 src/addon/mod/glossary/pages/index/index.ts diff --git a/src/addon/mod/glossary/pages/edit/edit.html b/src/addon/mod/glossary/pages/edit/edit.html new file mode 100644 index 000000000..f42d75b3e --- /dev/null +++ b/src/addon/mod/glossary/pages/edit/edit.html @@ -0,0 +1,51 @@ + + + + + + + + + + + + + {{ 'addon.mod_glossary.concept' | translate }} + + + + {{ 'addon.mod_glossary.definition' | translate }} + + + + + {{ 'addon.mod_glossary.categories' | translate }} + + {{ category.name }} + + + + {{ 'addon.mod_glossary.aliases' | translate }} + + + {{ 'addon.mod_glossary.attachment' | translate }} + + + {{ 'addon.mod_glossary.linking' | translate }} + + {{ 'addon.mod_glossary.entryusedynalink' | translate }} + + + + {{ 'addon.mod_glossary.casesensitive' | translate }} + + + + {{ 'addon.mod_glossary.fullmatch' | translate }} + + + + + + diff --git a/src/addon/mod/glossary/pages/edit/edit.module.ts b/src/addon/mod/glossary/pages/edit/edit.module.ts new file mode 100644 index 000000000..50cc3a93c --- /dev/null +++ b/src/addon/mod/glossary/pages/edit/edit.module.ts @@ -0,0 +1,33 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { NgModule } from '@angular/core'; +import { IonicPageModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { AddonModGlossaryEditPage } from './edit'; + +@NgModule({ + declarations: [ + AddonModGlossaryEditPage, + ], + imports: [ + CoreComponentsModule, + CoreDirectivesModule, + IonicPageModule.forChild(AddonModGlossaryEditPage), + TranslateModule.forChild() + ], +}) +export class AddonModGlossaryNewDiscussionPageModule {} diff --git a/src/addon/mod/glossary/pages/edit/edit.ts b/src/addon/mod/glossary/pages/edit/edit.ts new file mode 100644 index 000000000..ab1bf46eb --- /dev/null +++ b/src/addon/mod/glossary/pages/edit/edit.ts @@ -0,0 +1,253 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { Component, OnInit } from '@angular/core'; +import { FormControl } from '@angular/forms'; +import { IonicPage, NavController, NavParams } from 'ionic-angular'; +import { TranslateService } from '@ngx-translate/core'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader'; +import { AddonModGlossaryProvider } from '../../providers/glossary'; +import { AddonModGlossaryOfflineProvider } from '../../providers/offline'; +import { AddonModGlossaryHelperProvider } from '../../providers/helper'; + +/** + * Page that displays the edit form. + */ +@IonicPage({ segment: 'addon-mod-glossary-edit' }) +@Component({ + selector: 'page-addon-mod-glossary-edit', + templateUrl: 'edit.html', +}) +export class AddonModGlossaryEditPage implements OnInit { + component = AddonModGlossaryProvider.COMPONENT; + loaded = false; + entry = { + concept: '', + definition: '', + timecreated: 0, + }; + options = { + categories: [], + aliases: '', + usedynalink: false, + casesensitive: false, + fullmatch: false + }; + attachments = []; + definitionControl = new FormControl(); + categories = []; + + protected courseId: number; + protected module: any; + protected glossary: any; + protected syncId: string; + protected syncObserver: any; + protected isDestroyed = false; + protected originalData: any; + protected saved = false; + + constructor(private navParams: NavParams, + private navCtrl: NavController, + private translate: TranslateService, + private domUtils: CoreDomUtilsProvider, + private eventsProvider: CoreEventsProvider, + private sitesProvider: CoreSitesProvider, + private uploaderProvider: CoreFileUploaderProvider, + private textUtils: CoreTextUtilsProvider, + private glossaryProvider: AddonModGlossaryProvider, + private glossaryOffline: AddonModGlossaryOfflineProvider, + private glossaryHelper: AddonModGlossaryHelperProvider) { + this.courseId = navParams.get('courseId'); + this.module = navParams.get('module'); + this.glossary = navParams.get('glossary'); + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + const entry = this.navParams.get('entry'); + + let promise; + + if (entry) { + this.entry.concept = entry.concept || ''; + this.entry.definition = entry.definition || ''; + + this.originalData = { + concept: this.entry.concept, + definition: this.entry.definition, + files: [], + }; + + if (entry.options) { + this.options.categories = entry.options.categories || []; + this.options.aliases = entry.options.aliases || ''; + this.options.usedynalink = !!entry.options.usedynalink; + if (this.options.usedynalink) { + this.options.casesensitive = !!entry.options.casesensitive; + this.options.fullmatch = !!entry.options.fullmatch; + } + } + + // Treat offline attachments if any. + if (entry.attachments && entry.attachments.offline) { + promise = this.glossaryHelper.getStoredFiles(this.glossary.id, entry.concept, entry.timecreated).then((files) => { + this.attachments = files; + this.originalData.files = files.slice(); + }); + } + } + + this.definitionControl.setValue(this.entry.definition); + + Promise.resolve(promise).then(() => { + this.glossaryProvider.getAllCategories(this.glossary.id).then((categories) => { + this.categories = categories; + }).finally(() => { + this.loaded = true; + }); + }); + } + + /** + * Definition changed. + * + * @param {string} text The new text. + */ + onDefinitionChange(text: string): void { + this.entry.definition = text; + } + + /** + * Check if we can leave the page or not. + * + * @return {boolean|Promise} Resolved if we can leave it, rejected if not. + */ + ionViewCanLeave(): boolean | Promise { + let promise: any; + + if (!this.saved && this.glossaryHelper.hasEntryDataChanged(this.entry, this.attachments, this.originalData)) { + // Show confirmation if some data has been modified. + promise = this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit')); + } else { + promise = Promise.resolve(); + } + + return promise.then(() => { + // Delete the local files from the tmp folder. + this.uploaderProvider.clearTmpFiles(this.attachments); + }); + } + + /** + * Save the entry. + */ + save(): void { + let definition = this.entry.definition; + const timecreated = this.entry.timecreated || Date.now(); + let saveOffline = false; + + if (!this.entry.concept || !definition) { + this.domUtils.showErrorModal('addon.mod_glossary.fillfields', true); + + return; + } + + const modal = this.domUtils.showModalLoading('core.sending', true); + + // Check if rich text editor is enabled or not. + this.domUtils.isRichTextEditorEnabled().then((enabled) => { + if (!enabled) { + // Rich text editor not enabled, add some HTML to the definition if needed. + definition = this.textUtils.formatHtmlLines(definition); + } + + // Upload attachments first if any. + if (this.attachments.length > 0) { + return this.glossaryHelper.uploadOrStoreFiles(this.glossary.id, this.entry.concept, timecreated, this.attachments, + false).catch(() => { + // Cannot upload them in online, save them in offline. + saveOffline = true; + + return this.glossaryHelper.uploadOrStoreFiles(this.glossary.id, this.entry.concept, timecreated, + this.attachments, true); + }); + } + }).then((attach) => { + const options: any = { + aliases: this.options.aliases, + categories: this.options.categories.join(',') + }; + + if (this.glossary.usedynalink) { + options.usedynalink = this.options.usedynalink ? 1 : 0; + if (this.options.usedynalink) { + options.casesensitive = this.options.casesensitive ? 1 : 0; + options.fullmatch = this.options.fullmatch ? 1 : 0; + } + } + + if (saveOffline) { + let promise; + if (this.entry && !this.glossary.allowduplicatedentries) { + // Check if the entry is duplicated in online or offline mode. + promise = this.glossaryProvider.isConceptUsed(this.glossary.id, this.entry.concept, this.entry.timecreated) + .then((used) => { + if (used) { + // There's a entry with same name, reject with error message. + return Promise.reject(this.translate.instant('addon.mod_glossary.errconceptalreadyexists')); + } + }); + } else { + promise = Promise.resolve(); + } + + return promise.then(() => { + // Save entry in offline. + return this.glossaryOffline.addNewEntry(this.glossary.id, this.entry.concept, definition, this.courseId, + options, attach, timecreated, undefined, undefined, this.entry).then(() => { + // Don't return anything. + }); + }); + } else { + // Try to send it to server. + // Don't allow offline if there are attachments since they were uploaded fine. + return this.glossaryProvider.addEntry(this.glossary.id, this.entry.concept, definition, this.courseId, options, + attach, timecreated, undefined, this.entry, !this.attachments.length, !this.glossary.allowduplicatedentries); + } + }).then((entryId) => { + if (entryId) { + // Data sent to server, delete stored files (if any). + this.glossaryHelper.deleteStoredFiles(this.glossary.id, this.entry.concept, timecreated); + } + + const data = { + glossaryId: this.glossary.id, + }; + this.eventsProvider.trigger(AddonModGlossaryProvider.ADD_ENTRY_EVENT, data, this.sitesProvider.getCurrentSiteId()); + + this.saved = true; + this.navCtrl.pop(); + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'addon.mod_glossary.cannoteditentry', true); + }).finally(() => { + modal.dismiss(); + }); + } +} diff --git a/src/addon/mod/glossary/pages/entry/entry.html b/src/addon/mod/glossary/pages/entry/entry.html new file mode 100644 index 000000000..5bccd5f20 --- /dev/null +++ b/src/addon/mod/glossary/pages/entry/entry.html @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + +

+ {{ entry.timemodified | coreDateDayOrTime }} +

+
+ +

+ {{ entry.timemodified | coreDateDayOrTime }} +
+ + + + + + + +

{{ 'addon.mod_glossary.entrypendingapproval' | translate }}

+
+
+ + + + {{ 'addon.mod_glossary.errorloadingentry' | translate }} + + +
+
diff --git a/src/addon/mod/glossary/pages/entry/entry.module.ts b/src/addon/mod/glossary/pages/entry/entry.module.ts new file mode 100644 index 000000000..83b3642cf --- /dev/null +++ b/src/addon/mod/glossary/pages/entry/entry.module.ts @@ -0,0 +1,35 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { NgModule } from '@angular/core'; +import { IonicPageModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { CorePipesModule } from '@pipes/pipes.module'; +import { AddonModGlossaryEntryPage } from './entry'; + +@NgModule({ + declarations: [ + AddonModGlossaryEntryPage, + ], + imports: [ + CoreComponentsModule, + CoreDirectivesModule, + CorePipesModule, + IonicPageModule.forChild(AddonModGlossaryEntryPage), + TranslateModule.forChild() + ], +}) +export class AddonModForumDiscussionPageModule {} diff --git a/src/addon/mod/glossary/pages/entry/entry.ts b/src/addon/mod/glossary/pages/entry/entry.ts new file mode 100644 index 000000000..f6aa7df41 --- /dev/null +++ b/src/addon/mod/glossary/pages/entry/entry.ts @@ -0,0 +1,110 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { Component } from '@angular/core'; +import { IonicPage, NavParams } from 'ionic-angular'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { AddonModGlossaryProvider } from '../../providers/glossary'; + +/** + * Page that displays a glossary entry. + */ +@IonicPage({ segment: 'addon-mod-glossary-entry' }) +@Component({ + selector: 'page-addon-mod-glossary-entry', + templateUrl: 'entry.html', +}) +export class AddonModGlossaryEntryPage { + component = AddonModGlossaryProvider.COMPONENT; + componentId: number; + entry: any; + loaded = false; + showAuthor = false; + showDate = false; + + protected courseId: number; + protected entryId: number; + + constructor(navParams: NavParams, + private domUtils: CoreDomUtilsProvider, + private glossaryProvider: AddonModGlossaryProvider) { + this.courseId = navParams.get('courseId'); + this.entryId = navParams.get('entryId'); + } + + /** + * View loaded. + */ + ionViewDidLoad(): void { + this.fetchEntry().then(() => { + this.glossaryProvider.logEntryView(this.entry.id); + }).finally(() => { + this.loaded = true; + }); + } + + /** + * Refresh the data. + * + * @param {any} [refresher] Refresher. + * @return {Promise} Promise resolved when done. + */ + doRefresh(refresher?: any): Promise { + return this.glossaryProvider.invalidateEntry(this.entry.id).catch(() => { + // Ignore errors. + }).then(() => { + return this.fetchEntry(true); + }).finally(() => { + refresher && refresher.complete(); + }); + } + + /** + * Convenience function to get the glossary entry. + * + * @param {boolean} [refresh] Whether we're refreshing data. + * @return {Promise} Promise resolved when done. + */ + protected fetchEntry(refresh?: boolean): Promise { + return this.glossaryProvider.getEntry(this.entryId).then((result) => { + this.entry = result; + + if (!refresh) { + // Load the glossary. + return this.glossaryProvider.getGlossaryById(this.courseId, this.entry.glossaryid).then((glossary) => { + this.componentId = glossary.coursemodule; + + switch (glossary.displayformat) { + case 'fullwithauthor': + case 'encyclopedia': + this.showAuthor = true; + this.showDate = true; + break; + case 'fullwithoutauthor': + this.showAuthor = false; + this.showDate = true; + break; + default: // Default, and faq, simple, entrylist, continuous. + this.showAuthor = false; + this.showDate = false; + } + }); + } + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'addon.mod_glossary.errorloadingentry', true); + + return Promise.reject(null); + }); + } +} diff --git a/src/addon/mod/glossary/pages/index/index.html b/src/addon/mod/glossary/pages/index/index.html new file mode 100644 index 000000000..02c599875 --- /dev/null +++ b/src/addon/mod/glossary/pages/index/index.html @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/addon/mod/glossary/pages/index/index.module.ts b/src/addon/mod/glossary/pages/index/index.module.ts new file mode 100644 index 000000000..415f45272 --- /dev/null +++ b/src/addon/mod/glossary/pages/index/index.module.ts @@ -0,0 +1,33 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { NgModule } from '@angular/core'; +import { IonicPageModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { AddonModGlossaryComponentsModule } from '../../components/components.module'; +import { AddonModGlossaryIndexPage } from './index'; + +@NgModule({ + declarations: [ + AddonModGlossaryIndexPage, + ], + imports: [ + CoreDirectivesModule, + AddonModGlossaryComponentsModule, + IonicPageModule.forChild(AddonModGlossaryIndexPage), + TranslateModule.forChild() + ], +}) +export class AddonModGlossaryIndexPageModule {} diff --git a/src/addon/mod/glossary/pages/index/index.ts b/src/addon/mod/glossary/pages/index/index.ts new file mode 100644 index 000000000..a812235fe --- /dev/null +++ b/src/addon/mod/glossary/pages/index/index.ts @@ -0,0 +1,48 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { Component, ViewChild } from '@angular/core'; +import { IonicPage, NavParams } from 'ionic-angular'; +import { AddonModGlossaryIndexComponent } from '../../components/index/index'; + +/** + * Page that displays a glossary. + */ +@IonicPage({ segment: 'addon-mod-glossary-index' }) +@Component({ + selector: 'page-addon-mod-glossary-index', + templateUrl: 'index.html', +}) +export class AddonModGlossaryIndexPage { + @ViewChild(AddonModGlossaryIndexComponent) glossaryComponent: AddonModGlossaryIndexComponent; + + title: string; + module: any; + courseId: number; + + constructor(navParams: NavParams) { + this.module = navParams.get('module') || {}; + this.courseId = navParams.get('courseId'); + this.title = this.module.name; + } + + /** + * Update some data based on the glossary instance. + * + * @param {any} glossary Glossary instance. + */ + updateData(glossary: any): void { + this.title = glossary.name || this.title; + } +} From 3936438e7f1e19cb5ef12c5e3dbc9d45cf5bdd21 Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Fri, 25 May 2018 15:58:29 +0200 Subject: [PATCH 5/7] MOBILE-2342 glossary: Implement module, prefetch and link handlers --- src/addon/mod/glossary/glossary.module.ts | 24 +++ .../glossary/providers/entry-link-handler.ts | 77 +++++++ .../glossary/providers/index-link-handler.ts | 30 +++ .../mod/glossary/providers/module-handler.ts | 81 ++++++++ .../glossary/providers/prefetch-handler.ts | 194 ++++++++++++++++++ 5 files changed, 406 insertions(+) create mode 100644 src/addon/mod/glossary/providers/entry-link-handler.ts create mode 100644 src/addon/mod/glossary/providers/index-link-handler.ts create mode 100644 src/addon/mod/glossary/providers/module-handler.ts create mode 100644 src/addon/mod/glossary/providers/prefetch-handler.ts diff --git a/src/addon/mod/glossary/glossary.module.ts b/src/addon/mod/glossary/glossary.module.ts index 6c984079d..424f72bdb 100644 --- a/src/addon/mod/glossary/glossary.module.ts +++ b/src/addon/mod/glossary/glossary.module.ts @@ -13,10 +13,19 @@ // limitations under the License. import { NgModule } from '@angular/core'; +import { CoreCronDelegate } from '@providers/cron'; +import { CoreCourseModuleDelegate } from '@core/course/providers/module-delegate'; +import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; +import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate'; import { AddonModGlossaryProvider } from './providers/glossary'; import { AddonModGlossaryOfflineProvider } from './providers/offline'; import { AddonModGlossaryHelperProvider } from './providers/helper'; import { AddonModGlossarySyncProvider } from './providers/sync'; +import { AddonModGlossaryModuleHandler } from './providers/module-handler'; +import { AddonModGlossaryPrefetchHandler } from './providers/prefetch-handler'; +import { AddonModGlossarySyncCronHandler } from './providers/sync-cron-handler'; +import { AddonModGlossaryIndexLinkHandler } from './providers/index-link-handler'; +import { AddonModGlossaryEntryLinkHandler } from './providers/entry-link-handler'; import { AddonModGlossaryComponentsModule } from './components/components.module'; @NgModule({ @@ -30,7 +39,22 @@ import { AddonModGlossaryComponentsModule } from './components/components.module AddonModGlossaryOfflineProvider, AddonModGlossaryHelperProvider, AddonModGlossarySyncProvider, + AddonModGlossaryModuleHandler, + AddonModGlossaryPrefetchHandler, + AddonModGlossarySyncCronHandler, + AddonModGlossaryIndexLinkHandler, + AddonModGlossaryEntryLinkHandler, ] }) export class AddonModGlossaryModule { + constructor(moduleDelegate: CoreCourseModuleDelegate, moduleHandler: AddonModGlossaryModuleHandler, + prefetchDelegate: CoreCourseModulePrefetchDelegate, prefetchHandler: AddonModGlossaryPrefetchHandler, + cronDelegate: CoreCronDelegate, syncHandler: AddonModGlossarySyncCronHandler, linksDelegate: CoreContentLinksDelegate, + indexHandler: AddonModGlossaryIndexLinkHandler, discussionHandler: AddonModGlossaryEntryLinkHandler) { + moduleDelegate.registerHandler(moduleHandler); + prefetchDelegate.registerHandler(prefetchHandler); + cronDelegate.register(syncHandler); + linksDelegate.registerHandler(indexHandler); + linksDelegate.registerHandler(discussionHandler); + } } diff --git a/src/addon/mod/glossary/providers/entry-link-handler.ts b/src/addon/mod/glossary/providers/entry-link-handler.ts new file mode 100644 index 000000000..137ccca00 --- /dev/null +++ b/src/addon/mod/glossary/providers/entry-link-handler.ts @@ -0,0 +1,77 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler'; +import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate'; +import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; +import { CoreCourseHelperProvider } from '@core/course/providers/helper'; +import { AddonModGlossaryProvider } from './glossary'; + +/** + * Handler to treat links to glossary entries. + */ +@Injectable() +export class AddonModGlossaryEntryLinkHandler extends CoreContentLinksHandlerBase { + name = 'AddonModGlossaryEntryLinkHandler'; + featureName = 'CoreCourseModuleDelegate_AddonModGlossary'; + pattern = /\/mod\/glossary\/showentry\.php.*([\&\?]eid=\d+)/; + + constructor( + private domUtils: CoreDomUtilsProvider, + private linkHelper: CoreContentLinksHelperProvider, + private glossaryProvider: AddonModGlossaryProvider, + private courseHelper: CoreCourseHelperProvider) { + super(); + } + + /** + * Get the list of actions for a link (url). + * + * @param {string[]} siteIds List of sites the URL belongs to. + * @param {string} url The URL to treat. + * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} + * @param {number} [courseId] Course ID related to the URL. Optional but recommended. + * @return {CoreContentLinksAction[]|Promise} List of (or promise resolved with list of) actions. + */ + getActions(siteIds: string[], url: string, params: any, courseId?: number): + CoreContentLinksAction[] | Promise { + return [{ + action: (siteId, navCtrl?): void => { + const modal = this.domUtils.showModalLoading(); + const entryId = parseInt(params.eid, 10); + let promise; + + if (courseId) { + promise = Promise.resolve(courseId); + } else { + promise = this.glossaryProvider.getEntry(entryId, siteId).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'addon.mod_glossary.errorloadingentry', true); + + return Promise.reject(null); + }).then((entry) => { + return this.courseHelper.getModuleCourseIdByInstance(entry.glossaryid, 'glossary', siteId); + }); + } + + return promise.then((courseId) => { + this.linkHelper.goInSite(navCtrl, 'AddonModGlossaryEntryPage', {courseId, entryId}, siteId); + }).finally(() => { + modal.dismiss(); + }); + } + }]; + } +} diff --git a/src/addon/mod/glossary/providers/index-link-handler.ts b/src/addon/mod/glossary/providers/index-link-handler.ts new file mode 100644 index 000000000..d71be0005 --- /dev/null +++ b/src/addon/mod/glossary/providers/index-link-handler.ts @@ -0,0 +1,30 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 '@core/contentlinks/classes/module-index-handler'; +import { CoreCourseHelperProvider } from '@core/course/providers/helper'; +import { AddonModGlossaryProvider } from './glossary'; + +/** + * Handler to treat links to glossary index. + */ +@Injectable() +export class AddonModGlossaryIndexLinkHandler extends CoreContentLinksModuleIndexHandler { + name = 'AddonModGlossaryIndexLinkHandler'; + + constructor(courseHelper: CoreCourseHelperProvider, protected glossaryProvider: AddonModGlossaryProvider) { + super(courseHelper, 'AddonModGlossary', 'glossary'); + } +} diff --git a/src/addon/mod/glossary/providers/module-handler.ts b/src/addon/mod/glossary/providers/module-handler.ts new file mode 100644 index 000000000..ce81f445b --- /dev/null +++ b/src/addon/mod/glossary/providers/module-handler.ts @@ -0,0 +1,81 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { NavController, NavOptions } from 'ionic-angular'; +import { AddonModGlossaryIndexComponent } from '../components/index/index'; +import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@core/course/providers/module-delegate'; +import { CoreCourseProvider } from '@core/course/providers/course'; + +/** + * Handler to support glossary modules. + */ +@Injectable() +export class AddonModGlossaryModuleHandler implements CoreCourseModuleHandler { + name = 'AddonModGlossary'; + modName = 'glossary'; + + constructor(private courseProvider: CoreCourseProvider) { } + + /** + * Check if the handler is enabled on a site level. + * + * @return {boolean} Whether or not the handler is enabled on a site level. + */ + isEnabled(): boolean { + return true; + } + + /** + * Get the data required to display the module in the course contents view. + * + * @param {any} module The module object. + * @param {number} courseId The course ID. + * @param {number} sectionId The section ID. + * @return {CoreCourseModuleHandlerData} Data to render the module. + */ + getData(module: any, courseId: number, sectionId: number): CoreCourseModuleHandlerData { + return { + icon: this.courseProvider.getModuleIconSrc('glossary'), + title: module.name, + class: 'addon-mod_glossary-handler', + showDownloadButton: true, + action(event: Event, navCtrl: NavController, module: any, courseId: number, options: NavOptions): void { + navCtrl.push('AddonModGlossaryIndexPage', {module: module, courseId: courseId}, options); + } + }; + } + + /** + * Get the component to render the module. This is needed to support singleactivity course format. + * The component returned must implement CoreCourseModuleMainComponent. + * + * @param {any} course The course object. + * @param {any} module The module object. + * @return {any} The component to use, undefined if not found. + */ + getMainComponent(course: any, module: any): any { + return AddonModGlossaryIndexComponent; + } + + /** + * Whether to display the course refresher in single activity course format. If it returns false, a refresher must be + * included in the template that calls the doRefresh method of the component. Defaults to true. + * + * @return {boolean} Whether the refresher should be displayed. + */ + displayRefresherInSingleActivity(): boolean { + return false; + } +} diff --git a/src/addon/mod/glossary/providers/prefetch-handler.ts b/src/addon/mod/glossary/providers/prefetch-handler.ts new file mode 100644 index 000000000..16e088851 --- /dev/null +++ b/src/addon/mod/glossary/providers/prefetch-handler.ts @@ -0,0 +1,194 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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, Injector } from '@angular/core'; +import { CoreCourseModulePrefetchHandlerBase } from '@core/course/classes/module-prefetch-handler'; +import { CoreUserProvider } from '@core/user/providers/user'; +import { AddonModGlossaryProvider } from './glossary'; +import { CoreConstants } from '@core/constants'; + +/** + * Handler to prefetch forums. + */ +@Injectable() +export class AddonModGlossaryPrefetchHandler extends CoreCourseModulePrefetchHandlerBase { + name = 'AddonModGlossary'; + modName = 'glossary'; + component = AddonModGlossaryProvider.COMPONENT; + updatesNames = /^configuration$|^.*files$|^entries$/; + + constructor(injector: Injector, + private userProvider: CoreUserProvider, + private glossaryProvider: AddonModGlossaryProvider) { + super(injector); + } + + /** + * Return the status to show based on current status. E.g. a module might want to show outdated instead of downloaded. + * If not implemented, the original status will be returned. + * + * @param {any} module Module. + * @param {string} status The current status. + * @param {boolean} canCheck Whether the site allows checking for updates. + * @return {string} Status to display. + */ + determineStatus(module: any, status: string, canCheck: boolean): string { + if (!canCheck && status === CoreConstants.DOWNLOADED) { + /* Glossary are always marked as outdated if updates cannot be checked because we can't tell if there's something + new without having to call all the WebServices. */ + return CoreConstants.OUTDATED; + } else { + return status; + } + } + + /** + * Download the module. + * + * @param {any} module The module object returned by WS. + * @param {number} courseId Course ID. + * @param {string} [dirPath] Path of the directory where to store all the content files. @see downloadOrPrefetch. + * @return {Promise} Promise resolved when all content is downloaded. + */ + download(module: any, courseId: number, dirPath?: string): Promise { + // Glossaries cannot be downloaded right away, only prefetched. + return this.prefetch(module, courseId); + } + + /** + * Get list of files. If not defined, we'll assume they're in module.contents. + * + * @param {any} module Module. + * @param {Number} courseId Course ID the module belongs to. + * @param {boolean} [single] True if we're downloading a single module, false if we're downloading a whole section. + * @return {Promise} Promise resolved with the list of files. + */ + getFiles(module: any, courseId: number, single?: boolean): Promise { + return this.glossaryProvider.getGlossary(courseId, module.id).then((glossary) => { + return this.glossaryProvider.fetchAllEntries(this.glossaryProvider.getEntriesByLetter, [glossary.id, 'ALL']) + .then((entries) => { + 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 {any} module Module to get the files. + * @param {any} glossary Glossary + * @param {any[]} entries Entries of the Glossary. + * @return {any[]} List of Files. + */ + protected getFilesFromGlossaryAndEntries(module: any, glossary: any, entries: any[]): any[] { + let files = this.getIntroFilesFromInstance(module, glossary); + // Get entries files. + entries.forEach((entry) => { + files = files.concat(this.domUtils.extractDownloadableFilesFromHtmlAsFakeFileObjects(entry.definition)); + files = files.concat(entry.attachments); + }); + + return files; + } + + /** + * Invalidate the prefetched content. + * + * @param {number} moduleId The module ID. + * @param {number} courseId The course ID the module belongs to. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateContent(moduleId: number, courseId: number): Promise { + return this.glossaryProvider.invalidateContent(moduleId, courseId); + } + + /** + * Prefetch a module. + * + * @param {any} module Module. + * @param {number} courseId Course ID the module belongs to. + * @param {boolean} [single] True if we're downloading a single module, false if we're downloading a whole section. + * @param {string} [dirPath] Path of the directory where to store all the content files. @see downloadOrPrefetch. + * @return {Promise} Promise resolved when done. + */ + prefetch(module: any, courseId?: number, single?: boolean, dirPath?: string): Promise { + return this.prefetchPackage(module, courseId, single, this.prefetchGlossary.bind(this)); + } + + /** + * Prefetch a glossary. + * + * @param {any} module The module object returned by WS. + * @param {number} courseId Course ID the module belongs to. + * @param {boolean} single True if we're downloading a single module, false if we're downloading a whole section. + * @param {string} siteId Site ID. + * @return {Promise} Promise resolved when done. + */ + protected prefetchGlossary(module: any, courseId: number, single: boolean, siteId: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + // Prefetch the glossary data. + return this.glossaryProvider.getGlossary(courseId, module.id, siteId).then((glossary) => { + const promises = []; + + glossary.browsemodes.forEach((mode) => { + switch (mode) { + case 'letter': // Always done. Look bellow. + break; + case 'cat': // Not implemented. + promises.push(this.glossaryProvider.fetchAllEntries(this.glossaryProvider.getEntriesByCategory, + [glossary.id, AddonModGlossaryProvider.SHOW_ALL_CATERGORIES], false, siteId)); + break; + case 'date': + promises.push(this.glossaryProvider.fetchAllEntries(this.glossaryProvider.getEntriesByDate, + [glossary.id, 'CREATION', 'DESC'], false, siteId)); + promises.push(this.glossaryProvider.fetchAllEntries(this.glossaryProvider.getEntriesByDate, + [glossary.id, 'UPDATE', 'DESC'], false, siteId)); + break; + case 'author': + promises.push(this.glossaryProvider.fetchAllEntries(this.glossaryProvider.getEntriesByAuthor, + [glossary.id, 'ALL', 'LASTNAME', 'ASC'], false, siteId)); + break; + default: + } + }); + + // Fetch all entries to get information from. + promises.push(this.glossaryProvider.fetchAllEntries(this.glossaryProvider.getEntriesByLetter, + [glossary.id, 'ALL'], false, siteId).then((entries) => { + const promises = []; + const userIds = []; + + // Fetch user avatars. + entries.forEach((entry) => { + // Fetch individual entries. + promises.push(this.glossaryProvider.getEntry(entry.id, siteId)); + + userIds.push(entry.userid); + }); + + // Prefetch user profiles. + promises.push(this.userProvider.prefetchProfiles(userIds, courseId, siteId)); + + const files = this.getFilesFromGlossaryAndEntries(module, glossary, entries); + promises.push(this.filepoolProvider.addFilesToQueue(siteId, files, this.component, module.id)); + + return Promise.all(promises); + })); + }); + } +} From 5f6fabc5318dc4883404bb9fec9b41dc4fcdf443 Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Fri, 25 May 2018 16:22:15 +0200 Subject: [PATCH 6/7] MOBILE-2342 core: Support ion-textarea in auto-rows directive --- src/directives/auto-rows.ts | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/directives/auto-rows.ts b/src/directives/auto-rows.ts index 39301b339..3e7a24291 100644 --- a/src/directives/auto-rows.ts +++ b/src/directives/auto-rows.ts @@ -22,17 +22,14 @@ import { Directive, ElementRef, HostListener, Output, EventEmitter } from '@angu * */ @Directive({ - selector: 'textarea[core-auto-rows]' + selector: 'textarea[core-auto-rows], ion-textarea[core-auto-rows]' }) export class CoreAutoRowsDirective { - protected element: HTMLTextAreaElement; protected height = 0; @Output() onResize: EventEmitter; // Emit when resizing the textarea. - constructor(element: ElementRef) { - this.element = element.nativeElement || element; - this.height = this.element.scrollHeight; + constructor(private element: ElementRef) { this.onResize = new EventEmitter(); } @@ -48,9 +45,9 @@ export class CoreAutoRowsDirective { } /** - * Resize after init. + * Resize after content. */ - ngAfterViewInit(): void { + ngAfterViewContent(): void { this.resize(); } @@ -59,13 +56,19 @@ export class CoreAutoRowsDirective { * @param {any} $event Event fired. */ protected resize($event?: any): void { + let nativeElement = this.element.nativeElement; + if (nativeElement.tagName == 'ION-TEXTAREA') { + // The first child of ion-textarea is the actual textarea element. + nativeElement = nativeElement.firstElementChild; + } + // Set height to 1px to force scroll height to calculate correctly. - this.element.style.height = '1px'; - this.element.style.height = this.element.scrollHeight + 'px'; + nativeElement.style.height = '1px'; + nativeElement.style.height = nativeElement.scrollHeight + 'px'; // Emit event when resizing. - if (this.height != this.element.scrollHeight) { - this.height = this.element.scrollHeight; + if (this.height != nativeElement.scrollHeight) { + this.height = nativeElement.scrollHeight; this.onResize.emit(); } } From 2fc339a055b6a4c6029a4d3fee6a46fbb9b91d47 Mon Sep 17 00:00:00 2001 From: Albert Gasset Date: Tue, 29 May 2018 11:19:26 +0200 Subject: [PATCH 7/7] MOBILE-2342 glossary: PR fixes --- .../mod/glossary/components/index/index.ts | 2 +- src/addon/mod/glossary/pages/entry/entry.html | 3 --- src/addon/mod/glossary/providers/glossary.ts | 5 +++-- src/addon/mod/glossary/providers/helper.ts | 2 +- .../glossary/providers/prefetch-handler.ts | 20 ------------------- src/addon/mod/glossary/providers/sync.ts | 8 ++++---- 6 files changed, 9 insertions(+), 31 deletions(-) diff --git a/src/addon/mod/glossary/components/index/index.ts b/src/addon/mod/glossary/components/index/index.ts index cb19029fa..618b642ca 100644 --- a/src/addon/mod/glossary/components/index/index.ts +++ b/src/addon/mod/glossary/components/index/index.ts @@ -229,7 +229,7 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity this.fetchArguments = [this.glossary.id, 'ALL', 'LASTNAME', 'ASC']; this.getDivider = (entry: any): string => entry.userfullname; this.showDivider = (entry: any, previous?: any): boolean => { - return previous === 'undefined' || entry.userid != previous.userid; + return typeof previous === 'undefined' || entry.userid != previous.userid; }; break; case 'cat_all': diff --git a/src/addon/mod/glossary/pages/entry/entry.html b/src/addon/mod/glossary/pages/entry/entry.html index 5bccd5f20..117741895 100644 --- a/src/addon/mod/glossary/pages/entry/entry.html +++ b/src/addon/mod/glossary/pages/entry/entry.html @@ -1,9 +1,6 @@ - - - diff --git a/src/addon/mod/glossary/providers/glossary.ts b/src/addon/mod/glossary/providers/glossary.ts index 02482f0e9..943e91532 100644 --- a/src/addon/mod/glossary/providers/glossary.ts +++ b/src/addon/mod/glossary/providers/glossary.ts @@ -609,7 +609,7 @@ export class AddonModGlossaryProvider { /** * Invalidate the prefetched content for a given glossary, except files. - * To invalidate files, use $mmaModGlossary#invalidateFiles. + * To invalidate files, use AddonModGlossaryProvider#invalidateFiles. * * @param {any} glossary The glossary object. * @param {boolean} [onlyEntriesList] If true, entries won't be invalidated. @@ -705,7 +705,8 @@ export class AddonModGlossaryProvider { * @param {string} definition Glossary entry concept definition. * @param {number} courseId Course ID of the glossary. * @param {any} [options] Array of options for the entry. - * @param {any} [attach] Attachments ID if sending online, result of $mmFileUploader#storeFilesToUpload otherwise. + * @param {any} [attach] Attachments ID if sending online, result of CoreFileUploaderProvider#storeFilesToUpload + * otherwise. * @param {number} [timeCreated] The time the entry was created. If not defined, current time. * @param {string} [siteId] Site ID. If not defined, current site. * @param {any} [discardEntry] The entry provided will be discarded if found. diff --git a/src/addon/mod/glossary/providers/helper.ts b/src/addon/mod/glossary/providers/helper.ts index 55a36960b..d4cf592b1 100644 --- a/src/addon/mod/glossary/providers/helper.ts +++ b/src/addon/mod/glossary/providers/helper.ts @@ -39,7 +39,7 @@ export class AddonModGlossaryHelperProvider { */ deleteStoredFiles(glossaryId: number, entryName: string, timeCreated: number, siteId?: string): Promise { return this.glossaryOffline.getEntryFolder(glossaryId, entryName, timeCreated, siteId).then((folderPath) => { - this.fileProvider.removeDir(folderPath).catch(() => { + return this.fileProvider.removeDir(folderPath).catch(() => { // Ignore any errors, CoreFileProvider.removeDir fails if folder doesn't exists. }); }); diff --git a/src/addon/mod/glossary/providers/prefetch-handler.ts b/src/addon/mod/glossary/providers/prefetch-handler.ts index 16e088851..dc3dcad35 100644 --- a/src/addon/mod/glossary/providers/prefetch-handler.ts +++ b/src/addon/mod/glossary/providers/prefetch-handler.ts @@ -16,7 +16,6 @@ import { Injectable, Injector } from '@angular/core'; import { CoreCourseModulePrefetchHandlerBase } from '@core/course/classes/module-prefetch-handler'; import { CoreUserProvider } from '@core/user/providers/user'; import { AddonModGlossaryProvider } from './glossary'; -import { CoreConstants } from '@core/constants'; /** * Handler to prefetch forums. @@ -34,25 +33,6 @@ export class AddonModGlossaryPrefetchHandler extends CoreCourseModulePrefetchHan super(injector); } - /** - * Return the status to show based on current status. E.g. a module might want to show outdated instead of downloaded. - * If not implemented, the original status will be returned. - * - * @param {any} module Module. - * @param {string} status The current status. - * @param {boolean} canCheck Whether the site allows checking for updates. - * @return {string} Status to display. - */ - determineStatus(module: any, status: string, canCheck: boolean): string { - if (!canCheck && status === CoreConstants.DOWNLOADED) { - /* Glossary are always marked as outdated if updates cannot be checked because we can't tell if there's something - new without having to call all the WebServices. */ - return CoreConstants.OUTDATED; - } else { - return status; - } - } - /** * Download the module. * diff --git a/src/addon/mod/glossary/providers/sync.ts b/src/addon/mod/glossary/providers/sync.ts index ea509a690..aa82f859a 100644 --- a/src/addon/mod/glossary/providers/sync.ts +++ b/src/addon/mod/glossary/providers/sync.ts @@ -49,7 +49,7 @@ export class AddonModGlossarySyncProvider extends CoreSyncBaseProvider { textUtils: CoreTextUtilsProvider, private uploaderProvider: CoreFileUploaderProvider, private utils: CoreUtilsProvider, - private glossaruProvider: AddonModGlossaryProvider, + private glossaryProvider: AddonModGlossaryProvider, private glossaryHelper: AddonModGlossaryHelperProvider, private glossaryOffline: AddonModGlossaryOfflineProvider) { @@ -181,7 +181,7 @@ export class AddonModGlossarySyncProvider extends CoreSyncBaseProvider { // First of all upload the attachments (if any). promise = this.uploadAttachments(glossaryId, data, siteId).then((itemId) => { // Now try to add the entry. - return this.glossaruProvider.addEntryOnline( + return this.glossaryProvider.addEntryOnline( glossaryId, data.concept, data.definition, data.options, itemId, siteId); }); @@ -213,8 +213,8 @@ export class AddonModGlossarySyncProvider extends CoreSyncBaseProvider { }).then(() => { if (result.updated && courseId) { // Data has been sent to server. Now invalidate the WS calls. - return this.glossaruProvider.getGlossaryById(courseId, glossaryId).then((glossary) => { - return this.glossaruProvider.invalidateGlossaryEntries(glossary, true); + return this.glossaryProvider.getGlossaryById(courseId, glossaryId).then((glossary) => { + return this.glossaryProvider.invalidateGlossaryEntries(glossary, true); }).catch(() => { // Ignore errors. });