diff --git a/src/addon/mod/label/label.module.ts b/src/addon/mod/label/label.module.ts index 15be0e3cc..951242b36 100644 --- a/src/addon/mod/label/label.module.ts +++ b/src/addon/mod/label/label.module.ts @@ -13,10 +13,13 @@ // limitations under the License. import { NgModule } from '@angular/core'; +import { AddonModLabelProvider } from './providers/label'; import { AddonModLabelModuleHandler } from './providers/module-handler'; import { AddonModLabelLinkHandler } from './providers/link-handler'; +import { AddonModLabelPrefetchHandler } from './providers/prefetch-handler'; import { CoreCourseModuleDelegate } from '@core/course/providers/module-delegate'; import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate'; +import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; @NgModule({ declarations: [ @@ -24,14 +27,18 @@ import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate' imports: [ ], providers: [ + AddonModLabelProvider, AddonModLabelModuleHandler, - AddonModLabelLinkHandler + AddonModLabelLinkHandler, + AddonModLabelPrefetchHandler ] }) export class AddonModLabelModule { constructor(moduleDelegate: CoreCourseModuleDelegate, moduleHandler: AddonModLabelModuleHandler, - contentLinksDelegate: CoreContentLinksDelegate, linkHandler: AddonModLabelLinkHandler) { + contentLinksDelegate: CoreContentLinksDelegate, linkHandler: AddonModLabelLinkHandler, + prefetchDelegate: CoreCourseModulePrefetchDelegate, prefetchHandler: AddonModLabelPrefetchHandler) { moduleDelegate.registerHandler(moduleHandler); contentLinksDelegate.registerHandler(linkHandler); + prefetchDelegate.registerHandler(prefetchHandler); } } diff --git a/src/addon/mod/label/providers/label.ts b/src/addon/mod/label/providers/label.ts new file mode 100644 index 000000000..b40b11ac9 --- /dev/null +++ b/src/addon/mod/label/providers/label.ts @@ -0,0 +1,171 @@ +// (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 { CoreSitesProvider } from '@providers/sites'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreFilepoolProvider } from '@providers/filepool'; +import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; + +/** + * Service that provides some features for labels. + */ +@Injectable() +export class AddonModLabelProvider { + static COMPONENT = 'mmaModLabel'; + + protected ROOT_CACHE_KEY = 'mmaModLabel:'; + + constructor(private sitesProvider: CoreSitesProvider, private filepoolProvider: CoreFilepoolProvider, + private utils: CoreUtilsProvider) {} + + /** + * Get cache key for label data WS calls. + * + * @param {number} courseId Course ID. + * @return {string} Cache key. + */ + protected getLabelDataCacheKey(courseId: number): string { + return this.ROOT_CACHE_KEY + 'label:' + courseId; + } + + /** + * Get a label with key=value. If more than one is found, only the first will be returned. + * + * @param {number} courseId Course ID. + * @param {string} key Name of the property to check. + * @param {any} value Value to search. + * @param {boolean} [forceCache] True to always get the value from cache, false otherwise. + * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). + * @param {string} [siteId] Site ID. If not provided, current site. + * @return {Promise} Promise resolved when the label is retrieved. + */ + protected getLabelByField(courseId: number, key: string, value: any, forceCache?: boolean, ignoreCache?: boolean, + siteId?: string): Promise { + + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + courseids: [courseId] + }, + preSets: CoreSiteWSPreSets = { + cacheKey: this.getLabelDataCacheKey(courseId) + }; + + if (forceCache) { + preSets.omitExpires = true; + } else if (ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } + + return site.read('mod_label_get_labels_by_courses', params, preSets).then((response) => { + if (response && response.labels) { + const currentLabel = response.labels.find((label) => label[key] == value); + if (currentLabel) { + return currentLabel; + } + } + + return Promise.reject(null); + }); + }); + } + + /** + * Get a label by course module ID. + * + * @param {number} courseId Course ID. + * @param {number} cmId Course module ID. + * @param {boolean} [forceCache] True to always get the value from cache, false otherwise. + * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the label is retrieved. + */ + getLabel(courseId: number, cmId: number, forceCache?: boolean, ignoreCache?: boolean, siteId?: string): Promise { + return this.getLabelByField(courseId, 'coursemodule', cmId, forceCache, ignoreCache, siteId); + } + + /** + * Get a label by ID. + * + * @param {number} courseId Course ID. + * @param {number} labelId Label ID. + * @param {boolean} [forceCache] True to always get the value from cache, false otherwise. + * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the label is retrieved. + */ + getLabelById(courseId: number, labelId: number, forceCache?: boolean, ignoreCache?: boolean, siteId?: string): Promise { + return this.getLabelByField(courseId, 'id', labelId, forceCache, ignoreCache, siteId); + } + + /** + * Invalidate label data. + * + * @param {number} courseId Course ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateLabelData(courseId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(null).then((site) => { + return site.invalidateWsCacheForKey(this.getLabelDataCacheKey(courseId)); + }); + } + + /** + * Invalidate the prefetched content. + * + * @param {number} moduleId The module ID. + * @param {number} courseId Course ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when data is invalidated. + */ + invalidateContent(moduleId: number, courseId: number, siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + const promises = []; + + promises.push(this.invalidateLabelData(courseId, siteId)); + + promises.push(this.filepoolProvider.invalidateFilesByComponent(siteId, AddonModLabelProvider.COMPONENT, moduleId, true)); + + return this.utils.allPromises(promises); + } + + /** + * Check if the site has the WS to get label data. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with boolean: whether it's available. + * @since 3.3 + */ + isGetLabelAvailable(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.wsAvailable('mod_label_get_labels_by_courses'); + }); + } + + /** + * Check if the site has the WS to get label data. + * + * @param {CoreSite} [site] Site. If not defined, current site. + * @return {boolean} Whether it's available. + * @since 3.3 + */ + isGetLabelAvailableForSite(site?: CoreSite): boolean { + site = site || this.sitesProvider.getCurrentSite(); + + return site.wsAvailable('mod_label_get_labels_by_courses'); + } +} diff --git a/src/addon/mod/label/providers/prefetch-handler.ts b/src/addon/mod/label/providers/prefetch-handler.ts new file mode 100644 index 000000000..3f86dec31 --- /dev/null +++ b/src/addon/mod/label/providers/prefetch-handler.ts @@ -0,0 +1,92 @@ +// (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 { CoreAppProvider } from '@providers/app'; +import { CoreFilepoolProvider } from '@providers/filepool'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreCourseProvider } from '@core/course/providers/course'; +import { CoreCourseResourcePrefetchHandlerBase } from '@core/course/classes/resource-prefetch-handler'; +import { AddonModLabelProvider } from './label'; + +/** + * Handler to prefetch labels. + */ +@Injectable() +export class AddonModLabelPrefetchHandler extends CoreCourseResourcePrefetchHandlerBase { + name = 'AddonModLabel'; + modName = 'label'; + component = AddonModLabelProvider.COMPONENT; + updatesNames = /^.*files$/; + skipListStatus = true; + + constructor(translate: TranslateService, appProvider: CoreAppProvider, utils: CoreUtilsProvider, + courseProvider: CoreCourseProvider, filepoolProvider: CoreFilepoolProvider, sitesProvider: CoreSitesProvider, + domUtils: CoreDomUtilsProvider, protected labelProvider: AddonModLabelProvider) { + + super(translate, appProvider, utils, courseProvider, filepoolProvider, sitesProvider, domUtils); + } + + /** + * Returns module intro files. + * + * @param {any} module The module object returned by WS. + * @param {number} courseId Course ID. + * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). + * @return {Promise} Promise resolved with list of intro files. + */ + getIntroFiles(module: any, courseId: number, ignoreCache?: boolean): Promise { + let promise; + + if (this.labelProvider.isGetLabelAvailableForSite()) { + promise = this.labelProvider.getLabel(courseId, module.id, false, ignoreCache); + } else { + promise = Promise.resolve(); + } + + return promise.then((label) => { + return this.getIntroFilesFromInstance(module, label); + }); + } + + /** + * Invalidate the prefetched content. + * + * @param {number} moduleId The module ID. + * @param {number} courseId Course ID the module belongs to. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateContent(moduleId: number, courseId: number): Promise { + return this.labelProvider.invalidateContent(moduleId, courseId); + } + + /** + * Invalidate WS calls needed to determine module status. + * + * @param {any} module Module. + * @param {number} courseId Course ID the module belongs to. + * @return {Promise} Promise resolved when invalidated. + */ + invalidateModule(module: any, courseId: number): Promise { + const promises = []; + + promises.push(this.labelProvider.invalidateLabelData(courseId)); + promises.push(this.courseProvider.invalidateModule(module.id)); + + return this.utils.allPromises(promises); + } +} diff --git a/src/core/course/classes/module-prefetch-handler.ts b/src/core/course/classes/module-prefetch-handler.ts index 49ae7e6c4..e079055ed 100644 --- a/src/core/course/classes/module-prefetch-handler.ts +++ b/src/core/course/classes/module-prefetch-handler.ts @@ -52,6 +52,13 @@ export class CoreCourseModulePrefetchHandlerBase implements CoreCourseModulePref */ updatesNames = /^.*files$/; + /** + * If true, this module will be ignored when determining the status of a list of modules. The module will + * still be downloaded when downloading the section/course, it only affects whether the button should be displayed. + * @type {boolean} + */ + skipListStatus = false; + /** * List of download promises to prevent downloading the module twice at the same time. * @type {{[s: string]: {[s: string]: Promise}}} @@ -167,9 +174,10 @@ export class CoreCourseModulePrefetchHandlerBase implements CoreCourseModulePref * * @param {any} module The module object returned by WS. * @param {number} courseId Course ID. + * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). * @return {Promise} Promise resolved with list of intro files. */ - getIntroFiles(module: any, courseId: number): Promise { + getIntroFiles(module: any, courseId: number, ignoreCache?: boolean): Promise { return Promise.resolve(this.getIntroFilesFromInstance(module)); } diff --git a/src/core/course/classes/resource-prefetch-handler.ts b/src/core/course/classes/resource-prefetch-handler.ts index f2e51246b..f7e7386d3 100644 --- a/src/core/course/classes/resource-prefetch-handler.ts +++ b/src/core/course/classes/resource-prefetch-handler.ts @@ -67,7 +67,7 @@ export class CoreCourseResourcePrefetchHandlerBase extends CoreCourseModulePrefe return this.loadContents(module, courseId, true); }).then(() => { // Get the intro files. - return this.getIntroFiles(module, courseId); + return this.getIntroFiles(module, courseId, true); }).then((introFiles) => { const downloadFn = prefetch ? this.filepoolProvider.prefetchPackage.bind(this.filepoolProvider) : this.filepoolProvider.downloadPackage.bind(this.filepoolProvider), diff --git a/src/core/course/providers/course.ts b/src/core/course/providers/course.ts index f35b76067..e16318b09 100644 --- a/src/core/course/providers/course.ts +++ b/src/core/course/providers/course.ts @@ -43,6 +43,8 @@ export class CoreCourseProvider { static COMPLETION_COMPLETE_PASS = 2; static COMPLETION_COMPLETE_FAIL = 3; + static COMPONENT = 'CoreCourse'; + protected ROOT_CACHE_KEY = 'mmCourse:'; // Variables for database. diff --git a/src/core/course/providers/helper.ts b/src/core/course/providers/helper.ts index 6f05e157f..3a6b746ac 100644 --- a/src/core/course/providers/helper.ts +++ b/src/core/course/providers/helper.ts @@ -192,7 +192,7 @@ export class CoreCourseHelperProvider { } // Get the status of this section. - return this.prefetchDelegate.getModulesStatus(section.modules, courseId, section.id, refresh).then((result) => { + return this.prefetchDelegate.getModulesStatus(section.modules, courseId, section.id, refresh, true).then((result) => { // Check if it's being downloaded. const downloadId = this.getSectionDownloadId(section); if (this.prefetchDelegate.isBeingDownloaded(downloadId)) { @@ -401,11 +401,15 @@ export class CoreCourseHelperProvider { * @return {Promise} Promise resolved if the user confirms or there's no need to confirm. */ confirmDownloadSizeSection(courseId: number, section?: any, sections?: any[], alwaysConfirm?: boolean): Promise { - let sizePromise; + let sizePromise, + haveEmbeddedFiles = false; // Calculate the size of the download. if (section && section.id != CoreCourseProvider.ALL_SECTIONS_ID) { sizePromise = this.prefetchDelegate.getDownloadSize(section.modules, courseId); + + // Check if the section has embedded files in the description. + haveEmbeddedFiles = this.domUtils.extractDownloadableFilesFromHtml(section.summary).length > 0; } else { const promises = [], results = { @@ -419,6 +423,11 @@ export class CoreCourseHelperProvider { results.total = results.total && sectionSize.total; results.size += sectionSize.size; })); + + // Check if the section has embedded files in the description. + if (!haveEmbeddedFiles && this.domUtils.extractDownloadableFilesFromHtml(s.summary).length > 0) { + haveEmbeddedFiles = true; + } } }); @@ -428,6 +437,10 @@ export class CoreCourseHelperProvider { } return sizePromise.then((size) => { + if (haveEmbeddedFiles) { + size.total = false; + } + // Show confirm modal if needed. return this.domUtils.confirmDownloadSize(size, undefined, undefined, undefined, undefined, alwaysConfirm); }); @@ -1272,10 +1285,12 @@ export class CoreCourseHelperProvider { return Promise.resolve(); } + const promises = []; + section.isDownloading = true; // Validate the section needs to be downloaded and calculate amount of modules that need to be downloaded. - return this.prefetchDelegate.getModulesStatus(section.modules, courseId, section.id).then((result) => { + promises.push(this.prefetchDelegate.getModulesStatus(section.modules, courseId, section.id).then((result) => { if (result.status == CoreConstants.DOWNLOADED || result.status == CoreConstants.NOT_DOWNLOADABLE) { // Section is downloaded or not downloadable, nothing to do. return; @@ -1286,7 +1301,18 @@ export class CoreCourseHelperProvider { section.isDownloading = false; return Promise.reject(error); - }); + })); + + // Download the files in the section description. + const introFiles = this.domUtils.extractDownloadableFilesFromHtmlAsFakeFileObjects(section.summary), + siteId = this.sitesProvider.getCurrentSiteId(); + + promises.push(this.filepoolProvider.addFilesToQueue(siteId, introFiles, CoreCourseProvider.COMPONENT, courseId) + .catch(() => { + // Ignore errors. + })); + + return Promise.all(promises); } /** diff --git a/src/core/course/providers/module-prefetch-delegate.ts b/src/core/course/providers/module-prefetch-delegate.ts index c381ac64e..5d43db704 100644 --- a/src/core/course/providers/module-prefetch-delegate.ts +++ b/src/core/course/providers/module-prefetch-delegate.ts @@ -56,6 +56,12 @@ export type CoreCourseModulesProgressFunction = (data: CoreCourseModulesProgress * Interface that all course prefetch handlers must implement. */ export interface CoreCourseModulePrefetchHandler extends CoreDelegateHandler { + /** + * Name of the handler. + * @type {string} + */ + name: string; + /** * Name of the module. It should match the "modname" of the module returned in core_course_get_contents. * @type {string} @@ -75,6 +81,13 @@ export interface CoreCourseModulePrefetchHandler extends CoreDelegateHandler { */ updatesNames?: RegExp; + /** + * If true, this module will be treated as not downloadable when determining the status of a list of modules. The module will + * still be downloaded when downloading the section/course, it only affects whether the button should be displayed. + * @type {boolean} + */ + skipListStatus: boolean; + /** * Get the download size of a module. * @@ -769,6 +782,7 @@ export class CoreCourseModulePrefetchDelegate extends CoreDelegate { * @param {number} courseId Course ID the modules belong to. * @param {number} [sectionId] ID of the section the modules belong to. * @param {boolean} [refresh] True if it should always check the DB (slower). + * @param {boolean} [onlyToDisplay] True if the status will only be used to determine which button should be displayed. * @return {Promise} Promise resolved with an object with the following properties: * - status (string) Status of the module. * - total (number) Number of modules. @@ -777,7 +791,7 @@ export class CoreCourseModulePrefetchDelegate extends CoreDelegate { * - CoreConstants.DOWNLOADING (any[]) Modules with state DOWNLOADING. * - CoreConstants.OUTDATED (any[]) Modules with state OUTDATED. */ - getModulesStatus(modules: any[], courseId: number, sectionId?: number, refresh?: boolean): any { + getModulesStatus(modules: any[], courseId: number, sectionId?: number, refresh?: boolean, onlyToDisplay?: boolean): any { const promises = [], result: any = { total: 0 @@ -800,6 +814,11 @@ export class CoreCourseModulePrefetchDelegate extends CoreDelegate { // Check if the module has a prefetch handler. const handler = this.getPrefetchHandlerFor(module); if (handler) { + if (onlyToDisplay && handler.skipListStatus) { + // Skip this module. + return; + } + const packageId = this.filepoolProvider.getPackageId(handler.component, module.id); promises.push(this.getModuleStatus(module, courseId, updates, refresh).then((modStatus) => { diff --git a/src/directives/format-text.ts b/src/directives/format-text.ts index 08b4876e8..d840c8a84 100644 --- a/src/directives/format-text.ts +++ b/src/directives/format-text.ts @@ -98,6 +98,10 @@ export class CoreFormatTextDirective implements OnChanges { extContent.component = this.component; extContent.componentId = this.componentId; extContent.siteId = this.siteId; + extContent.src = element.getAttribute('src'); + extContent.href = element.getAttribute('href'); + extContent.targetSrc = element.getAttribute('target-src'); + extContent.poster = element.getAttribute('poster'); extContent.ngAfterViewInit(); }