1580 lines
57 KiB
TypeScript
1580 lines
57 KiB
TypeScript
// (C) Copyright 2015 Moodle Pty Ltd.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
import { Injectable } from '@angular/core';
|
|
import { Subject, BehaviorSubject, Subscription } from 'rxjs';
|
|
import { Md5 } from 'ts-md5/dist/md5';
|
|
|
|
import { CoreFile } from '@services/file';
|
|
import { CoreFileHelper } from '@services/file-helper';
|
|
import { CoreFilepool } from '@services/filepool';
|
|
import { CoreSites } from '@services/sites';
|
|
import { CoreTimeUtils } from '@services/utils/time';
|
|
import { CoreUtils } from '@services/utils/utils';
|
|
import { CoreCourse, CoreCourseAnyModuleData, CoreCourseModuleContentFile } from './course';
|
|
import { CoreCache } from '@classes/cache';
|
|
import { CoreSiteWSPreSets } from '@classes/site';
|
|
import { CoreConstants } from '@/core/constants';
|
|
import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate';
|
|
import { makeSingleton } from '@singletons';
|
|
import { CoreEvents, CoreEventSectionStatusChangedData } from '@singletons/events';
|
|
import { CoreError } from '@classes/errors/error';
|
|
import { CoreWSFile, CoreWSExternalWarning } from '@services/ws';
|
|
import { CHECK_UPDATES_TIMES_TABLE, CoreCourseCheckUpdatesDBRecord } from './database/module-prefetch';
|
|
import { CoreFileSizeSum } from '@services/plugin-file-delegate';
|
|
import { CoreCourseModuleData } from './course-helper';
|
|
|
|
const ROOT_CACHE_KEY = 'mmCourse:';
|
|
|
|
/**
|
|
* Delegate to register module prefetch handlers.
|
|
*/
|
|
@Injectable({ providedIn: 'root' })
|
|
export class CoreCourseModulePrefetchDelegateService extends CoreDelegate<CoreCourseModulePrefetchHandler> {
|
|
|
|
protected statusCache = new CoreCache();
|
|
protected featurePrefix = 'CoreCourseModuleDelegate_';
|
|
protected handlerNameProperty = 'modName';
|
|
|
|
// Promises for check updates, to prevent performing the same request twice at the same time.
|
|
protected courseUpdatesPromises: Record<string, Record<string, Promise<CourseUpdates>>> = {};
|
|
|
|
// Promises and observables for prefetching, to prevent downloading same section twice at the same time and notify progress.
|
|
protected prefetchData: Record<string, Record<string, OngoingPrefetch>> = {};
|
|
|
|
constructor() {
|
|
super('CoreCourseModulePrefetchDelegate', true);
|
|
}
|
|
|
|
/**
|
|
* Initialize.
|
|
*/
|
|
initialize(): void {
|
|
CoreEvents.on(CoreEvents.LOGOUT, this.clearStatusCache.bind(this));
|
|
|
|
CoreEvents.on(CoreEvents.PACKAGE_STATUS_CHANGED, (data) => {
|
|
this.updateStatusCache(data.status, data.component, data.componentId);
|
|
}, CoreSites.getCurrentSiteId());
|
|
|
|
// If a file inside a module is downloaded/deleted, clear the corresponding cache.
|
|
CoreEvents.on(CoreEvents.COMPONENT_FILE_ACTION, (data) => {
|
|
if (!CoreFilepool.isFileEventDownloadedOrDeleted(data)) {
|
|
return;
|
|
}
|
|
|
|
this.statusCache.invalidate(CoreFilepool.getPackageId(data.component, data.componentId));
|
|
}, CoreSites.getCurrentSiteId());
|
|
}
|
|
|
|
/**
|
|
* Check if current site can check updates using core_course_check_updates.
|
|
*
|
|
* @return True if can check updates, false otherwise.
|
|
* @deprecated since app 4.0
|
|
*/
|
|
canCheckUpdates(): boolean {
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Check if a certain module can use core_course_check_updates.
|
|
*
|
|
* @param module Module.
|
|
* @param courseId Course ID the module belongs to.
|
|
* @return Promise resolved with boolean: whether the module can use check updates WS.
|
|
*/
|
|
async canModuleUseCheckUpdates(module: CoreCourseAnyModuleData, courseId: number): Promise<boolean> {
|
|
const handler = this.getPrefetchHandlerFor(module.modname);
|
|
|
|
if (!handler) {
|
|
// Module not supported, cannot use check updates.
|
|
return false;
|
|
}
|
|
|
|
if (handler.canUseCheckUpdates) {
|
|
return await handler.canUseCheckUpdates(module, courseId);
|
|
}
|
|
|
|
// By default, modules can use check updates.
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Clear the status cache.
|
|
*/
|
|
clearStatusCache(): void {
|
|
this.statusCache.clear();
|
|
}
|
|
|
|
/**
|
|
* Creates the list of modules to check for get course updates.
|
|
*
|
|
* @param modules List of modules.
|
|
* @param courseId Course ID the modules belong to.
|
|
* @return Promise resolved with the lists.
|
|
*/
|
|
protected async createToCheckList(modules: CoreCourseModuleData[], courseId: number): Promise<ToCheckList> {
|
|
const result: ToCheckList = {
|
|
toCheck: [],
|
|
cannotUse: [],
|
|
};
|
|
|
|
const promises = modules.map(async (module) => {
|
|
try {
|
|
const data = await this.getModuleStatusAndDownloadTime(module, courseId);
|
|
if (data.status != CoreConstants.DOWNLOADED) {
|
|
return;
|
|
}
|
|
|
|
// Module is downloaded and not outdated. Check if it can check updates.
|
|
const canUse = await this.canModuleUseCheckUpdates(module, courseId);
|
|
if (canUse) {
|
|
// Can use check updates, add it to the tocheck list.
|
|
result.toCheck.push({
|
|
contextlevel: 'module',
|
|
id: module.id,
|
|
since: data.downloadTime || 0,
|
|
});
|
|
} else {
|
|
// Cannot use check updates, add it to the cannotUse array.
|
|
result.cannotUse.push(module);
|
|
}
|
|
} catch {
|
|
// Ignore errors.
|
|
}
|
|
});
|
|
|
|
await Promise.all(promises);
|
|
|
|
// Sort toCheck list.
|
|
result.toCheck.sort((a, b) => a.id >= b.id ? 1 : -1);
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Determines a module status based on current status, restoring downloads if needed.
|
|
*
|
|
* @param module Module.
|
|
* @param status Current status.
|
|
* @return Module status.
|
|
*/
|
|
determineModuleStatus(module: CoreCourseAnyModuleData, status: string): string {
|
|
const handler = this.getPrefetchHandlerFor(module.modname);
|
|
const siteId = CoreSites.getCurrentSiteId();
|
|
|
|
if (!handler) {
|
|
return status;
|
|
}
|
|
|
|
if (status == CoreConstants.DOWNLOADING) {
|
|
// Check if the download is being handled.
|
|
if (!CoreFilepool.getPackageDownloadPromise(siteId, handler.component, module.id)) {
|
|
// Not handled, the app was probably restarted or something weird happened.
|
|
// Re-start download (files already on queue or already downloaded will be skipped).
|
|
handler.prefetch(module, module.course);
|
|
}
|
|
} else if (handler.determineStatus) {
|
|
// The handler implements a determineStatus function. Apply it.
|
|
return handler.determineStatus(module, status, true);
|
|
}
|
|
|
|
return status;
|
|
}
|
|
|
|
/**
|
|
* Download a module.
|
|
*
|
|
* @param module Module to download.
|
|
* @param courseId Course ID the module belongs to.
|
|
* @param dirPath Path of the directory where to store all the content files.
|
|
* @return Promise resolved when finished.
|
|
*/
|
|
async downloadModule(module: CoreCourseAnyModuleData, courseId: number, dirPath?: string): Promise<void> {
|
|
// Check if the module has a prefetch handler.
|
|
const handler = this.getPrefetchHandlerFor(module.modname);
|
|
|
|
if (!handler) {
|
|
return;
|
|
}
|
|
|
|
await this.syncModule(module, courseId);
|
|
|
|
await handler.download(module, courseId, dirPath);
|
|
}
|
|
|
|
/**
|
|
* Check for updates in a course.
|
|
*
|
|
* @param modules List of modules.
|
|
* @param courseId Course ID the modules belong to.
|
|
* @return Promise resolved with the updates. If a module is set to false, it means updates cannot be
|
|
* checked for that module in the current site.
|
|
*/
|
|
async getCourseUpdates(modules: CoreCourseModuleData[], courseId: number): Promise<CourseUpdates> {
|
|
// Check if there's already a getCourseUpdates in progress.
|
|
const id = <string> Md5.hashAsciiStr(courseId + '#' + JSON.stringify(modules));
|
|
const siteId = CoreSites.getCurrentSiteId();
|
|
|
|
if (this.courseUpdatesPromises[siteId] && this.courseUpdatesPromises[siteId][id] !== undefined) {
|
|
// There's already a get updates ongoing, return the promise.
|
|
return this.courseUpdatesPromises[siteId][id];
|
|
} else if (!this.courseUpdatesPromises[siteId]) {
|
|
this.courseUpdatesPromises[siteId] = {};
|
|
}
|
|
|
|
this.courseUpdatesPromises[siteId][id] = this.fetchCourseUpdates(modules, courseId, siteId);
|
|
|
|
try {
|
|
return await this.courseUpdatesPromises[siteId][id];
|
|
} finally {
|
|
// Get updates finished, delete the promise.
|
|
delete this.courseUpdatesPromises[siteId][id];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetch updates in a course.
|
|
*
|
|
* @param modules List of modules.
|
|
* @param courseId Course ID the modules belong to.
|
|
* @param siteId Site ID.
|
|
* @return Promise resolved with the updates. If a module is set to false, it means updates cannot be
|
|
* checked for that module in the site.
|
|
*/
|
|
protected async fetchCourseUpdates(
|
|
modules: CoreCourseModuleData[],
|
|
courseId: number,
|
|
siteId: string,
|
|
): Promise<CourseUpdates> {
|
|
const data = await this.createToCheckList(modules, courseId);
|
|
const result: CourseUpdates = {};
|
|
|
|
// Mark as false the modules that cannot use check updates WS.
|
|
data.cannotUse.forEach((module) => {
|
|
result[module.id] = false;
|
|
});
|
|
|
|
if (!data.toCheck.length) {
|
|
// Nothing to check, no need to call the WS.
|
|
return result;
|
|
}
|
|
|
|
// Get the site, maybe the user changed site.
|
|
const site = await CoreSites.getSite(siteId);
|
|
|
|
const params: CoreCourseCheckUpdatesWSParams = {
|
|
courseid: courseId,
|
|
tocheck: data.toCheck,
|
|
};
|
|
const preSets: CoreSiteWSPreSets = {
|
|
cacheKey: this.getCourseUpdatesCacheKey(courseId),
|
|
emergencyCache: false, // If downloaded data has changed and offline, just fail. See MOBILE-2085.
|
|
uniqueCacheKey: true,
|
|
splitRequest: {
|
|
param: 'tocheck',
|
|
maxLength: 10,
|
|
},
|
|
};
|
|
|
|
try {
|
|
const response = await site.read<CoreCourseCheckUpdatesWSResponse>('core_course_check_updates', params, preSets);
|
|
|
|
// Store the last execution of the check updates call.
|
|
const entry: CoreCourseCheckUpdatesDBRecord = {
|
|
courseId: courseId,
|
|
time: CoreTimeUtils.timestamp(),
|
|
};
|
|
CoreUtils.ignoreErrors(site.getDb().insertRecord(CHECK_UPDATES_TIMES_TABLE, entry));
|
|
|
|
return this.treatCheckUpdatesResult(data.toCheck, response, result);
|
|
} catch (error) {
|
|
// Cannot get updates.
|
|
// Get cached entries but discard modules with a download time higher than the last execution of check updates.
|
|
let entry: CoreCourseCheckUpdatesDBRecord | undefined;
|
|
try {
|
|
entry = await site.getDb().getRecord<CoreCourseCheckUpdatesDBRecord>(
|
|
CHECK_UPDATES_TIMES_TABLE,
|
|
{ courseId: courseId },
|
|
);
|
|
} catch {
|
|
// No previous executions, return result as it is.
|
|
return result;
|
|
}
|
|
|
|
preSets.getCacheUsingCacheKey = true;
|
|
preSets.omitExpires = true;
|
|
|
|
const response = await site.read<CoreCourseCheckUpdatesWSResponse>('core_course_check_updates', params, preSets);
|
|
|
|
return this.treatCheckUpdatesResult(data.toCheck, response, result, entry.time);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check for updates in a course.
|
|
*
|
|
* @param courseId Course ID the modules belong to.
|
|
* @return Promise resolved with the updates.
|
|
*/
|
|
async getCourseUpdatesByCourseId(courseId: number): Promise<CourseUpdates> {
|
|
// Get course sections and all their modules.
|
|
const sections = await CoreCourse.getSections(courseId, false, true, { omitExpires: true });
|
|
|
|
return this.getCourseUpdates(CoreCourse.getSectionsModules(sections), courseId);
|
|
}
|
|
|
|
/**
|
|
* Get cache key for course updates WS calls.
|
|
*
|
|
* @param courseId Course ID.
|
|
* @return Cache key.
|
|
*/
|
|
protected getCourseUpdatesCacheKey(courseId: number): string {
|
|
return ROOT_CACHE_KEY + 'courseUpdates:' + courseId;
|
|
}
|
|
|
|
/**
|
|
* Get modules download size. Only treat the modules with status not downloaded or outdated.
|
|
*
|
|
* @param modules List of modules.
|
|
* @param courseId Course ID the modules belong to.
|
|
* @return Promise resolved with the size.
|
|
*/
|
|
async getDownloadSize(modules: CoreCourseModuleData[], courseId: number): Promise<CoreFileSizeSum> {
|
|
// Get the status of each module.
|
|
const data = await this.getModulesStatus(modules, courseId);
|
|
|
|
const downloadableModules = data[CoreConstants.NOT_DOWNLOADED].concat(data[CoreConstants.OUTDATED]);
|
|
const result: CoreFileSizeSum = {
|
|
size: 0,
|
|
total: true,
|
|
};
|
|
|
|
await Promise.all(downloadableModules.map(async (module) => {
|
|
const size = await this.getModuleDownloadSize(module, courseId);
|
|
|
|
result.total = result.total && size.total;
|
|
result.size += size.size;
|
|
}));
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Get the download size of a module.
|
|
*
|
|
* @param module Module to get size.
|
|
* @param courseId Course ID the module belongs to.
|
|
* @param single True if we're downloading a single module, false if we're downloading a whole section.
|
|
* @return Promise resolved with the size.
|
|
*/
|
|
async getModuleDownloadSize(module: CoreCourseAnyModuleData, courseId: number, single?: boolean): Promise<CoreFileSizeSum> {
|
|
const handler = this.getPrefetchHandlerFor(module.modname);
|
|
|
|
if (!handler) {
|
|
return { size: 0, total: false };
|
|
}
|
|
const downloadable = await this.isModuleDownloadable(module, courseId);
|
|
if (!downloadable) {
|
|
return { size: 0, total: true };
|
|
}
|
|
|
|
const packageId = CoreFilepool.getPackageId(handler.component, module.id);
|
|
const downloadSize = this.statusCache.getValue<CoreFileSizeSum>(packageId, 'downloadSize');
|
|
if (downloadSize !== undefined) {
|
|
return downloadSize;
|
|
}
|
|
|
|
try {
|
|
const size = await handler.getDownloadSize(module, courseId, single);
|
|
|
|
return this.statusCache.setValue(packageId, 'downloadSize', size);
|
|
} catch (error) {
|
|
const cachedSize = this.statusCache.getValue<CoreFileSizeSum>(packageId, 'downloadSize', true);
|
|
if (cachedSize) {
|
|
return cachedSize;
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the download size of a module.
|
|
*
|
|
* @param module Module to get size.
|
|
* @param courseId Course ID the module belongs to.
|
|
* @return Promise resolved with the size.
|
|
*/
|
|
async getModuleDownloadedSize(module: CoreCourseAnyModuleData, courseId: number): Promise<number> {
|
|
const handler = this.getPrefetchHandlerFor(module.modname);
|
|
if (!handler) {
|
|
return 0;
|
|
}
|
|
|
|
const downloadable = await this.isModuleDownloadable(module, courseId);
|
|
if (!downloadable) {
|
|
return 0;
|
|
}
|
|
|
|
const packageId = CoreFilepool.getPackageId(handler.component, module.id);
|
|
const downloadedSize = this.statusCache.getValue<number>(packageId, 'downloadedSize');
|
|
if (downloadedSize !== undefined) {
|
|
return downloadedSize;
|
|
}
|
|
|
|
try {
|
|
let size = 0;
|
|
|
|
if (handler.getDownloadedSize) {
|
|
// Handler implements a method to calculate the downloaded size, use it.
|
|
size = await handler.getDownloadedSize(module, courseId);
|
|
} else {
|
|
// Handler doesn't implement it, get the module files and check if they're downloaded.
|
|
const files = await this.getModuleFiles(module, courseId);
|
|
|
|
const siteId = CoreSites.getCurrentSiteId();
|
|
|
|
// Retrieve file size if it's downloaded.
|
|
await Promise.all(files.map(async (file) => {
|
|
const path = await CoreFilepool.getFilePathByUrl(siteId, CoreFileHelper.getFileUrl(file));
|
|
|
|
try {
|
|
const fileSize = await CoreFile.getFileSize(path);
|
|
|
|
size += fileSize;
|
|
} catch {
|
|
// Error getting size. Check if the file is being downloaded.
|
|
const isDownloading = await CoreFilepool.isFileDownloadingByUrl(siteId, CoreFileHelper.getFileUrl(file));
|
|
if (isDownloading) {
|
|
// If downloading, count as downloaded.
|
|
size += file.filesize || 0;
|
|
}
|
|
}
|
|
}));
|
|
}
|
|
|
|
return this.statusCache.setValue(packageId, 'downloadedSize', size);
|
|
} catch {
|
|
return this.statusCache.getValue<number>(packageId, 'downloadedSize', true) || 0;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the estimated total size of data stored for a module. This includes
|
|
* the files downloaded for it (getModuleDownloadedSize) and also the total
|
|
* size of web service requests stored for it.
|
|
*
|
|
* @param module Module to get the size.
|
|
* @param courseId Course ID the module belongs to.
|
|
* @return Promise resolved with the total size (0 if unknown)
|
|
*/
|
|
async getModuleStoredSize(module: CoreCourseAnyModuleData, courseId: number): Promise<number> {
|
|
let downloadedSize = await this.getModuleDownloadedSize(module, courseId);
|
|
|
|
if (isNaN(downloadedSize)) {
|
|
downloadedSize = 0;
|
|
}
|
|
|
|
const site = CoreSites.getCurrentSite();
|
|
const handler = this.getPrefetchHandlerFor(module.modname);
|
|
if (!handler || !site) {
|
|
// If there is no handler then we can't find out the component name.
|
|
// We can't work out the cached size, so just return downloaded size.
|
|
return downloadedSize;
|
|
}
|
|
|
|
const cachedSize = await site.getComponentCacheSize(handler.component, module.id);
|
|
|
|
return cachedSize + downloadedSize;
|
|
}
|
|
|
|
/**
|
|
* Get module files.
|
|
*
|
|
* @param module Module to get the files.
|
|
* @param courseId Course ID the module belongs to.
|
|
* @return Promise resolved with the list of files.
|
|
*/
|
|
async getModuleFiles(
|
|
module: CoreCourseAnyModuleData,
|
|
courseId: number,
|
|
): Promise<(CoreWSFile | CoreCourseModuleContentFile)[]> {
|
|
const handler = this.getPrefetchHandlerFor(module.modname);
|
|
|
|
if (handler?.getFiles) {
|
|
// The handler defines a function to get files, use it.
|
|
return await handler.getFiles(module, courseId);
|
|
} else if (handler?.loadContents) {
|
|
// The handler defines a function to load contents, use it before returning module contents.
|
|
await handler.loadContents(module, courseId);
|
|
|
|
return module.contents || [];
|
|
} else {
|
|
return module.contents || [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the module status.
|
|
*
|
|
* @param module Module.
|
|
* @param courseId Course ID the module belongs to.
|
|
* @param updates Result of getCourseUpdates for all modules in the course. If not provided, it will be
|
|
* calculated (slower). If it's false it means the site doesn't support check updates.
|
|
* @param refresh True if it should ignore the memory cache, not the WS cache.
|
|
* @param sectionId ID of the section the module belongs to.
|
|
* @return Promise resolved with the status.
|
|
*/
|
|
async getModuleStatus(
|
|
module: CoreCourseAnyModuleData,
|
|
courseId: number,
|
|
updates?: CourseUpdates | false,
|
|
refresh?: boolean,
|
|
sectionId?: number,
|
|
): Promise<string> {
|
|
const handler = this.getPrefetchHandlerFor(module.modname);
|
|
|
|
if (!handler) {
|
|
// No handler found, module not downloadable.
|
|
return CoreConstants.NOT_DOWNLOADABLE;
|
|
}
|
|
|
|
// Check if the status is cached.
|
|
const packageId = CoreFilepool.getPackageId(handler.component, module.id);
|
|
const status = this.statusCache.getValue<string>(packageId, 'status');
|
|
|
|
if (!refresh && status !== undefined) {
|
|
this.storeCourseAndSection(packageId, courseId, sectionId);
|
|
|
|
return this.determineModuleStatus(module, status);
|
|
}
|
|
|
|
const result = await this.calculateModuleStatus(handler, module, courseId, updates, sectionId);
|
|
if (result.updateStatus) {
|
|
this.updateStatusCache(result.status, handler.component, module.id, courseId, sectionId);
|
|
}
|
|
|
|
return this.determineModuleStatus(module, result.status);
|
|
}
|
|
|
|
/**
|
|
* Calculate a module status.
|
|
*
|
|
* @param handler Prefetch handler.
|
|
* @param module Module.
|
|
* @param courseId Course ID the module belongs to.
|
|
* @param updates Result of getCourseUpdates for all modules in the course. If not provided, it will be
|
|
* calculated (slower). If it's false it means the site doesn't support check updates.
|
|
* @param sectionId ID of the section the module belongs to.
|
|
* @return Promise resolved with the status.
|
|
*/
|
|
protected async calculateModuleStatus(
|
|
handler: CoreCourseModulePrefetchHandler,
|
|
module: CoreCourseAnyModuleData,
|
|
courseId: number,
|
|
updates?: CourseUpdates | false,
|
|
sectionId?: number,
|
|
): Promise<{status: string; updateStatus: boolean}> {
|
|
// Check if the module is downloadable.
|
|
const downloadable = await this.isModuleDownloadable(module, courseId);
|
|
if (!downloadable) {
|
|
return {
|
|
status: CoreConstants.NOT_DOWNLOADABLE,
|
|
updateStatus: true,
|
|
};
|
|
}
|
|
|
|
// Get the saved package status.
|
|
const siteId = CoreSites.getCurrentSiteId();
|
|
const currentStatus = await CoreFilepool.getPackageStatus(siteId, handler.component, module.id);
|
|
|
|
let status = handler.determineStatus ? handler.determineStatus(module, currentStatus, true) : currentStatus;
|
|
if (status != CoreConstants.DOWNLOADED || updates === false) {
|
|
return {
|
|
status,
|
|
updateStatus: true,
|
|
};
|
|
}
|
|
|
|
// Module is downloaded. Determine if there are updated in the module to show them outdated.
|
|
if (updates === undefined) {
|
|
try {
|
|
// We don't have course updates, calculate them.
|
|
updates = await this.getCourseUpdatesByCourseId(courseId);
|
|
} catch {
|
|
// Error getting updates, show the stored status.
|
|
const packageId = CoreFilepool.getPackageId(handler.component, module.id);
|
|
this.storeCourseAndSection(packageId, courseId, sectionId);
|
|
|
|
return {
|
|
status: currentStatus,
|
|
updateStatus: false,
|
|
};
|
|
}
|
|
}
|
|
|
|
if (!updates || updates[module.id] === false) {
|
|
// Cannot check updates, always show outdated.
|
|
return {
|
|
status: CoreConstants.OUTDATED,
|
|
updateStatus: true,
|
|
};
|
|
}
|
|
|
|
try {
|
|
// Check if the module has any update.
|
|
const hasUpdates = await this.moduleHasUpdates(module, courseId, updates);
|
|
|
|
if (!hasUpdates) {
|
|
// No updates, keep current status.
|
|
return {
|
|
status,
|
|
updateStatus: true,
|
|
};
|
|
}
|
|
|
|
// Has updates, mark the module as outdated.
|
|
status = CoreConstants.OUTDATED;
|
|
|
|
await CoreUtils.ignoreErrors(
|
|
CoreFilepool.storePackageStatus(siteId, status, handler.component, module.id),
|
|
);
|
|
|
|
return {
|
|
status,
|
|
updateStatus: true,
|
|
};
|
|
} catch {
|
|
// Error checking if module has updates.
|
|
const packageId = CoreFilepool.getPackageId(handler.component, module.id);
|
|
const status = this.statusCache.getValue<string>(packageId, 'status', true);
|
|
|
|
return {
|
|
status: this.determineModuleStatus(module, status || CoreConstants.NOT_DOWNLOADED),
|
|
updateStatus: true,
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the status of a list of modules, along with the lists of modules for each status.
|
|
*
|
|
* @param modules List of modules to prefetch.
|
|
* @param courseId Course ID the modules belong to.
|
|
* @param sectionId ID of the section the modules belong to.
|
|
* @param refresh True if it should always check the DB (slower).
|
|
* @param onlyToDisplay True if the status will only be used to determine which button should be displayed.
|
|
* @param checkUpdates Whether to use the WS to check updates. Defaults to true.
|
|
* @return Promise resolved with the data.
|
|
*/
|
|
async getModulesStatus(
|
|
modules: CoreCourseModuleData[],
|
|
courseId: number,
|
|
sectionId?: number,
|
|
refresh?: boolean,
|
|
onlyToDisplay?: boolean,
|
|
checkUpdates: boolean = true,
|
|
): Promise<CoreCourseModulesStatus> {
|
|
|
|
let updates: CourseUpdates | false = false;
|
|
const result: CoreCourseModulesStatus = {
|
|
total: 0,
|
|
status: CoreConstants.NOT_DOWNLOADABLE,
|
|
[CoreConstants.NOT_DOWNLOADED]: [],
|
|
[CoreConstants.DOWNLOADED]: [],
|
|
[CoreConstants.DOWNLOADING]: [],
|
|
[CoreConstants.OUTDATED]: [],
|
|
};
|
|
|
|
if (checkUpdates) {
|
|
// Check updates in course. Don't use getCourseUpdates because the list of modules might not be the whole course list.
|
|
try {
|
|
updates = await this.getCourseUpdatesByCourseId(courseId);
|
|
} catch {
|
|
// Cannot get updates.
|
|
}
|
|
}
|
|
|
|
await Promise.all(modules.map(async (module) => {
|
|
const handler = this.getPrefetchHandlerFor(module.modname);
|
|
if (!handler || (onlyToDisplay && handler.skipListStatus)) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const modStatus = await this.getModuleStatus(module, courseId, updates, refresh);
|
|
|
|
if (!result[modStatus]) {
|
|
return;
|
|
}
|
|
|
|
result.status = CoreFilepool.determinePackagesStatus(result.status, modStatus);
|
|
result[modStatus].push(module);
|
|
result.total++;
|
|
} catch (error) {
|
|
const packageId = CoreFilepool.getPackageId(handler.component, module.id);
|
|
const cacheStatus = this.statusCache.getValue<string>(packageId, 'status', true);
|
|
if (cacheStatus === undefined) {
|
|
throw error;
|
|
}
|
|
|
|
if (!result[cacheStatus]) {
|
|
return;
|
|
}
|
|
|
|
result.status = CoreFilepool.determinePackagesStatus(result.status, cacheStatus);
|
|
result[cacheStatus].push(module);
|
|
result.total++;
|
|
}
|
|
}));
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Get a module status and download time. It will only return the download time if the module is downloaded or outdated.
|
|
*
|
|
* @param module Module.
|
|
* @param courseId Course ID the module belongs to.
|
|
* @return Promise resolved with the data.
|
|
*/
|
|
protected async getModuleStatusAndDownloadTime(
|
|
module: CoreCourseAnyModuleData,
|
|
courseId: number,
|
|
): Promise<{ status: string; downloadTime?: number }> {
|
|
const handler = this.getPrefetchHandlerFor(module.modname);
|
|
const siteId = CoreSites.getCurrentSiteId();
|
|
|
|
if (!handler) {
|
|
// No handler found, module not downloadable.
|
|
return { status: CoreConstants.NOT_DOWNLOADABLE };
|
|
}
|
|
|
|
// Get the status from the cache.
|
|
const packageId = CoreFilepool.getPackageId(handler.component, module.id);
|
|
const status = this.statusCache.getValue<string>(packageId, 'status');
|
|
|
|
if (status !== undefined && !CoreFileHelper.isStateDownloaded(status)) {
|
|
// Module isn't downloaded, just return the status.
|
|
return { status };
|
|
}
|
|
|
|
// Check if the module is downloadable.
|
|
const downloadable = await this.isModuleDownloadable(module, courseId);
|
|
if (!downloadable) {
|
|
return { status: CoreConstants.NOT_DOWNLOADABLE };
|
|
}
|
|
|
|
// Get the stored data to get the status and downloadTime.
|
|
const data = await CoreFilepool.getPackageData(siteId, handler.component, module.id);
|
|
|
|
return {
|
|
status: data.status || CoreConstants.NOT_DOWNLOADED,
|
|
downloadTime: data.downloadTime || 0,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get updates for a certain module.
|
|
* It will only return the updates if the module can use check updates and it's downloaded or outdated.
|
|
*
|
|
* @param module Module to check.
|
|
* @param courseId Course the module belongs to.
|
|
* @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down).
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @return Promise resolved with the updates.
|
|
*/
|
|
async getModuleUpdates(
|
|
module: CoreCourseAnyModuleData,
|
|
courseId: number,
|
|
ignoreCache?: boolean,
|
|
siteId?: string,
|
|
): Promise<CheckUpdatesWSInstance | null> {
|
|
|
|
const site = await CoreSites.getSite(siteId);
|
|
|
|
const data = await this.getModuleStatusAndDownloadTime(module, courseId);
|
|
if (!CoreFileHelper.isStateDownloaded(data.status)) {
|
|
// Not downloaded, no updates.
|
|
return null;
|
|
}
|
|
|
|
// Module is downloaded. Check if it can check updates.
|
|
const canUse = await this.canModuleUseCheckUpdates(module, courseId);
|
|
if (!canUse) {
|
|
// Can't use check updates, no updates.
|
|
return null;
|
|
}
|
|
|
|
const params: CoreCourseCheckUpdatesWSParams = {
|
|
courseid: courseId,
|
|
tocheck: [
|
|
{
|
|
contextlevel: 'module',
|
|
id: module.id,
|
|
since: data.downloadTime || 0,
|
|
},
|
|
],
|
|
};
|
|
const preSets: CoreSiteWSPreSets = {
|
|
cacheKey: this.getModuleUpdatesCacheKey(courseId, module.id),
|
|
};
|
|
|
|
if (ignoreCache) {
|
|
preSets.getFromCache = false;
|
|
preSets.emergencyCache = false;
|
|
}
|
|
|
|
const response = await site.read<CoreCourseCheckUpdatesWSResponse>('core_course_check_updates', params, preSets);
|
|
if (!response.instances[0]) {
|
|
throw new CoreError('Could not get module updates.');
|
|
}
|
|
|
|
return response.instances[0];
|
|
}
|
|
|
|
/**
|
|
* Get cache key for module updates WS calls.
|
|
*
|
|
* @param courseId Course ID.
|
|
* @param moduleId Module ID.
|
|
* @return Cache key.
|
|
*/
|
|
protected getModuleUpdatesCacheKey(courseId: number, moduleId: number): string {
|
|
return this.getCourseUpdatesCacheKey(courseId) + ':' + moduleId;
|
|
}
|
|
|
|
/**
|
|
* Get a prefetch handler.
|
|
*
|
|
* @param moduleName The module name to work on.
|
|
* @return Prefetch handler.
|
|
*/
|
|
getPrefetchHandlerFor(moduleName: string): CoreCourseModulePrefetchHandler | undefined {
|
|
return this.getHandler(moduleName, true);
|
|
}
|
|
|
|
/**
|
|
* Invalidate check updates WS call.
|
|
*
|
|
* @param courseId Course ID.
|
|
* @return Promise resolved when data is invalidated.
|
|
*/
|
|
async invalidateCourseUpdates(courseId: number): Promise<void> {
|
|
const site = CoreSites.getCurrentSite();
|
|
if (!site) {
|
|
return;
|
|
}
|
|
|
|
await site.invalidateWsCacheForKey(this.getCourseUpdatesCacheKey(courseId));
|
|
}
|
|
|
|
/**
|
|
* Invalidate a list of modules in a course. This should only invalidate WS calls, not downloaded files.
|
|
*
|
|
* @param modules List of modules.
|
|
* @param courseId Course ID.
|
|
* @return Promise resolved when modules are invalidated.
|
|
*/
|
|
async invalidateModules(modules: CoreCourseModuleData[], courseId: number): Promise<void> {
|
|
|
|
const promises = modules.map(async (module) => {
|
|
const handler = this.getPrefetchHandlerFor(module.modname);
|
|
if (!handler) {
|
|
return;
|
|
}
|
|
|
|
if (handler.invalidateModule) {
|
|
await CoreUtils.ignoreErrors(handler.invalidateModule(module, courseId));
|
|
}
|
|
|
|
// Invalidate cache.
|
|
this.invalidateModuleStatusCache(module);
|
|
});
|
|
|
|
promises.push(this.invalidateCourseUpdates(courseId));
|
|
|
|
await Promise.all(promises);
|
|
}
|
|
|
|
/**
|
|
* Invalidates the cache for a given module.
|
|
*
|
|
* @param module Module to be invalidated.
|
|
*/
|
|
invalidateModuleStatusCache(module: CoreCourseAnyModuleData): void {
|
|
const handler = this.getPrefetchHandlerFor(module.modname);
|
|
if (handler) {
|
|
this.statusCache.invalidate(CoreFilepool.getPackageId(handler.component, module.id));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Invalidate check updates WS call for a certain module.
|
|
*
|
|
* @param courseId Course ID.
|
|
* @param moduleId Module ID.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @return Promise resolved when data is invalidated.
|
|
*/
|
|
async invalidateModuleUpdates(courseId: number, moduleId: number, siteId?: string): Promise<void> {
|
|
const site = await CoreSites.getSite(siteId);
|
|
|
|
await site.invalidateWsCacheForKey(this.getModuleUpdatesCacheKey(courseId, moduleId));
|
|
}
|
|
|
|
/**
|
|
* Check if a list of modules is being downloaded.
|
|
*
|
|
* @param id An ID to identify the download.
|
|
* @return True if it's being downloaded, false otherwise.
|
|
*/
|
|
isBeingDownloaded(id: string): boolean {
|
|
const siteId = CoreSites.getCurrentSiteId();
|
|
|
|
return !!(this.prefetchData[siteId]?.[id]);
|
|
}
|
|
|
|
/**
|
|
* Check if a module is downloadable.
|
|
*
|
|
* @param module Module.
|
|
* @param courseId Course ID the module belongs to.
|
|
* @return Promise resolved with true if downloadable, false otherwise.
|
|
*/
|
|
async isModuleDownloadable(module: CoreCourseAnyModuleData, courseId: number): Promise<boolean> {
|
|
if ('uservisible' in module && module.uservisible === false) {
|
|
// Module isn't visible by the user, cannot be downloaded.
|
|
return false;
|
|
}
|
|
|
|
const handler = this.getPrefetchHandlerFor(module.modname);
|
|
if (!handler) {
|
|
return false;
|
|
}
|
|
|
|
if (!handler.isDownloadable) {
|
|
// Function not defined, assume it's downloadable.
|
|
return true;
|
|
}
|
|
|
|
const packageId = CoreFilepool.getPackageId(handler.component, module.id);
|
|
let downloadable = this.statusCache.getValue<boolean>(packageId, 'downloadable');
|
|
|
|
if (downloadable !== undefined) {
|
|
return downloadable;
|
|
}
|
|
|
|
try {
|
|
downloadable = await handler.isDownloadable(module, courseId);
|
|
|
|
return this.statusCache.setValue(packageId, 'downloadable', downloadable);
|
|
} catch {
|
|
// Something went wrong, assume it's not downloadable.
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if a module has updates based on the result of getCourseUpdates.
|
|
*
|
|
* @param module Module.
|
|
* @param courseId Course ID the module belongs to.
|
|
* @param updates Result of getCourseUpdates.
|
|
* @return Promise resolved with boolean: whether the module has updates.
|
|
*/
|
|
async moduleHasUpdates(module: CoreCourseAnyModuleData, courseId: number, updates: CourseUpdates): Promise<boolean> {
|
|
const handler = this.getPrefetchHandlerFor(module.modname);
|
|
const moduleUpdates = updates[module.id];
|
|
|
|
if (handler?.hasUpdates) {
|
|
// Handler implements its own function to check the updates, use it.
|
|
return await handler.hasUpdates(module, courseId, moduleUpdates);
|
|
} else if (!moduleUpdates || !moduleUpdates.updates || !moduleUpdates.updates.length) {
|
|
// Module doesn't have any update.
|
|
return false;
|
|
} else if (handler?.updatesNames?.test) {
|
|
// Check the update names defined by the handler.
|
|
for (let i = 0, len = moduleUpdates.updates.length; i < len; i++) {
|
|
if (handler.updatesNames.test(moduleUpdates.updates[i].name)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Handler doesn't define hasUpdates or updatesNames and there is at least 1 update. Assume it has updates.
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Prefetch a module.
|
|
*
|
|
* @param module Module to prefetch.
|
|
* @param courseId Course ID the module belongs to.
|
|
* @param single True if we're downloading a single module, false if we're downloading a whole section.
|
|
* @return Promise resolved when finished.
|
|
*/
|
|
async prefetchModule(module: CoreCourseAnyModuleData, courseId: number, single?: boolean): Promise<void> {
|
|
const handler = this.getPrefetchHandlerFor(module.modname);
|
|
if (!handler) {
|
|
return;
|
|
}
|
|
|
|
await this.syncModule(module, courseId);
|
|
|
|
await handler.prefetch(module, courseId, single);
|
|
}
|
|
|
|
/**
|
|
* Sync a group of modules.
|
|
*
|
|
* @param modules Array of modules to sync.
|
|
* @param courseId Course ID the module belongs to.
|
|
* @return Promise resolved when finished.
|
|
*/
|
|
syncModules(modules: CoreCourseModuleData[], courseId: number): Promise<unknown> {
|
|
return Promise.all(modules.map(async (module) => {
|
|
await this.syncModule(module, courseId);
|
|
|
|
// Invalidate course updates.
|
|
await CoreUtils.ignoreErrors(this.invalidateCourseUpdates(courseId));
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Sync a module.
|
|
*
|
|
* @param module Module to sync.
|
|
* @param courseId Course ID the module belongs to.
|
|
* @return Promise resolved when finished.
|
|
*/
|
|
async syncModule<T = unknown>(module: CoreCourseAnyModuleData, courseId: number): Promise<T | undefined> {
|
|
const handler = this.getPrefetchHandlerFor(module.modname);
|
|
if (!handler?.sync) {
|
|
return;
|
|
}
|
|
|
|
const result = await CoreUtils.ignoreErrors(handler.sync(module, courseId));
|
|
|
|
// Always invalidate status cache for this module. We cannot know if data was sent to server or not.
|
|
this.invalidateModuleStatusCache(module);
|
|
|
|
return <T> result;
|
|
}
|
|
|
|
/**
|
|
* Prefetches a list of modules using their prefetch handlers.
|
|
* If a prefetch already exists for this site and id, returns the current promise.
|
|
*
|
|
* @param id An ID to identify the download. It can be used to retrieve the download promise.
|
|
* @param modules List of modules to prefetch.
|
|
* @param courseId Course ID the modules belong to.
|
|
* @param onProgress Function to call everytime a module is downloaded.
|
|
* @return Promise resolved when all modules have been prefetched.
|
|
*/
|
|
async prefetchModules(
|
|
id: string,
|
|
modules: CoreCourseModuleData[],
|
|
courseId: number,
|
|
onProgress?: CoreCourseModulesProgressFunction,
|
|
): Promise<void> {
|
|
|
|
const siteId = CoreSites.getCurrentSiteId();
|
|
const currentPrefetchData = this.prefetchData[siteId]?.[id];
|
|
|
|
if (currentPrefetchData) {
|
|
// There's a prefetch ongoing, return the current promise.
|
|
if (onProgress) {
|
|
currentPrefetchData.subscriptions.push(currentPrefetchData.observable.subscribe(onProgress));
|
|
}
|
|
|
|
return currentPrefetchData.promise;
|
|
}
|
|
|
|
let count = 0;
|
|
const total = modules.length;
|
|
const moduleIds = modules.map((module) => module.id);
|
|
const prefetchData: OngoingPrefetch = {
|
|
observable: new BehaviorSubject<CoreCourseModulesProgress>({ count: count, total: total }),
|
|
promise: Promise.resolve(),
|
|
subscriptions: [],
|
|
};
|
|
|
|
if (onProgress) {
|
|
prefetchData.observable.subscribe(onProgress);
|
|
}
|
|
|
|
const promises = modules.map(async (module) => {
|
|
// Check if the module has a prefetch handler.
|
|
const handler = this.getPrefetchHandlerFor(module.modname);
|
|
if (!handler) {
|
|
return;
|
|
}
|
|
|
|
const downloadable = await this.isModuleDownloadable(module, courseId);
|
|
if (!downloadable) {
|
|
return;
|
|
}
|
|
|
|
await handler.prefetch(module, courseId);
|
|
|
|
const index = moduleIds.indexOf(module.id);
|
|
if (index > -1) {
|
|
moduleIds.splice(index, 1);
|
|
count++;
|
|
prefetchData.observable.next({ count: count, total: total });
|
|
}
|
|
});
|
|
|
|
// Set the promise.
|
|
prefetchData.promise = CoreUtils.allPromises(promises);
|
|
|
|
// Store the prefetch data in the list.
|
|
this.prefetchData[siteId] = this.prefetchData[siteId] || {};
|
|
this.prefetchData[siteId][id] = prefetchData;
|
|
|
|
try {
|
|
await prefetchData.promise;
|
|
} finally {
|
|
// Unsubscribe all observers.
|
|
prefetchData.subscriptions.forEach((subscription) => {
|
|
subscription.unsubscribe();
|
|
});
|
|
delete this.prefetchData[siteId][id];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove module Files from handler.
|
|
*
|
|
* @param module Module to remove the files.
|
|
* @param courseId Course ID the module belongs to.
|
|
* @return Promise resolved when done.
|
|
*/
|
|
async removeModuleFiles(module: CoreCourseAnyModuleData, courseId: number): Promise<void> {
|
|
const handler = this.getPrefetchHandlerFor(module.modname);
|
|
const siteId = CoreSites.getCurrentSiteId();
|
|
|
|
if (handler?.removeFiles) {
|
|
// Handler implements a method to remove the files, use it.
|
|
await handler.removeFiles(module, courseId);
|
|
} else {
|
|
// No method to remove files, use get files to try to remove the files.
|
|
const files = await this.getModuleFiles(module, courseId);
|
|
|
|
await Promise.all(files.map(async (file) => {
|
|
await CoreUtils.ignoreErrors(CoreFilepool.removeFileByUrl(siteId, CoreFileHelper.getFileUrl(file)));
|
|
}));
|
|
}
|
|
|
|
if (!handler) {
|
|
return;
|
|
}
|
|
|
|
// Update downloaded size.
|
|
const packageId = CoreFilepool.getPackageId(handler.component, module.id);
|
|
this.statusCache.setValue(packageId, 'downloadedSize', 0);
|
|
|
|
// If module is downloadable, set not dowloaded status.
|
|
const downloadable = await this.isModuleDownloadable(module, courseId);
|
|
if (!downloadable) {
|
|
return;
|
|
}
|
|
|
|
await CoreFilepool.storePackageStatus(siteId, CoreConstants.NOT_DOWNLOADED, handler.component, module.id);
|
|
}
|
|
|
|
/**
|
|
* Set an on progress function for the download of a list of modules.
|
|
*
|
|
* @param id An ID to identify the download.
|
|
* @param onProgress Function to call everytime a module is downloaded.
|
|
*/
|
|
setOnProgress(id: string, onProgress: CoreCourseModulesProgressFunction): void {
|
|
const currentData = this.prefetchData[CoreSites.getCurrentSiteId()]?.[id];
|
|
|
|
if (currentData) {
|
|
// There's a prefetch ongoing, return the current promise.
|
|
currentData.subscriptions.push(currentData.observable.subscribe(onProgress));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* If courseId or sectionId is set, save them in the cache.
|
|
*
|
|
* @param packageId The package ID.
|
|
* @param courseId Course ID.
|
|
* @param sectionId Section ID.
|
|
*/
|
|
storeCourseAndSection(packageId: string, courseId?: number, sectionId?: number): void {
|
|
if (courseId) {
|
|
this.statusCache.setValue(packageId, 'courseId', courseId);
|
|
}
|
|
if (sectionId && sectionId > 0) {
|
|
this.statusCache.setValue(packageId, 'sectionId', sectionId);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Treat the result of the check updates WS call.
|
|
*
|
|
* @param toCheckList List of modules to check (from createToCheckList).
|
|
* @param response WS call response.
|
|
* @param result Object where to store the result.
|
|
* @param previousTime Time of the previous check updates execution. If set, modules downloaded
|
|
* after this time will be ignored.
|
|
* @return Result.
|
|
*/
|
|
protected treatCheckUpdatesResult(
|
|
toCheckList: CheckUpdatesToCheckWSParam[],
|
|
response: CoreCourseCheckUpdatesWSResponse,
|
|
result: CourseUpdates,
|
|
previousTime?: number,
|
|
): CourseUpdates {
|
|
// Format the response to index it by module ID.
|
|
CoreUtils.arrayToObject<false | CheckUpdatesWSInstance>(response.instances, 'id', result);
|
|
|
|
// Treat warnings, adding the not supported modules.
|
|
response.warnings?.forEach((warning) => {
|
|
if (warning.warningcode == 'missingcallback') {
|
|
result[warning.itemid || -1] = false;
|
|
}
|
|
});
|
|
|
|
if (previousTime) {
|
|
// Remove from the list the modules downloaded after previousTime.
|
|
toCheckList.forEach((entry) => {
|
|
if (result[entry.id] && entry.since > previousTime) {
|
|
delete result[entry.id];
|
|
}
|
|
});
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Update the status of a module in the "cache".
|
|
*
|
|
* @param status New status.
|
|
* @param component Package's component.
|
|
* @param componentId An ID to use in conjunction with the component.
|
|
* @param courseId Course ID of the module.
|
|
* @param sectionId Section ID of the module.
|
|
*/
|
|
updateStatusCache(
|
|
status: string,
|
|
component: string,
|
|
componentId?: string | number,
|
|
courseId?: number,
|
|
sectionId?: number,
|
|
): void {
|
|
const packageId = CoreFilepool.getPackageId(component, componentId);
|
|
const cachedStatus = this.statusCache.getValue<string>(packageId, 'status', true);
|
|
|
|
// If courseId/sectionId is set, store it.
|
|
this.storeCourseAndSection(packageId, courseId, sectionId);
|
|
|
|
if (cachedStatus === undefined || cachedStatus === status) {
|
|
this.statusCache.setValue(packageId, 'status', status);
|
|
|
|
return;
|
|
}
|
|
|
|
// The status has changed, notify that the section has changed.
|
|
courseId = courseId || this.statusCache.getValue(packageId, 'courseId', true);
|
|
sectionId = sectionId || this.statusCache.getValue(packageId, 'sectionId', true);
|
|
|
|
// Invalidate and set again.
|
|
this.statusCache.invalidate(packageId);
|
|
this.statusCache.setValue(packageId, 'status', status);
|
|
|
|
if (courseId && sectionId) {
|
|
const data: CoreEventSectionStatusChangedData = {
|
|
sectionId,
|
|
courseId: courseId,
|
|
};
|
|
CoreEvents.trigger(CoreEvents.SECTION_STATUS_CHANGED, data, CoreSites.getCurrentSiteId());
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
export const CoreCourseModulePrefetchDelegate = makeSingleton(CoreCourseModulePrefetchDelegateService);
|
|
|
|
/**
|
|
* Progress of downloading a list of modules.
|
|
*/
|
|
export type CoreCourseModulesProgress = {
|
|
/**
|
|
* Number of modules downloaded so far.
|
|
*/
|
|
count: number;
|
|
|
|
/**
|
|
* Toal of modules to download.
|
|
*/
|
|
total: number;
|
|
};
|
|
|
|
/**
|
|
* Progress function for downloading a list of modules.
|
|
*
|
|
* @param data Progress data.
|
|
*/
|
|
export type CoreCourseModulesProgressFunction = (data: CoreCourseModulesProgress) => void;
|
|
|
|
/**
|
|
* Interface that all course prefetch handlers must implement.
|
|
*/
|
|
export interface CoreCourseModulePrefetchHandler extends CoreDelegateHandler {
|
|
/**
|
|
* Name of the handler.
|
|
*/
|
|
name: string;
|
|
|
|
/**
|
|
* Name of the module. It should match the "modname" of the module returned in core_course_get_contents.
|
|
*/
|
|
modName: string;
|
|
|
|
/**
|
|
* The handler's component.
|
|
*/
|
|
component: string;
|
|
|
|
/**
|
|
* The RegExp to check updates. If a module has an update whose name matches this RegExp, the module will be marked
|
|
* as outdated. This RegExp is ignored if hasUpdates function is defined.
|
|
*/
|
|
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.
|
|
*/
|
|
skipListStatus: boolean;
|
|
|
|
/**
|
|
* Get the download size of a module.
|
|
*
|
|
* @param module Module.
|
|
* @param courseId Course ID the module belongs to.
|
|
* @param single True if we're downloading a single module, false if we're downloading a whole section.
|
|
* @return Promise resolved with the size.
|
|
*/
|
|
getDownloadSize(module: CoreCourseAnyModuleData, courseId: number, single?: boolean): Promise<CoreFileSizeSum>;
|
|
|
|
/**
|
|
* Prefetch a module.
|
|
*
|
|
* @param module Module.
|
|
* @param courseId Course ID the module belongs to.
|
|
* @param single True if we're downloading a single module, false if we're downloading a whole section.
|
|
* @param dirPath Path of the directory where to store all the content files.
|
|
* @return Promise resolved when done.
|
|
*/
|
|
prefetch(module: CoreCourseAnyModuleData, courseId: number, single?: boolean, dirPath?: string): Promise<void>;
|
|
|
|
/**
|
|
* Download the module.
|
|
*
|
|
* @param module The module object returned by WS.
|
|
* @param courseId Course ID.
|
|
* @param dirPath Path of the directory where to store all the content files.
|
|
* @return Promise resolved when all content is downloaded.
|
|
*/
|
|
download(module: CoreCourseAnyModuleData, courseId: number, dirPath?: string): Promise<void>;
|
|
|
|
/**
|
|
* Invalidate the prefetched content.
|
|
*
|
|
* @param moduleId The module ID.
|
|
* @param courseId Course ID the module belongs to.
|
|
* @return Promise resolved when the data is invalidated.
|
|
*/
|
|
invalidateContent(moduleId: number, courseId: number): Promise<void>;
|
|
|
|
/**
|
|
* Check if a certain module can use core_course_check_updates to check if it has updates.
|
|
* If not defined, it will assume all modules can be checked.
|
|
* The modules that return false will always be shown as outdated when they're downloaded.
|
|
*
|
|
* @param module Module.
|
|
* @param courseId Course ID the module belongs to.
|
|
* @return Whether the module can use check_updates. The promise should never be rejected.
|
|
*/
|
|
canUseCheckUpdates?(module: CoreCourseAnyModuleData, courseId: number): Promise<boolean>;
|
|
|
|
/**
|
|
* 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 module Module.
|
|
* @param status The current status.
|
|
* @param canCheck Whether the site allows checking for updates. This parameter was deprecated since app 4.0.
|
|
* @return Status to display.
|
|
*/
|
|
determineStatus?(module: CoreCourseAnyModuleData, status: string, canCheck: true): string;
|
|
|
|
/**
|
|
* Get the downloaded size of a module. If not defined, we'll use getFiles to calculate it (it can be slow).
|
|
*
|
|
* @param module Module.
|
|
* @param courseId Course ID the module belongs to.
|
|
* @return Size, or promise resolved with the size.
|
|
*/
|
|
getDownloadedSize?(module: CoreCourseAnyModuleData, courseId: number): Promise<number>;
|
|
|
|
/**
|
|
* Get the list of files of the module. If not defined, we'll assume they are in module.contents.
|
|
*
|
|
* @param module Module.
|
|
* @param courseId Course ID the module belongs to.
|
|
* @return List of files, or promise resolved with the files.
|
|
*/
|
|
getFiles?(module: CoreCourseAnyModuleData, courseId: number): Promise<(CoreWSFile | CoreCourseModuleContentFile)[]>;
|
|
|
|
/**
|
|
* Check if a certain module has updates based on the result of check updates.
|
|
*
|
|
* @param module Module.
|
|
* @param courseId Course ID the module belongs to.
|
|
* @param moduleUpdates List of updates for the module.
|
|
* @return Whether the module has updates. The promise should never be rejected.
|
|
*/
|
|
hasUpdates?(module: CoreCourseAnyModuleData, courseId: number, moduleUpdates: false | CheckUpdatesWSInstance): Promise<boolean>;
|
|
|
|
/**
|
|
* Invalidate WS calls needed to determine module status (usually, to check if module is downloadable).
|
|
* It doesn't need to invalidate check updates. It should NOT invalidate files nor all the prefetched data.
|
|
*
|
|
* @param module Module.
|
|
* @param courseId Course ID the module belongs to.
|
|
* @return Promise resolved when invalidated.
|
|
*/
|
|
invalidateModule?(module: CoreCourseAnyModuleData, courseId: number): Promise<void>;
|
|
|
|
/**
|
|
* Check if a module can be downloaded. If the function is not defined, we assume that all modules are downloadable.
|
|
*
|
|
* @param module Module.
|
|
* @param courseId Course ID the module belongs to.
|
|
* @return Whether the module can be downloaded. The promise should never be rejected.
|
|
*/
|
|
isDownloadable?(module: CoreCourseAnyModuleData, courseId: number): Promise<boolean>;
|
|
|
|
/**
|
|
* Load module contents in module.contents if they aren't loaded already. This is meant for resources.
|
|
*
|
|
* @param module Module.
|
|
* @param courseId Course ID the module belongs to.
|
|
* @return Promise resolved when done.
|
|
*/
|
|
loadContents?(module: CoreCourseAnyModuleData, courseId: number): Promise<void>;
|
|
|
|
/**
|
|
* Remove module downloaded files. If not defined, we'll use getFiles to remove them (slow).
|
|
*
|
|
* @param module Module.
|
|
* @param courseId Course ID the module belongs to.
|
|
* @return Promise resolved when done.
|
|
*/
|
|
removeFiles?(module: CoreCourseAnyModuleData, courseId: number): Promise<void>;
|
|
|
|
/**
|
|
* Sync a module.
|
|
*
|
|
* @param module Module.
|
|
* @param courseId Course ID the module belongs to
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @return Promise resolved when done.
|
|
*/
|
|
sync?(module: CoreCourseAnyModuleData, courseId: number, siteId?: string): Promise<unknown>;
|
|
}
|
|
|
|
type ToCheckList = {
|
|
toCheck: CheckUpdatesToCheckWSParam[];
|
|
cannotUse: CoreCourseModuleData[];
|
|
};
|
|
|
|
/**
|
|
* Course updates.
|
|
*/
|
|
type CourseUpdates = Record<number, false | CheckUpdatesWSInstance>;
|
|
|
|
/**
|
|
* Status data about a list of modules.
|
|
*/
|
|
export type CoreCourseModulesStatus = {
|
|
total: number; // Number of modules.
|
|
status: string; // Status of the list of modules.
|
|
[CoreConstants.NOT_DOWNLOADED]: CoreCourseModuleData[]; // Modules with state NOT_DOWNLOADED.
|
|
[CoreConstants.DOWNLOADED]: CoreCourseModuleData[]; // Modules with state DOWNLOADED.
|
|
[CoreConstants.DOWNLOADING]: CoreCourseModuleData[]; // Modules with state DOWNLOADING.
|
|
[CoreConstants.OUTDATED]: CoreCourseModuleData[]; // Modules with state OUTDATED.
|
|
};
|
|
|
|
/**
|
|
* Data for an ongoing module prefetch.
|
|
*/
|
|
type OngoingPrefetch = {
|
|
promise: Promise<void>; // Prefetch promise.
|
|
observable: Subject<CoreCourseModulesProgress>; // Observable to notify the download progress.
|
|
subscriptions: Subscription[]; // Subscriptions that are currently listening the progress.
|
|
};
|
|
|
|
/**
|
|
* Params of core_course_check_updates WS.
|
|
*/
|
|
export type CoreCourseCheckUpdatesWSParams = {
|
|
courseid: number; // Course id to check.
|
|
tocheck: CheckUpdatesToCheckWSParam[]; // Instances to check.
|
|
filter?: string[]; // Check only for updates in these areas.
|
|
};
|
|
|
|
/**
|
|
* Data to send in tocheck parameter.
|
|
*/
|
|
type CheckUpdatesToCheckWSParam = {
|
|
contextlevel: string; // The context level for the file location. Only module supported right now.
|
|
id: number; // Context instance id.
|
|
since: number; // Check updates since this time stamp.
|
|
};
|
|
|
|
/**
|
|
* Data returned by core_course_check_updates WS.
|
|
*/
|
|
export type CoreCourseCheckUpdatesWSResponse = {
|
|
instances: CheckUpdatesWSInstance[];
|
|
warnings?: CoreWSExternalWarning[];
|
|
};
|
|
|
|
/**
|
|
* Instance data returned by the WS.
|
|
*/
|
|
type CheckUpdatesWSInstance = {
|
|
contextlevel: string; // The context level.
|
|
id: number; // Instance id.
|
|
updates: {
|
|
name: string; // Name of the area updated.
|
|
timeupdated?: number; // Last time was updated.
|
|
itemids?: number[]; // The ids of the items updated.
|
|
}[];
|
|
};
|