// (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 { CoreEventsProvider } from '@providers/events'; import { CoreLoggerProvider } from '@providers/logger'; import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreSiteWSPreSets, CoreSite } from '@classes/site'; import { CoreConstants } from '../../constants'; import { CoreCourseOfflineProvider } from './course-offline'; /** * Service that provides some features regarding a course. */ @Injectable() export class CoreCourseProvider { static ALL_SECTIONS_ID = -2; static STEALTH_MODULES_SECTION_ID = -1; static ACCESS_GUEST = 'courses_access_guest'; static ACCESS_DEFAULT = 'courses_access_default'; static COMPLETION_TRACKING_NONE = 0; static COMPLETION_TRACKING_MANUAL = 1; static COMPLETION_TRACKING_AUTOMATIC = 2; static COMPLETION_INCOMPLETE = 0; static COMPLETION_COMPLETE = 1; static COMPLETION_COMPLETE_PASS = 2; static COMPLETION_COMPLETE_FAIL = 3; static COMPONENT = 'CoreCourse'; protected ROOT_CACHE_KEY = 'mmCourse:'; // Variables for database. protected COURSE_STATUS_TABLE = 'course_status'; protected siteSchema: CoreSiteSchema = { name: 'CoreCourseProvider', version: 1, tables: [ { name: this.COURSE_STATUS_TABLE, columns: [ { name: 'id', type: 'INTEGER', primaryKey: true }, { name: 'status', type: 'TEXT', notNull: true }, { name: 'previous', type: 'TEXT' }, { name: 'updated', type: 'INTEGER' }, { name: 'downloadTime', type: 'INTEGER' }, { name: 'previousDownloadTime', type: 'INTEGER' } ] } ] }; protected logger; protected CORE_MODULES = [ 'assign', 'assignment', 'book', 'chat', 'choice', 'data', 'database', 'date', 'external-tool', 'feedback', 'file', 'folder', 'forum', 'glossary', 'ims', 'imscp', 'label', 'lesson', 'lti', 'page', 'quiz', 'resource', 'scorm', 'survey', 'url', 'wiki', 'workshop' ]; constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private eventsProvider: CoreEventsProvider, private utils: CoreUtilsProvider, private timeUtils: CoreTimeUtilsProvider, private translate: TranslateService, private courseOffline: CoreCourseOfflineProvider, private appProvider: CoreAppProvider) { this.logger = logger.getInstance('CoreCourseProvider'); this.sitesProvider.registerSiteSchema(this.siteSchema); } /** * Check if the get course blocks WS is available in current site. * * @return {boolean} Whether it's available. * @since 3.3 */ canGetCourseBlocks(): boolean { return this.sitesProvider.wsAvailableInCurrentSite('core_block_get_course_blocks'); } /** * Check whether the site supports requesting stealth modules. * * @param {CoreSite} [site] Site. If not defined, current site. * @return {boolean} Whether the site supports requesting stealth modules. * @since 3.4.6, 3.5.3, 3.6 */ canRequestStealthModules(site?: CoreSite): boolean { site = site || this.sitesProvider.getCurrentSite(); return site.isVersionGreaterEqualThan(['3.4.6', '3.5.3']); } /** * Check if module completion could have changed. If it could have, trigger event. This function must be used, * for example, after calling a "module_view" WS since it can change the module completion. * * @param {number} courseId Course ID. * @param {any} completion Completion status of the module. */ checkModuleCompletion(courseId: number, completion: any): void { if (completion && completion.tracking === 2 && completion.state === 0) { this.invalidateSections(courseId).finally(() => { this.eventsProvider.trigger(CoreEventsProvider.COMPLETION_MODULE_VIEWED, { courseId: courseId }); }); } } /** * Clear all courses status in a site. * * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved when all status are cleared. */ clearAllCoursesStatus(siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { this.logger.debug('Clear all course status for site ' + site.id); return site.getDb().deleteRecords(this.COURSE_STATUS_TABLE).then(() => { this.triggerCourseStatusChanged(-1, CoreConstants.NOT_DOWNLOADED, site.id); }); }); } /** * Get completion status of all the activities in a course for a certain user. * * @param {number} courseId Course ID. * @param {string} [siteId] Site ID. If not defined, current site. * @param {number} [userId] User ID. If not defined, current user. * @param {boolean} [forceCache] True if it should return cached data. Has priority over ignoreCache. * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). * @param {boolean} [includeOffline=true] True if it should load offline data in the completion status. * @return {Promise} Promise resolved with the completion statuses: object where the key is module ID. */ getActivitiesCompletionStatus(courseId: number, siteId?: string, userId?: number, forceCache: boolean = false, ignoreCache: boolean = false, includeOffline: boolean = true): Promise { return this.sitesProvider.getSite(siteId).then((site) => { userId = userId || site.getUserId(); this.logger.debug(`Getting completion status for user ${userId} in course ${courseId}`); const params = { courseid: courseId, userid: userId }, preSets: CoreSiteWSPreSets = { cacheKey: this.getActivitiesCompletionCacheKey(courseId, userId) }; if (forceCache) { preSets.omitExpires = true; } else if (ignoreCache) { preSets.getFromCache = false; preSets.emergencyCache = false; } return site.read('core_completion_get_activities_completion_status', params, preSets).then((data) => { if (data && data.statuses) { return this.utils.arrayToObject(data.statuses, 'cmid'); } return Promise.reject(null); }).then((completionStatus) => { if (!includeOffline) { return completionStatus; } // Now get the offline completion (if any). return this.courseOffline.getCourseManualCompletions(courseId, site.id).then((offlineCompletions) => { offlineCompletions.forEach((offlineCompletion) => { if (offlineCompletion && typeof completionStatus[offlineCompletion.cmid] != 'undefined') { const onlineCompletion = completionStatus[offlineCompletion.cmid]; // If the activity uses manual completion, override the value with the offline one. if (onlineCompletion.tracking === 1) { onlineCompletion.state = offlineCompletion.completed; onlineCompletion.offline = true; } } }); return completionStatus; }).catch(() => { // Ignore errors. return completionStatus; }); }); }); } /** * Get cache key for activities completion WS calls. * * @param {number} courseId Course ID. * @param {number} userId User ID. * @return {string} Cache key. */ protected getActivitiesCompletionCacheKey(courseId: number, userId: number): string { return this.ROOT_CACHE_KEY + 'activitiescompletion:' + courseId + ':' + userId; } /** * Get course blocks. * * @param {number} courseId Course ID. * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved with the list of blocks. * @since 3.3 */ getCourseBlocks(courseId: number, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params = { courseid: courseId }, preSets: CoreSiteWSPreSets = { cacheKey: this.getCourseBlocksCacheKey(courseId) }; return site.read('core_block_get_course_blocks', params, preSets).then((result) => { return result.blocks || []; }); }); } /** * Get cache key for course blocks WS calls. * * @param {number} courseId Course ID. * @return {string} Cache key. */ protected getCourseBlocksCacheKey(courseId: number): string { return this.ROOT_CACHE_KEY + 'courseblocks:' + courseId; } /** * Get the data stored for a course. * * @param {number} courseId Course ID. * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved with the data. */ getCourseStatusData(courseId: number, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { return site.getDb().getRecord(this.COURSE_STATUS_TABLE, { id: courseId }).then((entry) => { if (!entry) { return Promise.reject(null); } return entry; }); }); } /** * Get a course status. * * @param {number} courseId Course ID. * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved with the status. */ getCourseStatus(courseId: number, siteId?: string): Promise { return this.getCourseStatusData(courseId, siteId).then((entry) => { return entry.status || CoreConstants.NOT_DOWNLOADED; }).catch(() => { return CoreConstants.NOT_DOWNLOADED; }); } /** * Get a module from Moodle. * * @param {number} moduleId The module ID. * @param {number} [courseId] The course ID. Recommended to speed up the process and minimize data usage. * @param {number} [sectionId] The section ID. * @param {boolean} [preferCache] True if shouldn't call WS if data is cached, 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. * @param {string} [modName] If set, the app will retrieve all modules of this type with a single WS call. This reduces the * number of WS calls, but it isn't recommended for modules that can return a lot of contents. * @return {Promise} Promise resolved with the module. */ getModule(moduleId: number, courseId?: number, sectionId?: number, preferCache?: boolean, ignoreCache?: boolean, siteId?: string, modName?: string): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); // Helper function to do the WS request without processing the result. const doRequest = (site: CoreSite, moduleId: number, modName: string, includeStealth: boolean, preferCache: boolean): Promise => { const params: any = { courseid: courseId, options: [] }; const preSets: CoreSiteWSPreSets = { omitExpires: preferCache }; if (includeStealth) { params.options.push({ name: 'includestealthmodules', value: 1 }); } // If modName is set, retrieve all modules of that type. Otherwise get only the module. if (modName) { params.options.push({ name: 'modname', value: modName }); preSets.cacheKey = this.getModuleByModNameCacheKey(modName); } else { params.options.push({ name: 'cmid', value: moduleId }); preSets.cacheKey = this.getModuleCacheKey(moduleId); } if (!preferCache && ignoreCache) { preSets.getFromCache = false; preSets.emergencyCache = false; } return site.read('core_course_get_contents', params, preSets).catch(() => { // The module might still be cached by a request with different parameters. if (!ignoreCache && !this.appProvider.isOnline()) { if (includeStealth) { // Older versions didn't include the includestealthmodules option. return doRequest(site, moduleId, modName, false, true); } else if (modName) { // Falback to the request for the given moduleId only. return doRequest(site, moduleId, undefined, this.canRequestStealthModules(site), true); } } return Promise.reject(null); }); }; let promise; if (!courseId) { // No courseId passed, try to retrieve it. promise = this.getModuleBasicInfo(moduleId, siteId).then((module) => { courseId = module.course; }); } else { promise = Promise.resolve(); } return promise.then(() => { return this.sitesProvider.getSite(siteId); }).then((site) => { // We have courseId, we can use core_course_get_contents for compatibility. this.logger.debug(`Getting module ${moduleId} in course ${courseId}`); return doRequest(site, moduleId, modName, this.canRequestStealthModules(site), preferCache); }).catch(() => { // Error getting the module. Try to get all contents (without filtering by module). const preSets: CoreSiteWSPreSets = { omitExpires: preferCache }; if (!preferCache && ignoreCache) { preSets.getFromCache = false; preSets.emergencyCache = false; } return this.getSections(courseId, false, false, preSets, siteId); }).then((sections) => { for (let i = 0; i < sections.length; i++) { const section = sections[i]; if (sectionId != null && !isNaN(sectionId) && section.id != CoreCourseProvider.STEALTH_MODULES_SECTION_ID && sectionId != section.id) { continue; } for (let j = 0; j < section.modules.length; j++) { const module = section.modules[j]; if (module.id == moduleId) { module.course = courseId; return module; } } } return Promise.reject(null); }); } /** * Gets a module basic info by module ID. * * @param {number} moduleId Module ID. * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved with the module's info. */ getModuleBasicInfo(moduleId: number, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params = { cmid: moduleId }, preSets = { cacheKey: this.getModuleCacheKey(moduleId) }; return site.read('core_course_get_course_module', params, preSets).then((response) => { if (response.warnings && response.warnings.length) { return Promise.reject(response.warnings[0]); } else if (response.cm) { return response.cm; } return Promise.reject(null); }); }); } /** * Gets a module basic grade info by module ID. * * @param {number} moduleId Module ID. * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved with the module's grade info. */ getModuleBasicGradeInfo(moduleId: number, siteId?: string): Promise { return this.getModuleBasicInfo(moduleId, siteId).then((info) => { const grade = { advancedgrading: info.advancedgrading || false, grade: info.grade || false, gradecat: info.gradecat || false, gradepass: info.gradepass || false, outcomes: info.outcomes || false, scale: info.scale || false }; if (grade.grade !== false || grade.advancedgrading !== false || grade.outcomes !== false) { return grade; } return false; }); } /** * Gets a module basic info by instance. * * @param {number} id Instance ID. * @param {string} module Name of the module. E.g. 'glossary'. * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved with the module's info. */ getModuleBasicInfoByInstance(id: number, module: string, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params = { instance: id, module: module }, preSets = { cacheKey: this.getModuleBasicInfoByInstanceCacheKey(id, module) }; return site.read('core_course_get_course_module_by_instance', params, preSets).then((response) => { if (response.warnings && response.warnings.length) { return Promise.reject(response.warnings[0]); } else if (response.cm) { return response.cm; } return Promise.reject(null); }); }); } /** * Get cache key for get module by instance WS calls. * * @param {number} id Instance ID. * @param {string} module Name of the module. E.g. 'glossary'. * @return {string} Cache key. */ protected getModuleBasicInfoByInstanceCacheKey(id: number, module: string): string { return this.ROOT_CACHE_KEY + 'moduleByInstance:' + module + ':' + id; } /** * Get cache key for module WS calls. * * @param {number} moduleId Module ID. * @return {string} Cache key. */ protected getModuleCacheKey(moduleId: number): string { return this.ROOT_CACHE_KEY + 'module:' + moduleId; } /** * Get cache key for module by modname WS calls. * * @param {string} modName Name of the module. * @return {string} Cache key. */ protected getModuleByModNameCacheKey(modName: string): string { return this.ROOT_CACHE_KEY + 'module:modName:' + modName; } /** * Returns the source to a module icon. * * @param {string} moduleName The module name. * @param {string} [modicon] The mod icon string to use in case we are not using a core activity. * @return {string} The IMG src. */ getModuleIconSrc(moduleName: string, modicon?: string): string { // @TODO: Check modicon url theme to apply other theme icons. // Use default icon on core themes. if (this.CORE_MODULES.indexOf(moduleName) < 0) { if (modicon) { return modicon; } moduleName = 'external-tool'; } return 'assets/img/mod/' + moduleName + '.svg'; } /** * Get the section ID a module belongs to. * * @param {number} moduleId The module ID. * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved with the section ID. */ getModuleSectionId(moduleId: number, siteId?: string): Promise { // Try to get the section using getModuleBasicInfo. return this.getModuleBasicInfo(moduleId, siteId).then((module) => { return module.section; }); } /** * Return a specific section. * * @param {number} courseId The course ID. * @param {number} sectionId The section ID. * @param {boolean} [excludeModules] Do not return modules, return only the sections structure. * @param {boolean} [excludeContents] Do not return module contents (i.e: files inside a resource). * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved with the section. */ getSection(courseId: number, sectionId?: number, excludeModules?: boolean, excludeContents?: boolean, siteId?: string) : Promise { if (sectionId < 0) { return Promise.reject('Invalid section ID'); } return this.getSections(courseId, excludeModules, excludeContents, undefined, siteId).then((sections) => { for (let i = 0; i < sections.length; i++) { if (sections[i].id == sectionId) { return sections[i]; } } return Promise.reject('Unkown section'); }); } /** * Get the course sections. * * @param {number} courseId The course ID. * @param {boolean} [excludeModules] Do not return modules, return only the sections structure. * @param {boolean} [excludeContents] Do not return module contents (i.e: files inside a resource). * @param {CoreSiteWSPreSets} [preSets] Presets to use. * @param {string} [siteId] Site ID. If not defined, current site. * @param {boolean} [includeStealthModules] Whether to include stealth modules. Defaults to true. * @return {Promise} The reject contains the error message, else contains the sections. */ getSections(courseId?: number, excludeModules?: boolean, excludeContents?: boolean, preSets?: CoreSiteWSPreSets, siteId?: string, includeStealthModules: boolean = true): Promise { return this.sitesProvider.getSite(siteId).then((site) => { preSets = preSets || {}; preSets.cacheKey = this.getSectionsCacheKey(courseId); const params = { courseid: courseId, options: [ { name: 'excludemodules', value: excludeModules ? 1 : 0 }, { name: 'excludecontents', value: excludeContents ? 1 : 0 } ] }; if (this.canRequestStealthModules(site)) { params.options.push({ name: 'includestealthmodules', value: includeStealthModules ? 1 : 0 }); } return site.read('core_course_get_contents', params, preSets).catch(() => { // Error getting the data, it could fail because we added a new parameter and the call isn't cached. // Retry without the new parameter and forcing cache. preSets.omitExpires = true; params.options.splice(-1, 1); return site.read('core_course_get_contents', params, preSets); }).then((sections) => { const siteHomeId = site.getSiteHomeId(); let showSections = true; if (courseId == siteHomeId) { showSections = site.getStoredConfig('numsections'); } if (typeof showSections != 'undefined' && !showSections && sections.length > 0) { // Get only the last section (Main menu block section). sections.pop(); } return sections; }); }); } /** * Get cache key for section WS call. * * @param {number} courseId Course ID. * @return {string} Cache key. */ protected getSectionsCacheKey(courseId: number): string { return this.ROOT_CACHE_KEY + 'sections:' + courseId; } /** * Given a list of sections, returns the list of modules in the sections. * * @param {any[]} sections Sections. * @return {any[]} Modules. */ getSectionsModules(sections: any[]): any[] { if (!sections || !sections.length) { return []; } let modules = []; sections.forEach((section) => { if (section.modules) { modules = modules.concat(section.modules); } }); return modules; } /** * Invalidates course blocks WS call. * * @param {number} courseId Course ID. * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved when the data is invalidated. */ invalidateCourseBlocks(courseId: number, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { return site.invalidateWsCacheForKey(this.getCourseBlocksCacheKey(courseId)); }); } /** * Invalidates module WS call. * * @param {number} moduleId Module ID. * @param {string} [siteId] Site ID. If not defined, current site. * @param {string} [modName] Module name. E.g. 'label', 'url', ... * @return {Promise} Promise resolved when the data is invalidated. */ invalidateModule(moduleId: number, siteId?: string, modName?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const promises = []; if (modName) { promises.push(site.invalidateWsCacheForKey(this.getModuleByModNameCacheKey(modName))); } promises.push(site.invalidateWsCacheForKey(this.getModuleCacheKey(moduleId))); return Promise.all(promises); }); } /** * Invalidates module WS call. * * @param {number} id Instance ID. * @param {string} module Name of the module. E.g. 'glossary'. * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved when the data is invalidated. */ invalidateModuleByInstance(id: number, module: string, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { return site.invalidateWsCacheForKey(this.getModuleBasicInfoByInstanceCacheKey(id, module)); }); } /** * Invalidates sections WS call. * * @param {number} courseId Course ID. * @param {string} [siteId] Site ID. If not defined, current site. * @param {number} [userId] User ID. If not defined, current user. * @return {Promise} Promise resolved when the data is invalidated. */ invalidateSections(courseId: number, siteId?: string, userId?: number): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const promises = [], siteHomeId = site.getSiteHomeId(); userId = userId || site.getUserId(); promises.push(site.invalidateWsCacheForKey(this.getSectionsCacheKey(courseId))); promises.push(site.invalidateWsCacheForKey(this.getActivitiesCompletionCacheKey(courseId, userId))); if (courseId == siteHomeId) { promises.push(site.invalidateConfig()); } return Promise.all(promises); }); } /** * Load module contents into module.contents if they aren't loaded already. * * @param {any} module Module to load the contents. * @param {number} [courseId] The course ID. Recommended to speed up the process and minimize data usage. * @param {number} [sectionId] The section ID. * @param {boolean} [preferCache] True if shouldn't call WS if data is cached, 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. * @param {string} [modName] If set, the app will retrieve all modules of this type with a single WS call. This reduces the * number of WS calls, but it isn't recommended for modules that can return a lot of contents. * @return {Promise} Promise resolved when loaded. */ loadModuleContents(module: any, courseId?: number, sectionId?: number, preferCache?: boolean, ignoreCache?: boolean, siteId?: string, modName?: string): Promise { if (!ignoreCache && module.contents && module.contents.length) { // Already loaded. return Promise.resolve(); } return this.getModule(module.id, courseId, sectionId, preferCache, ignoreCache, siteId, modName).then((mod) => { module.contents = mod.contents; }); } /** * Report a course and section as being viewed. * * @param {number} courseId Course ID. * @param {number} [sectionNumber] Section number. * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved when the WS call is successful. */ logView(courseId: number, sectionNumber?: number, siteId?: string): Promise { const params: any = { courseid: courseId }; if (typeof sectionNumber != 'undefined') { params.sectionnumber = sectionNumber; } return this.sitesProvider.getSite(siteId).then((site) => { return site.write('core_course_view_course', params).then((response) => { if (!response.status) { return Promise.reject(null); } }); }); } /** * Offline version for manually marking a module as completed. * * @param {number} cmId The module ID. * @param {number} completed Whether the module is completed or not. * @param {number} courseId Course ID the module belongs to. * @param {string} [courseName] Course name. Recommended, it is used to display a better warning message. * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved when completion is successfully sent or stored. */ markCompletedManually(cmId: number, completed: number, courseId: number, courseName?: string, siteId?: string) : Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); // Convenience function to store a completion to be synchronized later. const storeOffline = (): Promise => { return this.courseOffline.markCompletedManually(cmId, completed, courseId, courseName, siteId); }; // The offline function requires a courseId and it could be missing because it's a calculated field. if (!this.appProvider.isOnline() && courseId) { // App is offline, store the action. return storeOffline(); } // Try to send it to server. return this.markCompletedManuallyOnline(cmId, completed, siteId).then((result) => { // Data sent to server, if there is some offline data delete it now. return this.courseOffline.deleteManualCompletion(cmId, siteId).catch(() => { // Ignore errors, shouldn't happen. }).then(() => { return result; }); }).catch((error) => { if (this.utils.isWebServiceError(error) || !courseId) { // The WebService has thrown an error, this means that responses cannot be submitted. return Promise.reject(error); } else { // Couldn't connect to server, store it offline. return storeOffline(); } }); } /** * Offline version for manually marking a module as completed. * * @param {number} cmId The module ID. * @param {number} completed Whether the module is completed or not. * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved when completion is successfully sent. */ markCompletedManuallyOnline(cmId: number, completed: number, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const params = { cmid: cmId, completed: completed }; return site.write('core_completion_update_activity_completion_status_manually', params); }); } /** * Check if a module has a view page. E.g. labels don't have a view page. * * @param {any} module The module object. * @return {boolean} Whether the module has a view page. */ moduleHasView(module: any): boolean { return !!module.url; } /** * Change the course status, setting it to the previous status. * * @param {number} courseId Course ID. * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved when the status is changed. Resolve param: new status. */ setCoursePreviousStatus(courseId: number, siteId?: string): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); this.logger.debug(`Set previous status for course ${courseId} in site ${siteId}`); return this.sitesProvider.getSite(siteId).then((site) => { const db = site.getDb(), newData: any = {}; // Get current stored data. return this.getCourseStatusData(courseId, siteId).then((entry) => { this.logger.debug(`Set previous status '${entry.status}' for course ${courseId}`); newData.status = entry.previous || CoreConstants.NOT_DOWNLOADED; newData.updated = Date.now(); if (entry.status == CoreConstants.DOWNLOADING) { // Going back from downloading to previous status, restore previous download time. newData.downloadTime = entry.previousDownloadTime; } return db.updateRecords(this.COURSE_STATUS_TABLE, newData, { id: courseId }).then(() => { // Success updating, trigger event. this.triggerCourseStatusChanged(courseId, newData.status, siteId); return newData.status; }); }); }); } /** * Store course status. * * @param {number} courseId Course ID. * @param {string} status New course status. * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved when the status is stored. */ setCourseStatus(courseId: number, status: string, siteId?: string): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); this.logger.debug(`Set status '${status}' for course ${courseId} in site ${siteId}`); return this.sitesProvider.getSite(siteId).then((site) => { let downloadTime, previousDownloadTime; if (status == CoreConstants.DOWNLOADING) { // Set download time if course is now downloading. downloadTime = this.timeUtils.timestamp(); } // Search current status to set it as previous status. return this.getCourseStatusData(courseId, siteId).then((entry) => { if (typeof downloadTime == 'undefined') { // Keep previous download time. downloadTime = entry.downloadTime; previousDownloadTime = entry.previousDownloadTime; } else { // The downloadTime will be updated, store current time as previous. previousDownloadTime = entry.downloadTime; } return entry.status; }).catch(() => { // No previous status. }).then((previousStatus) => { if (previousStatus != status) { // Status has changed, update it. const data = { id: courseId, status: status, previous: previousStatus, updated: new Date().getTime(), downloadTime: downloadTime, previousDownloadTime: previousDownloadTime }; return site.getDb().insertRecord(this.COURSE_STATUS_TABLE, data); } }).then(() => { // Success inserting, trigger event. this.triggerCourseStatusChanged(courseId, status, siteId); }); }); } /** * Translate a module name to current language. * * @param {string} moduleName The module name. * @return {string} Translated name. */ translateModuleName(moduleName: string): string { if (this.CORE_MODULES.indexOf(moduleName) < 0) { moduleName = 'external-tool'; } const langKey = 'core.mod_' + moduleName, translated = this.translate.instant(langKey); return translated !== langKey ? translated : moduleName; } /** * Trigger COURSE_STATUS_CHANGED with the right data. * * @param {number} courseId Course ID. * @param {string} status New course status. * @param {string} [siteId] Site ID. If not defined, current site. */ protected triggerCourseStatusChanged(courseId: number, status: string, siteId?: string): void { this.eventsProvider.trigger(CoreEventsProvider.COURSE_STATUS_CHANGED, { courseId: courseId, status: status }, siteId); } }