2133 lines
78 KiB
TypeScript
2133 lines
78 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 { Params } from '@angular/router';
|
|
|
|
import { CoreNetwork } from '@services/network';
|
|
import { CoreEvents } from '@singletons/events';
|
|
import { CoreLogger } from '@singletons/logger';
|
|
import { CoreSitesCommonWSOptions, CoreSites, CoreSitesReadingStrategy } from '@services/sites';
|
|
import { CoreTimeUtils } from '@services/utils/time';
|
|
import { CoreUtils } from '@services/utils/utils';
|
|
import { CoreSite } from '@classes/sites/site';
|
|
import { CoreConstants, DownloadStatus } from '@/core/constants';
|
|
import { makeSingleton, Translate } from '@singletons';
|
|
import { CoreStatusWithWarningsWSResponse, CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws';
|
|
|
|
import {
|
|
CoreCourseStatusDBRecord,
|
|
CoreCourseViewedModulesDBPrimaryKeys,
|
|
CoreCourseViewedModulesDBRecord,
|
|
COURSE_STATUS_TABLE,
|
|
COURSE_VIEWED_MODULES_PRIMARY_KEYS,
|
|
COURSE_VIEWED_MODULES_TABLE,
|
|
} from './database/course';
|
|
import { CoreCourseOffline } from './course-offline';
|
|
import { CoreError } from '@classes/errors/error';
|
|
import {
|
|
CoreCourseAnyCourseData,
|
|
CoreCourses,
|
|
CoreCoursesProvider,
|
|
} from '../../courses/services/courses';
|
|
import { CoreDomUtils } from '@services/utils/dom';
|
|
import { CoreWSError } from '@classes/errors/wserror';
|
|
import { CoreCourseHelper, CoreCourseModuleData, CoreCourseModuleCompletionData } from './course-helper';
|
|
import { CoreCourseFormatDelegate } from './format-delegate';
|
|
import { CoreCronDelegate } from '@services/cron';
|
|
import { CoreCourseLogCronHandler } from './handlers/log-cron';
|
|
import { CoreSitePlugins } from '@features/siteplugins/services/siteplugins';
|
|
import { CoreCourseAutoSyncData, CoreCourseSyncProvider } from './sync';
|
|
import { CoreTagItem } from '@features/tag/services/tag';
|
|
import { CoreNavigationOptions, CoreNavigator } from '@services/navigator';
|
|
import { CoreCourseModuleDelegate } from './module-delegate';
|
|
import { lazyMap, LazyMap } from '@/core/utils/lazy-map';
|
|
import { asyncInstance, AsyncInstance } from '@/core/utils/async-instance';
|
|
import { CoreDatabaseTable } from '@classes/database/database-table';
|
|
import { CoreDatabaseCachingStrategy } from '@classes/database/database-table-proxy';
|
|
import { CorePlatform } from '@services/platform';
|
|
import { asyncObservable } from '@/core/utils/rxjs';
|
|
import { firstValueFrom } from 'rxjs';
|
|
import { map } from 'rxjs/operators';
|
|
import { CoreSiteWSPreSets, WSObservable } from '@classes/sites/authenticated-site';
|
|
import { CoreLoadings } from '@services/loadings';
|
|
import { CoreArray } from '@singletons/array';
|
|
import { CoreText } from '@singletons/text';
|
|
import { ArrayElement } from '@/core/utils/types';
|
|
|
|
const ROOT_CACHE_KEY = 'mmCourse:';
|
|
|
|
export type CoreCourseProgressUpdated = { progress: number; courseId: number };
|
|
|
|
declare module '@singletons/events' {
|
|
|
|
/**
|
|
* Augment CoreEventsData interface with events specific to this service.
|
|
*
|
|
* @see https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation
|
|
*/
|
|
export interface CoreEventsData {
|
|
[CoreCourseSyncProvider.AUTO_SYNCED]: CoreCourseAutoSyncData;
|
|
[CoreCourseProvider.PROGRESS_UPDATED]: CoreCourseProgressUpdated;
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Course Module completion status enumeration.
|
|
*/
|
|
export const enum CoreCourseModuleCompletionStatus {
|
|
COMPLETION_INCOMPLETE = 0,
|
|
COMPLETION_COMPLETE = 1,
|
|
COMPLETION_COMPLETE_PASS = 2,
|
|
COMPLETION_COMPLETE_FAIL = 3,
|
|
}
|
|
|
|
/**
|
|
* @deprecated since 4.3 Not used anymore.
|
|
*/
|
|
export const enum CoreCourseCompletionMode {
|
|
FULL = 'full',
|
|
BASIC = 'basic',
|
|
}
|
|
|
|
/**
|
|
* Completion tracking valid values.
|
|
*/
|
|
export const enum CoreCourseModuleCompletionTracking {
|
|
COMPLETION_TRACKING_NONE = 0,
|
|
COMPLETION_TRACKING_MANUAL = 1,
|
|
COMPLETION_TRACKING_AUTOMATIC = 2,
|
|
}
|
|
|
|
export const CoreCourseAccessDataType = {
|
|
ACCESS_GUEST: 'courses_access_guest', // eslint-disable-line @typescript-eslint/naming-convention
|
|
ACCESS_DEFAULT: 'courses_access_default', // eslint-disable-line @typescript-eslint/naming-convention
|
|
} as const;
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
|
export type CoreCourseAccessDataType = typeof CoreCourseAccessDataType[keyof typeof CoreCourseAccessDataType];
|
|
|
|
/**
|
|
* Service that provides some features regarding a course.
|
|
*/
|
|
@Injectable({ providedIn: 'root' })
|
|
export class CoreCourseProvider {
|
|
|
|
static readonly ALL_SECTIONS_ID = -2;
|
|
static readonly STEALTH_MODULES_SECTION_ID = -1;
|
|
static readonly ALL_COURSES_CLEARED = -1;
|
|
static readonly PROGRESS_UPDATED = 'progress_updated';
|
|
|
|
/**
|
|
* @deprecated since 4.4 Not used anymore. Use CoreCourseAccessDataType instead.
|
|
*/
|
|
static readonly ACCESS_GUEST = CoreCourseAccessDataType.ACCESS_GUEST;
|
|
/**
|
|
* @deprecated since 4.4 Not used anymore. Use CoreCourseAccessDataType instead.
|
|
*/
|
|
static readonly ACCESS_DEFAULT = CoreCourseAccessDataType.ACCESS_DEFAULT;
|
|
|
|
static readonly COMPONENT = 'CoreCourse';
|
|
|
|
static readonly CORE_MODULES = [
|
|
'assign', 'bigbluebuttonbn', 'book', 'chat', 'choice', 'data', 'feedback', 'folder', 'forum', 'glossary', 'h5pactivity',
|
|
'imscp', 'label', 'lesson', 'lti', 'page', 'quiz', 'resource', 'scorm', 'survey', 'url', 'wiki', 'workshop',
|
|
];
|
|
|
|
protected logger: CoreLogger;
|
|
protected statusTables: LazyMap<AsyncInstance<CoreDatabaseTable<CoreCourseStatusDBRecord>>>;
|
|
protected viewedModulesTables: LazyMap<
|
|
AsyncInstance<CoreDatabaseTable<CoreCourseViewedModulesDBRecord, CoreCourseViewedModulesDBPrimaryKeys, never>>
|
|
>;
|
|
|
|
constructor() {
|
|
this.logger = CoreLogger.getInstance('CoreCourseProvider');
|
|
this.statusTables = lazyMap(
|
|
siteId => asyncInstance(
|
|
() => CoreSites.getSiteTable(COURSE_STATUS_TABLE, {
|
|
siteId,
|
|
config: { cachingStrategy: CoreDatabaseCachingStrategy.Eager },
|
|
onDestroy: () => delete this.statusTables[siteId],
|
|
}),
|
|
),
|
|
);
|
|
|
|
this.viewedModulesTables = lazyMap(
|
|
siteId => asyncInstance(
|
|
() => CoreSites.getSiteTable<CoreCourseViewedModulesDBRecord, CoreCourseViewedModulesDBPrimaryKeys, never>(
|
|
COURSE_VIEWED_MODULES_TABLE,
|
|
{
|
|
siteId,
|
|
config: { cachingStrategy: CoreDatabaseCachingStrategy.None },
|
|
primaryKeyColumns: [...COURSE_VIEWED_MODULES_PRIMARY_KEYS],
|
|
rowIdColumn: null,
|
|
onDestroy: () => delete this.viewedModulesTables[siteId],
|
|
},
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Initialize.
|
|
*/
|
|
initialize(): void {
|
|
CorePlatform.resume.subscribe(() => {
|
|
// Run the handler the app is open to keep user in online status.
|
|
setTimeout(() => {
|
|
CoreUtils.ignoreErrors(
|
|
CoreCronDelegate.forceCronHandlerExecution(CoreCourseLogCronHandler.name),
|
|
);
|
|
}, 1000);
|
|
});
|
|
|
|
CoreEvents.on(CoreEvents.LOGIN, () => {
|
|
setTimeout(() => {
|
|
// Ignore errors here, since probably login is not complete: it happens on token invalid.
|
|
CoreUtils.ignoreErrors(
|
|
CoreCronDelegate.forceCronHandlerExecution(CoreCourseLogCronHandler.name),
|
|
);
|
|
}, 1000);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Check if the get course blocks WS is available in current site.
|
|
*
|
|
* @param site Site to check. If not defined, current site.
|
|
* @returns Whether it's available.
|
|
* @since 3.7
|
|
*/
|
|
canGetCourseBlocks(site?: CoreSite): boolean {
|
|
site = site || CoreSites.getCurrentSite();
|
|
|
|
return !!site && site.isVersionGreaterEqualThan('3.7');
|
|
}
|
|
|
|
/**
|
|
* Check whether the site supports requesting stealth modules.
|
|
*
|
|
* @param site Site. If not defined, current site.
|
|
* @returns Whether the site supports requesting stealth modules.
|
|
* @since 3.5.3, 3.6
|
|
*/
|
|
canRequestStealthModules(site?: CoreSite): boolean {
|
|
site = site || CoreSites.getCurrentSite();
|
|
|
|
return !!site && site.isVersionGreaterEqualThan('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 courseId Course ID.
|
|
* @param completion Completion status of the module.
|
|
*/
|
|
checkModuleCompletion(courseId: number, completion?: CoreCourseModuleCompletionData): void {
|
|
if (completion && this.isIncompleteAutomaticCompletion(completion)) {
|
|
this.invalidateSections(courseId).finally(() => {
|
|
CoreEvents.trigger(CoreEvents.COMPLETION_MODULE_VIEWED, {
|
|
courseId: courseId,
|
|
cmId: completion.cmid,
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Given some completion data, return whether it's an automatic completion that hasn't been completed yet.
|
|
*
|
|
* @param completion Completion data.
|
|
* @returns Whether it's an automatic completion that hasn't been completed yet.
|
|
*/
|
|
isIncompleteAutomaticCompletion(completion: CoreCourseModuleCompletionData): boolean {
|
|
return completion.tracking === CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_AUTOMATIC &&
|
|
completion.state === CoreCourseModuleCompletionStatus.COMPLETION_INCOMPLETE;
|
|
}
|
|
|
|
/**
|
|
* Check whether a course has indentation enabled.
|
|
*
|
|
* @param site Site.
|
|
* @param courseId Course id.
|
|
* @returns Whether indentation is enabled.
|
|
*/
|
|
async isCourseIndentationEnabled(site: CoreSite, courseId: number): Promise<boolean> {
|
|
if (!site.isVersionGreaterEqualThan('4.0')) {
|
|
return false;
|
|
}
|
|
|
|
const course = await CoreCourses.getCourseByField('id', courseId, site.id);
|
|
const formatOptions = CoreUtils.objectToKeyValueMap(
|
|
course.courseformatoptions ?? [],
|
|
'name',
|
|
'value',
|
|
) as { indentation?: string };
|
|
|
|
return formatOptions.indentation === '1';
|
|
}
|
|
|
|
/**
|
|
* Clear all courses status in a site.
|
|
*
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @returns Promise resolved when all status are cleared.
|
|
*/
|
|
async clearAllCoursesStatus(siteId?: string): Promise<void> {
|
|
const site = await CoreSites.getSite(siteId);
|
|
this.logger.debug('Clear all course status for site ' + site.id);
|
|
|
|
await this.statusTables[site.getId()].delete();
|
|
this.triggerCourseStatusChanged(
|
|
CoreCourseProvider.ALL_COURSES_CLEARED,
|
|
DownloadStatus.DOWNLOADABLE_NOT_DOWNLOADED,
|
|
site.id,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Check if the current view is a certain course initial page.
|
|
*
|
|
* @param courseId Course ID.
|
|
* @returns Whether the current view is a certain course.
|
|
*/
|
|
currentViewIsCourse(courseId: number): boolean {
|
|
const route = CoreNavigator.getCurrentRoute({ routeData: { isCourseIndex: true } });
|
|
|
|
if (!route) {
|
|
return false;
|
|
}
|
|
|
|
return Number(CoreNavigator.getRouteParams(route).courseId) == courseId;
|
|
}
|
|
|
|
/**
|
|
* Get completion status of all the activities in a course for a certain user.
|
|
*
|
|
* @param courseId Course ID.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @param userId User ID. If not defined, current user.
|
|
* @param forceCache True if it should return cached data. Has priority over ignoreCache.
|
|
* @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down).
|
|
* @param includeOffline True if it should load offline data in the completion status.
|
|
* @returns Promise resolved with the completion statuses: object where the key is module ID.
|
|
*/
|
|
async getActivitiesCompletionStatus(
|
|
courseId: number,
|
|
siteId?: string,
|
|
userId?: number,
|
|
forceCache: boolean = false,
|
|
ignoreCache: boolean = false,
|
|
includeOffline: boolean = true,
|
|
): Promise<Record<string, CoreCourseCompletionActivityStatus>> {
|
|
|
|
const site = await CoreSites.getSite(siteId);
|
|
userId = userId || site.getUserId();
|
|
|
|
this.logger.debug(`Getting completion status for user ${userId} in course ${courseId}`);
|
|
|
|
const params: CoreCompletionGetActivitiesCompletionStatusWSParams = {
|
|
courseid: courseId,
|
|
userid: userId,
|
|
};
|
|
const preSets: CoreSiteWSPreSets = {
|
|
cacheKey: this.getActivitiesCompletionCacheKey(courseId, userId),
|
|
};
|
|
|
|
if (forceCache) {
|
|
preSets.omitExpires = true;
|
|
} else if (ignoreCache) {
|
|
preSets.getFromCache = false;
|
|
preSets.emergencyCache = false;
|
|
}
|
|
|
|
const data = await site.read<CoreCourseCompletionActivityStatusWSResponse>(
|
|
'core_completion_get_activities_completion_status',
|
|
params,
|
|
preSets,
|
|
);
|
|
|
|
if (!data || !data.statuses) {
|
|
throw Error('WS core_completion_get_activities_completion_status failed');
|
|
}
|
|
|
|
const completionStatus = CoreUtils.arrayToObject(data.statuses, 'cmid');
|
|
if (!includeOffline) {
|
|
return completionStatus;
|
|
}
|
|
|
|
try {
|
|
// Now get the offline completion (if any).
|
|
const offlineCompletions = await CoreCourseOffline.getCourseManualCompletions(courseId, site.id);
|
|
|
|
offlineCompletions.forEach((offlineCompletion) => {
|
|
|
|
if (offlineCompletion && 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 === CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_MANUAL) {
|
|
onlineCompletion.state = offlineCompletion.completed;
|
|
onlineCompletion.offline = true;
|
|
}
|
|
}
|
|
});
|
|
|
|
return completionStatus;
|
|
} catch {
|
|
// Ignore errors.
|
|
return completionStatus;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get cache key for activities completion WS calls.
|
|
*
|
|
* @param courseId Course ID.
|
|
* @param userId User ID.
|
|
* @returns Cache key.
|
|
*/
|
|
protected getActivitiesCompletionCacheKey(courseId: number, userId: number): string {
|
|
return ROOT_CACHE_KEY + 'activitiescompletion:' + courseId + ':' + userId;
|
|
}
|
|
|
|
/**
|
|
* Get certain module viewed records in the app.
|
|
*
|
|
* @param ids Module IDs.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @returns Promise resolved with map of last module viewed data.
|
|
*/
|
|
async getCertainModulesViewed(ids: number[] = [], siteId?: string): Promise<Record<number, CoreCourseViewedModulesDBRecord>> {
|
|
if (!ids.length) {
|
|
return {};
|
|
}
|
|
|
|
const site = await CoreSites.getSite(siteId);
|
|
const entries = await this.viewedModulesTables[site.getId()].getManyWhere({
|
|
sql: `cmId IN (${ids.map(() => '?').join(', ')})`,
|
|
sqlParams: ids,
|
|
js: (record) => ids.includes(record.cmId),
|
|
});
|
|
|
|
return CoreUtils.arrayToObject(entries, 'cmId');
|
|
}
|
|
|
|
/**
|
|
* Get course blocks.
|
|
*
|
|
* @param courseId Course ID.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @returns Promise resolved with the list of blocks.
|
|
* @since 3.7
|
|
*/
|
|
getCourseBlocks(courseId: number, siteId?: string): Promise<CoreCourseBlock[]> {
|
|
return firstValueFrom(this.getCourseBlocksObservable(courseId, { siteId }));
|
|
}
|
|
|
|
/**
|
|
* Get course blocks.
|
|
*
|
|
* @param courseId Course ID.
|
|
* @param options Options.
|
|
* @returns Observable that returns the blocks.
|
|
* @since 3.7
|
|
*/
|
|
getCourseBlocksObservable(courseId: number, options: CoreSitesCommonWSOptions = {}): WSObservable<CoreCourseBlock[]> {
|
|
return asyncObservable(async () => {
|
|
const site = await CoreSites.getSite(options.siteId);
|
|
|
|
const params: CoreBlockGetCourseBlocksWSParams = {
|
|
courseid: courseId,
|
|
returncontents: true,
|
|
};
|
|
const preSets: CoreSiteWSPreSets = {
|
|
cacheKey: this.getCourseBlocksCacheKey(courseId),
|
|
updateFrequency: CoreSite.FREQUENCY_RARELY,
|
|
...CoreSites.getReadingStrategyPreSets(options.readingStrategy),
|
|
};
|
|
|
|
return site.readObservable<CoreCourseBlocksWSResponse>('core_block_get_course_blocks', params, preSets).pipe(
|
|
map(result => result.blocks),
|
|
);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get cache key for course blocks WS calls.
|
|
*
|
|
* @param courseId Course ID.
|
|
* @returns Cache key.
|
|
*/
|
|
protected getCourseBlocksCacheKey(courseId: number): string {
|
|
return ROOT_CACHE_KEY + 'courseblocks:' + courseId;
|
|
}
|
|
|
|
/**
|
|
* Get the data stored for a course.
|
|
*
|
|
* @param courseId Course ID.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @returns Promise resolved with the data.
|
|
*/
|
|
async getCourseStatusData(courseId: number, siteId?: string): Promise<CoreCourseStatusDBRecord> {
|
|
const site = await CoreSites.getSite(siteId);
|
|
const entry = await this.statusTables[site.getId()].getOneByPrimaryKey({ id: courseId });
|
|
if (!entry) {
|
|
throw Error('No entry found on course status table');
|
|
}
|
|
|
|
return entry;
|
|
}
|
|
|
|
/**
|
|
* Get a course status.
|
|
*
|
|
* @param courseId Course ID.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @returns Promise resolved with the status.
|
|
*/
|
|
async getCourseStatus(courseId: number, siteId?: string): Promise<DownloadStatus> {
|
|
try {
|
|
const entry = await this.getCourseStatusData(courseId, siteId);
|
|
|
|
return entry.status || DownloadStatus.DOWNLOADABLE_NOT_DOWNLOADED;
|
|
} catch {
|
|
return DownloadStatus.DOWNLOADABLE_NOT_DOWNLOADED;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Obtain ids of downloaded courses.
|
|
*
|
|
* @param siteId Site id.
|
|
* @returns Resolves with an array containing downloaded course ids.
|
|
*/
|
|
async getDownloadedCourseIds(siteId?: string): Promise<number[]> {
|
|
const downloadedStatuses: DownloadStatus[] =
|
|
[DownloadStatus.DOWNLOADED, DownloadStatus.DOWNLOADING, DownloadStatus.OUTDATED];
|
|
const site = await CoreSites.getSite(siteId);
|
|
const entries = await this.statusTables[site.getId()].getManyWhere({
|
|
sql: 'status IN (?,?,?)',
|
|
sqlParams: downloadedStatuses,
|
|
js: ({ status }) => downloadedStatuses.includes(status),
|
|
});
|
|
|
|
return entries.map((entry) => entry.id);
|
|
}
|
|
|
|
/**
|
|
* Get last module viewed in the app for a course.
|
|
*
|
|
* @param id Course ID.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @returns Promise resolved with last module viewed data, undefined if none.
|
|
*/
|
|
async getLastModuleViewed(id: number, siteId?: string): Promise<CoreCourseViewedModulesDBRecord | undefined> {
|
|
const viewedModules = await this.getViewedModules(id, siteId);
|
|
|
|
return viewedModules[0];
|
|
}
|
|
|
|
/**
|
|
* Get a module from Moodle.
|
|
*
|
|
* @param moduleId The module ID.
|
|
* @param courseId The course ID. Recommended to speed up the process and minimize data usage.
|
|
* @param sectionId The section ID.
|
|
* @param preferCache True if shouldn't call WS if data is cached, false otherwise.
|
|
* @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.
|
|
* @param 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.
|
|
* @returns Promise resolved with the module.
|
|
*/
|
|
async getModule(
|
|
moduleId: number,
|
|
courseId?: number,
|
|
sectionId?: number,
|
|
preferCache: boolean = false,
|
|
ignoreCache: boolean = false,
|
|
siteId?: string,
|
|
modName?: string,
|
|
): Promise<CoreCourseModuleData> {
|
|
siteId = siteId || CoreSites.getCurrentSiteId();
|
|
|
|
// Helper function to do the WS request without processing the result.
|
|
const doRequest = async (
|
|
site: CoreSite,
|
|
courseId: number,
|
|
moduleId: number,
|
|
modName: string | undefined,
|
|
includeStealth: boolean,
|
|
preferCache: boolean,
|
|
): Promise<CoreCourseGetContentsWSSection[]> => {
|
|
const params: CoreCourseGetContentsParams = {
|
|
courseid: courseId,
|
|
};
|
|
params.options = [];
|
|
|
|
const preSets: CoreSiteWSPreSets = {
|
|
omitExpires: preferCache,
|
|
updateFrequency: CoreSite.FREQUENCY_RARELY,
|
|
};
|
|
|
|
if (includeStealth) {
|
|
params.options.push({
|
|
name: 'includestealthmodules',
|
|
value: true,
|
|
});
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
try {
|
|
const sections = await site.read<CoreCourseGetContentsWSResponse>('core_course_get_contents', params, preSets);
|
|
|
|
return sections;
|
|
} catch {
|
|
// The module might still be cached by a request with different parameters.
|
|
if (!ignoreCache && !CoreNetwork.isOnline()) {
|
|
if (includeStealth) {
|
|
// Older versions didn't include the includestealthmodules option.
|
|
return doRequest(site, courseId, moduleId, modName, false, true);
|
|
} else if (modName) {
|
|
// Falback to the request for the given moduleId only.
|
|
return doRequest(site, courseId, moduleId, undefined, this.canRequestStealthModules(site), true);
|
|
}
|
|
}
|
|
|
|
throw Error('WS core_course_get_contents failed, cache ignored');
|
|
}
|
|
};
|
|
|
|
if (!courseId) {
|
|
// No courseId passed, try to retrieve it.
|
|
const module = await this.getModuleBasicInfo(
|
|
moduleId,
|
|
{ siteId, readingStrategy: CoreSitesReadingStrategy.PREFER_CACHE },
|
|
);
|
|
courseId = module.course;
|
|
sectionId = module.section;
|
|
}
|
|
|
|
const site = await CoreSites.getSite(siteId);
|
|
let sections: CoreCourseGetContentsWSSection[];
|
|
try {
|
|
// We have courseId, we can use core_course_get_contents for compatibility.
|
|
this.logger.debug(`Getting module ${moduleId} in course ${courseId}`);
|
|
|
|
sections = await doRequest(site, courseId, 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;
|
|
}
|
|
|
|
sections = await firstValueFrom(this.callGetSectionsWS(site, courseId, {
|
|
excludeModules: false,
|
|
excludeContents: false,
|
|
preSets,
|
|
}));
|
|
}
|
|
|
|
let foundModule: CoreCourseGetContentsWSModule | undefined;
|
|
|
|
const foundSection = sections.find((section) => {
|
|
if (section.id != CoreCourseProvider.STEALTH_MODULES_SECTION_ID &&
|
|
sectionId !== undefined &&
|
|
sectionId != section.id
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
foundModule = section.modules.find((module) => module.id == moduleId);
|
|
|
|
return !!foundModule;
|
|
});
|
|
|
|
if (foundSection && foundModule) {
|
|
return this.addAdditionalModuleData(foundModule, courseId, foundSection.id);
|
|
}
|
|
|
|
throw new CoreError(Translate.instant('core.course.modulenotfound'));
|
|
}
|
|
|
|
/**
|
|
* Add some additional info to course module.
|
|
*
|
|
* @param module Module.
|
|
* @param courseId Course ID of the module.
|
|
* @param sectionId Section ID of the module.
|
|
* @returns Module with additional info.
|
|
*/
|
|
protected addAdditionalModuleData(
|
|
module: CoreCourseGetContentsWSModule,
|
|
courseId: number,
|
|
sectionId: number,
|
|
): CoreCourseModuleData {
|
|
let completionData: CoreCourseModuleCompletionData | undefined = undefined;
|
|
|
|
if (module.completiondata && module.completion) {
|
|
completionData = {
|
|
...module.completiondata,
|
|
tracking: module.completion,
|
|
cmid: module.id,
|
|
courseId,
|
|
};
|
|
}
|
|
|
|
return {
|
|
...module,
|
|
course: courseId,
|
|
section: sectionId,
|
|
completiondata: completionData,
|
|
availabilityinfo: this.treatAvailablityInfo(module.availabilityinfo),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Gets a module basic info by module ID.
|
|
*
|
|
* @param moduleId Module ID.
|
|
* @param options Comon site WS options.
|
|
* @returns Promise resolved with the module's info.
|
|
*/
|
|
async getModuleBasicInfo(moduleId: number, options: CoreSitesCommonWSOptions = {}): Promise<CoreCourseModuleBasicInfo> {
|
|
const site = await CoreSites.getSite(options.siteId);
|
|
const params: CoreCourseGetCourseModuleWSParams = {
|
|
cmid: moduleId,
|
|
};
|
|
const preSets: CoreSiteWSPreSets = {
|
|
cacheKey: this.getModuleCacheKey(moduleId),
|
|
updateFrequency: CoreSite.FREQUENCY_RARELY,
|
|
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
|
};
|
|
const response = await site.read<CoreCourseGetCourseModuleWSResponse>('core_course_get_course_module', params, preSets);
|
|
|
|
if (response.warnings && response.warnings.length) {
|
|
throw new CoreWSError(response.warnings[0]);
|
|
}
|
|
|
|
return response.cm;
|
|
}
|
|
|
|
/**
|
|
* Gets a module basic grade info by module ID.
|
|
*
|
|
* @param moduleId Module ID.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @returns Promise resolved with the module's grade info.
|
|
*/
|
|
async getModuleBasicGradeInfo(moduleId: number, siteId?: string): Promise<CoreCourseModuleGradeInfo | undefined> {
|
|
const info = await this.getModuleBasicInfo(moduleId, { siteId });
|
|
|
|
if (
|
|
info.grade !== undefined ||
|
|
info.advancedgrading !== undefined ||
|
|
info.outcomes !== undefined
|
|
) {
|
|
return {
|
|
advancedgrading: info.advancedgrading,
|
|
grade: info.grade,
|
|
gradecat: info.gradecat,
|
|
gradepass: info.gradepass,
|
|
outcomes: info.outcomes,
|
|
scale: info.scale,
|
|
};
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Gets a module basic info by instance.
|
|
*
|
|
* @param instanceId Instance ID.
|
|
* @param moduleName Name of the module. E.g. 'glossary'.
|
|
* @param options Comon site WS options.
|
|
* @returns Promise resolved with the module's info.
|
|
*/
|
|
async getModuleBasicInfoByInstance(
|
|
instanceId: number,
|
|
moduleName: string,
|
|
options: CoreSitesCommonWSOptions = {},
|
|
): Promise<CoreCourseModuleBasicInfo> {
|
|
const site = await CoreSites.getSite(options.siteId);
|
|
|
|
const params: CoreCourseGetCourseModuleByInstanceWSParams = {
|
|
instance: instanceId,
|
|
module: moduleName,
|
|
};
|
|
|
|
const preSets: CoreSiteWSPreSets = {
|
|
cacheKey: this.getModuleBasicInfoByInstanceCacheKey(instanceId, moduleName),
|
|
updateFrequency: CoreSite.FREQUENCY_RARELY,
|
|
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
|
};
|
|
|
|
const response: CoreCourseGetCourseModuleWSResponse =
|
|
await site.read('core_course_get_course_module_by_instance', params, preSets);
|
|
|
|
if (response.warnings && response.warnings.length) {
|
|
throw new CoreWSError(response.warnings[0]);
|
|
} else if (response.cm) {
|
|
return response.cm;
|
|
}
|
|
|
|
throw Error('WS core_course_get_course_module_by_instance failed');
|
|
}
|
|
|
|
/**
|
|
* Get cache key for get module by instance WS calls.
|
|
*
|
|
* @param instanceId Instance ID.
|
|
* @param moduleName Name of the module. E.g. 'glossary'.
|
|
* @returns Cache key.
|
|
*/
|
|
protected getModuleBasicInfoByInstanceCacheKey(instanceId: number, moduleName: string): string {
|
|
return ROOT_CACHE_KEY + 'moduleByInstance:' + moduleName + ':' + instanceId;
|
|
}
|
|
|
|
/**
|
|
* Get cache key for module WS calls.
|
|
*
|
|
* @param moduleId Module ID.
|
|
* @returns Cache key.
|
|
*/
|
|
protected getModuleCacheKey(moduleId: number): string {
|
|
return ROOT_CACHE_KEY + 'module:' + moduleId;
|
|
}
|
|
|
|
/**
|
|
* Get cache key for module by modname WS calls.
|
|
*
|
|
* @param modName Name of the module.
|
|
* @returns Cache key.
|
|
*/
|
|
protected getModuleByModNameCacheKey(modName: string): string {
|
|
return ROOT_CACHE_KEY + 'module:modName:' + modName;
|
|
}
|
|
|
|
/**
|
|
* Returns the source to a module icon.
|
|
*
|
|
* @param moduleName The module name.
|
|
* @param modicon The mod icon string to use in case we are not using a core activity.
|
|
* @returns The IMG src.
|
|
*/
|
|
getModuleIconSrc(moduleName: string, modicon?: string, mimetypeIcon = ''): string {
|
|
if (mimetypeIcon) {
|
|
return mimetypeIcon;
|
|
}
|
|
|
|
if (!CoreCourse.isCoreModule(moduleName)) {
|
|
if (modicon) {
|
|
return modicon;
|
|
}
|
|
|
|
moduleName = 'external-tool';
|
|
}
|
|
|
|
const path = this.getModuleIconsPath();
|
|
|
|
// Use default icon on core modules.
|
|
return path + moduleName + '.svg';
|
|
}
|
|
|
|
/**
|
|
* Get the path where the module icons are stored.
|
|
*
|
|
* @returns Path.
|
|
*/
|
|
getModuleIconsPath(): string {
|
|
if (!CoreSites.getCurrentSite()?.isVersionGreaterEqualThan('4.0')) {
|
|
// @deprecatedonmoodle since 3.11.
|
|
return 'assets/img/mod_legacy/';
|
|
}
|
|
|
|
if (!CoreSites.getCurrentSite()?.isVersionGreaterEqualThan('4.4')) {
|
|
// @deprecatedonmoodle since 4.3.
|
|
return 'assets/img/mod_40/';
|
|
}
|
|
|
|
return 'assets/img/mod/';
|
|
}
|
|
|
|
/**
|
|
* Return a specific section.
|
|
*
|
|
* @param courseId The course ID.
|
|
* @param sectionId The section ID.
|
|
* @param excludeModules Do not return modules, return only the sections structure.
|
|
* @param excludeContents Do not return module contents (i.e: files inside a resource).
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @returns Promise resolved with the section.
|
|
*/
|
|
async getSection(
|
|
courseId: number,
|
|
sectionId: number,
|
|
excludeModules?: boolean,
|
|
excludeContents?: boolean,
|
|
siteId?: string,
|
|
): Promise<CoreCourseWSSection> {
|
|
|
|
if (sectionId < 0) {
|
|
throw new CoreError('Invalid section ID');
|
|
}
|
|
|
|
const sections = await this.getSections(courseId, excludeModules, excludeContents, undefined, siteId);
|
|
const section = sections.find((section) => section.id == sectionId);
|
|
|
|
if (section) {
|
|
return section;
|
|
}
|
|
|
|
throw new CoreError('Unknown section');
|
|
}
|
|
|
|
/**
|
|
* Get the course sections.
|
|
*
|
|
* @param courseId The course ID.
|
|
* @param excludeModules Do not return modules, return only the sections structure.
|
|
* @param excludeContents Do not return module contents (i.e: files inside a resource).
|
|
* @param preSets Presets to use.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @param includeStealthModules Whether to include stealth modules. Defaults to true.
|
|
* @returns The reject contains the error message, else contains the sections.
|
|
*/
|
|
getSections(
|
|
courseId: number,
|
|
excludeModules: boolean = false,
|
|
excludeContents: boolean = false,
|
|
preSets?: CoreSiteWSPreSets,
|
|
siteId?: string,
|
|
includeStealthModules: boolean = true,
|
|
): Promise<CoreCourseWSSection[]> {
|
|
return firstValueFrom(this.getSectionsObservable(courseId, {
|
|
excludeModules,
|
|
excludeContents,
|
|
includeStealthModules,
|
|
preSets,
|
|
siteId,
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Get the course sections.
|
|
*
|
|
* @param courseId The course ID.
|
|
* @param options Options.
|
|
* @returns Observable that returns the sections.
|
|
*/
|
|
getSectionsObservable(
|
|
courseId: number,
|
|
options: CoreCourseGetSectionsOptions = {},
|
|
): WSObservable<CoreCourseWSSection[]> {
|
|
return asyncObservable(async () => {
|
|
const site = await CoreSites.getSite(options.siteId);
|
|
|
|
return this.callGetSectionsWS(site, courseId, options).pipe(
|
|
map(sections => {
|
|
const siteHomeId = site.getSiteHomeId();
|
|
let showSections = true;
|
|
if (courseId === siteHomeId) {
|
|
const storedNumSections = site.getStoredConfig('numsections');
|
|
showSections = storedNumSections !== undefined && !!storedNumSections;
|
|
}
|
|
|
|
if (showSections !== undefined && !showSections && sections.length > 0) {
|
|
// Get only the last section (Main menu block section).
|
|
sections.pop();
|
|
}
|
|
|
|
// First format all the sections and their modules.
|
|
const formattedSections: CoreCourseWSSection[] = sections.map((section) => ({
|
|
...section,
|
|
availabilityinfo: this.treatAvailablityInfo(section.availabilityinfo),
|
|
modules: section.modules.map((module) => this.addAdditionalModuleData(module, courseId, section.id)),
|
|
contents: [],
|
|
}));
|
|
|
|
// Only return the root sections, subsections are included in section contents.
|
|
return this.addSectionsContents(formattedSections).filter((section) => !section.component);
|
|
}),
|
|
);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Call the WS to get the course sections.
|
|
*
|
|
* @param site Site.
|
|
* @param courseId The course ID.
|
|
* @param options Options.
|
|
* @returns Observable that returns the sections.
|
|
*/
|
|
protected callGetSectionsWS(
|
|
site: CoreSite,
|
|
courseId: number,
|
|
options: CoreCourseGetSectionsOptions = {},
|
|
): WSObservable<CoreCourseGetContentsWSSection[]> {
|
|
const preSets: CoreSiteWSPreSets = {
|
|
...options.preSets,
|
|
cacheKey: this.getSectionsCacheKey(courseId),
|
|
updateFrequency: CoreSite.FREQUENCY_RARELY,
|
|
...CoreSites.getReadingStrategyPreSets(options.readingStrategy),
|
|
};
|
|
|
|
const params: CoreCourseGetContentsParams = {
|
|
courseid: courseId,
|
|
};
|
|
params.options = [
|
|
{
|
|
name: 'excludemodules',
|
|
value: !!options.excludeModules,
|
|
},
|
|
{
|
|
name: 'excludecontents',
|
|
value: !!options.excludeContents,
|
|
},
|
|
];
|
|
|
|
if (this.canRequestStealthModules(site)) {
|
|
params.options.push({
|
|
name: 'includestealthmodules',
|
|
value: !!(options.includeStealthModules ?? true),
|
|
});
|
|
}
|
|
|
|
return site.readObservable<CoreCourseGetContentsWSSection[]>('core_course_get_contents', params, preSets);
|
|
}
|
|
|
|
/**
|
|
* Calculate and add the section contents. Section contents include modules and subsections.
|
|
*
|
|
* @param sections Sections to calculate.
|
|
* @returns Sections with contents.
|
|
*/
|
|
protected addSectionsContents(sections: CoreCourseWSSection[]): CoreCourseWSSection[] {
|
|
const subsections = sections.filter((section) => !!section.component);
|
|
const subsectionsComponents = CoreArray.unique(subsections.map(section => (section.component ?? '').replace('mod_', '')));
|
|
|
|
sections.forEach(section => {
|
|
// eslint-disable-next-line deprecation/deprecation
|
|
section.contents = section.modules.map(module => {
|
|
if (!subsectionsComponents.includes(module.modname)) {
|
|
return module;
|
|
}
|
|
|
|
// Replace the module with the subsection. If subsection not found, the module will be removed from the list.
|
|
const customData = CoreText.parseJSON<{ sectionid?: string | number }>(module.customdata ?? '{}', {});
|
|
|
|
return subsections.find(subsection => subsection.id === Number(customData.sectionid));
|
|
}).filter((content): content is (CoreCourseWSSection | CoreCourseModuleData) => content !== undefined);
|
|
});
|
|
|
|
return sections;
|
|
}
|
|
|
|
/**
|
|
* Get cache key for section WS call.
|
|
*
|
|
* @param courseId Course ID.
|
|
* @returns Cache key.
|
|
*/
|
|
protected getSectionsCacheKey(courseId: number): string {
|
|
return ROOT_CACHE_KEY + 'sections:' + courseId;
|
|
}
|
|
|
|
/**
|
|
* Given a list of sections, returns the list of modules in the sections.
|
|
* The modules are ordered in the order of appearance in the course.
|
|
*
|
|
* @param sections Sections.
|
|
* @param options Other options.
|
|
* @returns Modules.
|
|
*/
|
|
getSectionsModules<
|
|
Section extends CoreCourseWSSection,
|
|
Module = Extract<ArrayElement<Section['contents']>, CoreCourseModuleData>
|
|
>(
|
|
sections: Section[],
|
|
options: CoreCourseGetSectionsModulesOptions<Section, Module> = {},
|
|
): Module[] {
|
|
let modules: Module[] = [];
|
|
|
|
sections.forEach((section) => {
|
|
if (options.ignoreSection && options.ignoreSection(section)) {
|
|
return;
|
|
}
|
|
|
|
section.contents.forEach((modOrSubsection) => {
|
|
if (sectionContentIsModule(modOrSubsection)) {
|
|
if (options.ignoreModule && options.ignoreModule(modOrSubsection as Module)) {
|
|
return;
|
|
}
|
|
|
|
modules.push(modOrSubsection as Module);
|
|
} else {
|
|
modules = modules.concat(this.getSectionsModules([modOrSubsection], options));
|
|
}
|
|
});
|
|
});
|
|
|
|
return modules;
|
|
}
|
|
|
|
/**
|
|
* Get all viewed modules in a course, ordered by timeaccess in descending order.
|
|
*
|
|
* @param courseId Course ID.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @returns Promise resolved with the list of viewed modules.
|
|
*/
|
|
async getViewedModules(courseId: number, siteId?: string): Promise<CoreCourseViewedModulesDBRecord[]> {
|
|
const site = await CoreSites.getSite(siteId);
|
|
|
|
return this.viewedModulesTables[site.getId()].getMany({ courseId }, {
|
|
sorting: [
|
|
{ timeaccess: 'desc' },
|
|
],
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Invalidates course blocks WS call.
|
|
*
|
|
* @param courseId Course ID.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @returns Promise resolved when the data is invalidated.
|
|
*/
|
|
async invalidateCourseBlocks(courseId: number, siteId?: string): Promise<void> {
|
|
const site = await CoreSites.getSite(siteId);
|
|
|
|
await site.invalidateWsCacheForKey(this.getCourseBlocksCacheKey(courseId));
|
|
}
|
|
|
|
/**
|
|
* Invalidates module WS call.
|
|
*
|
|
* @param moduleId Module ID.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @param modName Module name. E.g. 'label', 'url', ...
|
|
* @returns Promise resolved when the data is invalidated.
|
|
*/
|
|
async invalidateModule(moduleId: number, siteId?: string, modName?: string): Promise<void> {
|
|
const site = await CoreSites.getSite(siteId);
|
|
const promises: Promise<void>[] = [];
|
|
if (modName) {
|
|
promises.push(site.invalidateWsCacheForKey(this.getModuleByModNameCacheKey(modName)));
|
|
}
|
|
promises.push(site.invalidateWsCacheForKey(this.getModuleCacheKey(moduleId)));
|
|
|
|
await Promise.all(promises);
|
|
}
|
|
|
|
/**
|
|
* Invalidates module WS call.
|
|
*
|
|
* @param id Instance ID.
|
|
* @param module Name of the module. E.g. 'glossary'.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @returns Promise resolved when the data is invalidated.
|
|
*/
|
|
async invalidateModuleByInstance(id: number, module: string, siteId?: string): Promise<void> {
|
|
const site = await CoreSites.getSite(siteId);
|
|
|
|
await site.invalidateWsCacheForKey(this.getModuleBasicInfoByInstanceCacheKey(id, module));
|
|
}
|
|
|
|
/**
|
|
* Invalidates sections WS call.
|
|
*
|
|
* @param courseId Course ID.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @param userId User ID. If not defined, current user.
|
|
* @returns Promise resolved when the data is invalidated.
|
|
*/
|
|
async invalidateSections(courseId: number, siteId?: string, userId?: number): Promise<void> {
|
|
const site = await CoreSites.getSite(siteId);
|
|
const promises: Promise<void>[] = [];
|
|
const 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());
|
|
}
|
|
|
|
await Promise.all(promises);
|
|
}
|
|
|
|
/**
|
|
* Load module contents into module.contents if they aren't loaded already.
|
|
*
|
|
* @param module Module to load the contents.
|
|
* @param courseId Not used since 4.0.
|
|
* @param sectionId The section ID.
|
|
* @param preferCache True if shouldn't call WS if data is cached, false otherwise.
|
|
* @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.
|
|
* @param 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.
|
|
* @returns Promise resolved when loaded.
|
|
*/
|
|
async loadModuleContents(
|
|
module: CoreCourseAnyModuleData,
|
|
courseId?: number,
|
|
sectionId?: number,
|
|
preferCache?: boolean,
|
|
ignoreCache?: boolean,
|
|
siteId?: string,
|
|
modName?: string,
|
|
): Promise<void> {
|
|
|
|
if (!ignoreCache && module.contents && module.contents.length) {
|
|
// Already loaded.
|
|
return;
|
|
}
|
|
|
|
const mod = await this.getModule(module.id, module.course, sectionId, preferCache, ignoreCache, siteId, modName);
|
|
|
|
if (!mod.contents) {
|
|
throw new CoreError(Translate.instant('core.course.modulenotfound'));
|
|
}
|
|
|
|
module.contents = mod.contents;
|
|
}
|
|
|
|
/**
|
|
* Get module contents. If not present, this function will try to load them into module.contents.
|
|
* It will throw an error if contents cannot be loaded.
|
|
*
|
|
* @param module Module to get its contents.
|
|
* @param courseId Not used since 4.0.
|
|
* @param sectionId The section ID.
|
|
* @param preferCache True if shouldn't call WS if data is cached, false otherwise.
|
|
* @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.
|
|
* @param 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.
|
|
* @returns Promise resolved when loaded.
|
|
*/
|
|
async getModuleContents(
|
|
module: CoreCourseModuleData,
|
|
courseId?: number,
|
|
sectionId?: number,
|
|
preferCache?: boolean,
|
|
ignoreCache?: boolean,
|
|
siteId?: string,
|
|
modName?: string,
|
|
): Promise<CoreCourseModuleContentFile[]> {
|
|
// Make sure contents are loaded.
|
|
await this.loadModuleContents(module, undefined, sectionId, preferCache, ignoreCache, siteId, modName);
|
|
|
|
if (!module.contents) {
|
|
throw new CoreError(Translate.instant('core.course.modulenotfound'));
|
|
}
|
|
|
|
return module.contents;
|
|
}
|
|
|
|
/**
|
|
* Report a course and section as being viewed.
|
|
*
|
|
* @param courseId Course ID.
|
|
* @param sectionNumber Section number.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @returns Promise resolved when the WS call is successful.
|
|
*/
|
|
async logView(courseId: number, sectionNumber?: number, siteId?: string): Promise<void> {
|
|
const params: CoreCourseViewCourseWSParams = {
|
|
courseid: courseId,
|
|
};
|
|
|
|
if (sectionNumber !== undefined) {
|
|
params.sectionnumber = sectionNumber;
|
|
}
|
|
|
|
const site = await CoreSites.getSite(siteId);
|
|
const response: CoreStatusWithWarningsWSResponse = await site.write('core_course_view_course', params);
|
|
|
|
if (!response.status) {
|
|
throw Error('WS core_course_view_course failed.');
|
|
} else {
|
|
CoreEvents.trigger(CoreCoursesProvider.EVENT_MY_COURSES_UPDATED, {
|
|
courseId: courseId,
|
|
action: CoreCoursesProvider.ACTION_VIEW,
|
|
}, site.getId());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Offline version for manually marking a module as completed.
|
|
*
|
|
* @param cmId The module ID.
|
|
* @param completed Whether the module is completed or not.
|
|
* @param courseId Course ID the module belongs to.
|
|
* @param courseName Not used since 4.0.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @returns Promise resolved when completion is successfully sent or stored.
|
|
*/
|
|
async markCompletedManually(
|
|
cmId: number,
|
|
completed: boolean,
|
|
courseId: number,
|
|
courseName?: string,
|
|
siteId?: string,
|
|
): Promise<CoreStatusWithWarningsWSResponse> {
|
|
|
|
siteId = siteId || CoreSites.getCurrentSiteId();
|
|
|
|
// Convenience function to store a completion to be synchronized later.
|
|
const storeOffline = (): Promise<CoreStatusWithWarningsWSResponse> =>
|
|
CoreCourseOffline.markCompletedManually(cmId, completed, courseId, undefined, siteId);
|
|
|
|
// The offline function requires a courseId and it could be missing because it's a calculated field.
|
|
if (!CoreNetwork.isOnline()) {
|
|
// App is offline, store the action.
|
|
return storeOffline();
|
|
}
|
|
|
|
// Try to send it to server.
|
|
try {
|
|
const result = await this.markCompletedManuallyOnline(cmId, completed, siteId);
|
|
|
|
// Data sent to server, if there is some offline data delete it now.
|
|
await CoreUtils.ignoreErrors(CoreCourseOffline.deleteManualCompletion(cmId, siteId));
|
|
|
|
// Invalidate module now, completion has changed.
|
|
await this.invalidateModule(cmId, siteId);
|
|
|
|
return result;
|
|
} catch (error) {
|
|
if (CoreUtils.isWebServiceError(error)) {
|
|
// The WebService has thrown an error, this means that responses cannot be submitted.
|
|
throw error;
|
|
} else {
|
|
// Couldn't connect to server, store it offline.
|
|
return storeOffline();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Offline version for manually marking a module as completed.
|
|
*
|
|
* @param cmId The module ID.
|
|
* @param completed Whether the module is completed or not.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @returns Promise resolved when completion is successfully sent.
|
|
*/
|
|
async markCompletedManuallyOnline(
|
|
cmId: number,
|
|
completed: boolean,
|
|
siteId?: string,
|
|
): Promise<CoreStatusWithWarningsWSResponse> {
|
|
const site = await CoreSites.getSite(siteId);
|
|
const params: CoreCompletionUpdateActivityCompletionStatusManuallyWSParams = {
|
|
cmid: cmId,
|
|
completed: completed,
|
|
};
|
|
|
|
const result = await site.write<CoreStatusWithWarningsWSResponse>(
|
|
'core_completion_update_activity_completion_status_manually',
|
|
params,
|
|
);
|
|
|
|
if (!result.status) {
|
|
if (result.warnings && result.warnings.length) {
|
|
throw new CoreWSError(result.warnings[0]);
|
|
}
|
|
|
|
throw new CoreError('Cannot change completion.');
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Check if a module has a view page. E.g. labels don't have a view page.
|
|
*
|
|
* @param module The module object.
|
|
* @returns Whether the module has a view page.
|
|
*/
|
|
moduleHasView(module: CoreCourseModuleSummary | CoreCourseModuleData): boolean {
|
|
if ('modname' in module) {
|
|
// noviewlink was introduced in 3.8.5, use supports feature as a fallback.
|
|
if (module.noviewlink ||
|
|
CoreCourseModuleDelegate.supportsFeature(module.modname, CoreConstants.FEATURE_NO_VIEW_LINK, false)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return !!module.url;
|
|
}
|
|
|
|
/**
|
|
* Check if the module is a core module.
|
|
*
|
|
* @param moduleName The module name.
|
|
* @returns Whether it's a core module.
|
|
*/
|
|
isCoreModule(moduleName: string): boolean {
|
|
// If core modules are removed for a certain version we should check the version of the site.
|
|
return CoreCourseProvider.CORE_MODULES.includes(moduleName);
|
|
}
|
|
|
|
/**
|
|
* Wait for any course format plugin to load, and open the course page.
|
|
*
|
|
* If the plugin's promise is resolved, the course page will be opened. If it is rejected, they will see an error.
|
|
* If the promise for the plugin is still in progress when the user tries to open the course, a loader
|
|
* will be displayed until it is complete, before the course page is opened. If the promise is already complete,
|
|
* they will see the result immediately.
|
|
*
|
|
* This function must be in here instead of course helper to prevent circular dependencies.
|
|
*
|
|
* @param course Course to open
|
|
* @param navOptions Navigation options that includes params to pass to the page.
|
|
* @returns Promise resolved when done.
|
|
*/
|
|
async openCourse(
|
|
course: CoreCourseAnyCourseData | { id: number },
|
|
navOptions?: CoreNavigationOptions,
|
|
): Promise<void> {
|
|
if (course.id === CoreSites.getCurrentSite()?.getSiteHomeId()) {
|
|
// Open site home.
|
|
await CoreNavigator.navigate('/main/home/site', navOptions);
|
|
|
|
return;
|
|
}
|
|
|
|
const loading = await CoreLoadings.show();
|
|
|
|
// Wait for site plugins to be fetched.
|
|
await CoreUtils.ignoreErrors(CoreSitePlugins.waitFetchPlugins());
|
|
|
|
if (!('format' in course) || course.format === undefined) {
|
|
const result = await CoreCourseHelper.getCourse(course.id);
|
|
|
|
course = result.course;
|
|
}
|
|
|
|
const format = 'format' in course && `format_${course.format}`;
|
|
|
|
if (!format || !CoreSitePlugins.sitePluginPromiseExists(`format_${format}`)) {
|
|
// No custom format plugin. We don't need to wait for anything.
|
|
loading.dismiss();
|
|
await CoreCourseFormatDelegate.openCourse(<CoreCourseAnyCourseData> course, navOptions);
|
|
|
|
return;
|
|
}
|
|
|
|
// This course uses a custom format plugin, wait for the format plugin to finish loading.
|
|
try {
|
|
await CoreSitePlugins.sitePluginLoaded(format);
|
|
|
|
// The format loaded successfully, but the handlers wont be registered until all site plugins have loaded.
|
|
if (CoreSitePlugins.sitePluginsFinishedLoading) {
|
|
return CoreCourseFormatDelegate.openCourse(<CoreCourseAnyCourseData> course, navOptions);
|
|
}
|
|
|
|
// Wait for plugins to be loaded.
|
|
await new Promise((resolve, reject) => {
|
|
const observer = CoreEvents.on(CoreEvents.SITE_PLUGINS_LOADED, () => {
|
|
observer?.off();
|
|
|
|
CoreCourseFormatDelegate.openCourse(<CoreCourseAnyCourseData> course, navOptions).then(resolve).catch(reject);
|
|
});
|
|
});
|
|
|
|
return;
|
|
} catch {
|
|
// The site plugin failed to load. The user needs to restart the app to try loading it again.
|
|
const message = Translate.instant('core.courses.errorloadplugins');
|
|
const reload = Translate.instant('core.courses.reload');
|
|
const ignore = Translate.instant('core.courses.ignore');
|
|
|
|
await CoreDomUtils.showConfirm(message, '', reload, ignore);
|
|
window.location.reload();
|
|
} finally {
|
|
loading.dismiss();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Select a certain tab in the course. Please use currentViewIsCourse() first to verify user is viewing the course.
|
|
*
|
|
* @param name Name of the tab. If not provided, course contents.
|
|
* @param params Other params.
|
|
*/
|
|
selectCourseTab(name?: string, params?: Params): void {
|
|
params = params || {};
|
|
params.name = name || '';
|
|
|
|
CoreEvents.trigger(CoreEvents.SELECT_COURSE_TAB, params);
|
|
}
|
|
|
|
/**
|
|
* Change the course status, setting it to the previous status.
|
|
*
|
|
* @param courseId Course ID.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @returns Promise resolved when the status is changed. Resolve param: new status.
|
|
*/
|
|
async setCoursePreviousStatus(courseId: number, siteId?: string): Promise<DownloadStatus> {
|
|
siteId = siteId || CoreSites.getCurrentSiteId();
|
|
|
|
this.logger.debug(`Set previous status for course ${courseId} in site ${siteId}`);
|
|
|
|
const site = await CoreSites.getSite(siteId);
|
|
const entry = await this.getCourseStatusData(courseId, siteId);
|
|
|
|
this.logger.debug(`Set previous status '${entry.status}' for course ${courseId}`);
|
|
|
|
const newData = {
|
|
id: courseId,
|
|
status: entry.previous || DownloadStatus.DOWNLOADABLE_NOT_DOWNLOADED,
|
|
updated: Date.now(),
|
|
// Going back from downloading to previous status, restore previous download time.
|
|
downloadTime: entry.status == DownloadStatus.DOWNLOADING ? entry.previousDownloadTime : entry.downloadTime,
|
|
};
|
|
|
|
await this.statusTables[site.getId()].update(newData, { id: courseId });
|
|
// Success updating, trigger event.
|
|
this.triggerCourseStatusChanged(courseId, newData.status, siteId);
|
|
|
|
return newData.status;
|
|
}
|
|
|
|
/**
|
|
* Store course status.
|
|
*
|
|
* @param courseId Course ID.
|
|
* @param status New course status.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
* @returns Promise resolved when the status is stored.
|
|
*/
|
|
async setCourseStatus(courseId: number, status: DownloadStatus, siteId?: string): Promise<void> {
|
|
siteId = siteId || CoreSites.getCurrentSiteId();
|
|
|
|
this.logger.debug(`Set status '${status}' for course ${courseId} in site ${siteId}`);
|
|
|
|
const site = await CoreSites.getSite(siteId);
|
|
let downloadTime = 0;
|
|
let previousDownloadTime = 0;
|
|
let previousStatus: DownloadStatus | undefined;
|
|
|
|
if (status === DownloadStatus.DOWNLOADING) {
|
|
// Set download time if course is now downloading.
|
|
downloadTime = CoreTimeUtils.timestamp();
|
|
}
|
|
|
|
try {
|
|
const entry = await this.getCourseStatusData(courseId, siteId);
|
|
if (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;
|
|
}
|
|
previousStatus = entry.status;
|
|
} catch {
|
|
// New entry.
|
|
}
|
|
|
|
if (previousStatus !== status) {
|
|
// Status has changed, update it.
|
|
await this.statusTables[site.getId()].insert({
|
|
id: courseId,
|
|
status: status,
|
|
previous: previousStatus,
|
|
updated: Date.now(),
|
|
downloadTime: downloadTime,
|
|
previousDownloadTime: previousDownloadTime,
|
|
});
|
|
}
|
|
|
|
// Success inserting, trigger event.
|
|
this.triggerCourseStatusChanged(courseId, status, siteId);
|
|
}
|
|
|
|
/**
|
|
* Store activity as viewed.
|
|
*
|
|
* @param courseId Chapter ID.
|
|
* @param cmId Module ID.
|
|
* @param options Other options.
|
|
* @returns Promise resolved with last chapter viewed, undefined if none.
|
|
*/
|
|
async storeModuleViewed(courseId: number, cmId: number, options: CoreCourseStoreModuleViewedOptions = {}): Promise<void> {
|
|
const site = await CoreSites.getSite(options.siteId);
|
|
|
|
const timeaccess = options.timeaccess ?? Date.now();
|
|
|
|
await this.viewedModulesTables[site.getId()].insert({
|
|
courseId,
|
|
cmId,
|
|
sectionId: options.sectionId,
|
|
timeaccess,
|
|
});
|
|
|
|
CoreEvents.trigger(CoreEvents.COURSE_MODULE_VIEWED, {
|
|
courseId,
|
|
cmId,
|
|
timeaccess,
|
|
sectionId: options.sectionId,
|
|
}, site.getId());
|
|
}
|
|
|
|
/**
|
|
* Translate a module name to current language.
|
|
*
|
|
* @param moduleName The module name.
|
|
* @param fallback Fallback text to use if not translated. Will use moduleName otherwise.
|
|
*
|
|
* @returns Translated name.
|
|
*/
|
|
translateModuleName(moduleName: string, fallback?: string): string {
|
|
const langKey = 'core.mod_' + moduleName;
|
|
const translated = Translate.instant(langKey);
|
|
|
|
return translated !== langKey ?
|
|
translated :
|
|
(fallback || moduleName);
|
|
}
|
|
|
|
/**
|
|
* Trigger COURSE_STATUS_CHANGED with the right data.
|
|
*
|
|
* @param courseId Course ID.
|
|
* @param status New course status.
|
|
* @param siteId Site ID. If not defined, current site.
|
|
*/
|
|
protected triggerCourseStatusChanged(courseId: number, status: DownloadStatus, siteId?: string): void {
|
|
CoreEvents.trigger(CoreEvents.COURSE_STATUS_CHANGED, {
|
|
courseId: courseId,
|
|
status: status,
|
|
}, siteId);
|
|
}
|
|
|
|
/**
|
|
* Treat availability info HTML.
|
|
*
|
|
* @param availabilityInfo HTML to treat.
|
|
* @returns Treated HTML.
|
|
*/
|
|
protected treatAvailablityInfo(availabilityInfo?: string): string | undefined {
|
|
if (!availabilityInfo) {
|
|
return availabilityInfo;
|
|
}
|
|
|
|
// Remove "Show more" option in 4.2 or older sites.
|
|
return CoreDomUtils.removeElementFromHtml(availabilityInfo, 'li[data-action="showmore"]');
|
|
}
|
|
|
|
/**
|
|
* Given section contents, classify them into modules and sections.
|
|
*
|
|
* @param contents Contents.
|
|
* @returns Classified contents.
|
|
*/
|
|
classifyContents<
|
|
Contents extends CoreCourseModuleOrSection,
|
|
Module = Extract<Contents, CoreCourseModuleData>,
|
|
Section = Extract<Contents, CoreCourseWSSection>,
|
|
>(contents: Contents[]): { modules: Module[]; subsections: Section[] } {
|
|
const modules: Module[] = [];
|
|
const subsections: Section[] = [];
|
|
|
|
contents.forEach((content) => {
|
|
if (sectionContentIsModule(content)) {
|
|
modules.push(content as Module);
|
|
} else {
|
|
subsections.push(content as unknown as Section);
|
|
}
|
|
});
|
|
|
|
return { modules, subsections };
|
|
}
|
|
|
|
}
|
|
|
|
export const CoreCourse = makeSingleton(CoreCourseProvider);
|
|
|
|
/**
|
|
* Type guard to detect if a section content (module or subsection) is a module.
|
|
*
|
|
* @param content Section module or subsection.
|
|
* @returns Whether section content is a module.
|
|
*/
|
|
export function sectionContentIsModule<Section extends CoreCourseWSSection, Module extends CoreCourseModuleData>(
|
|
content: Module | Section,
|
|
): content is Module {
|
|
return 'modname' in content;
|
|
}
|
|
|
|
/**
|
|
* Common options used by modules when calling a WS through CoreSite.
|
|
*/
|
|
export type CoreCourseCommonModWSOptions = CoreSitesCommonWSOptions & {
|
|
cmId?: number; // Module ID.
|
|
};
|
|
|
|
/**
|
|
* Data returned by course_summary_exporter.
|
|
*/
|
|
export type CoreCourseSummary = {
|
|
id: number; // Id.
|
|
fullname: string; // Fullname.
|
|
shortname: string; // Shortname.
|
|
idnumber: string; // Idnumber.
|
|
summary: string; // Summary.
|
|
summaryformat: number; // Summary format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN).
|
|
startdate: number; // Startdate.
|
|
enddate: number; // Enddate.
|
|
visible: boolean; // @since 3.8. Visible.
|
|
fullnamedisplay: string; // Fullnamedisplay.
|
|
viewurl: string; // Viewurl.
|
|
courseimage: string; // @since 3.6. Courseimage.
|
|
progress?: number; // @since 3.6. Progress.
|
|
hasprogress: boolean; // @since 3.6. Hasprogress.
|
|
isfavourite: boolean; // @since 3.6. Isfavourite.
|
|
hidden: boolean; // @since 3.6. Hidden.
|
|
timeaccess?: number; // @since 3.6. Timeaccess.
|
|
showshortname: boolean; // @since 3.6. Showshortname.
|
|
coursecategory: string; // @since 3.7. Coursecategory.
|
|
showactivitydates: boolean | null; // @since 3.11. Whether the activity dates are shown or not.
|
|
showcompletionconditions: boolean | null; // @since 3.11. Whether the activity completion conditions are shown or not.
|
|
timemodified?: number; // @since 4.0. Last time course settings were updated (timestamp).
|
|
};
|
|
|
|
/**
|
|
* Data returned by course_module_summary_exporter.
|
|
*/
|
|
export type CoreCourseModuleSummary = {
|
|
id: number; // Id.
|
|
name: string; // Name.
|
|
url?: string; // Url.
|
|
iconurl: string; // Iconurl.
|
|
};
|
|
|
|
/**
|
|
* Params of core_completion_get_activities_completion_status WS.
|
|
*/
|
|
type CoreCompletionGetActivitiesCompletionStatusWSParams = {
|
|
courseid: number; // Course ID.
|
|
userid: number; // User ID.
|
|
};
|
|
|
|
/**
|
|
* Data returned by core_completion_get_activities_completion_status WS.
|
|
*/
|
|
export type CoreCourseCompletionActivityStatusWSResponse = {
|
|
statuses: CoreCourseCompletionActivityStatus[]; // List of activities status.
|
|
warnings?: CoreStatusWithWarningsWSResponse[];
|
|
};
|
|
|
|
/**
|
|
* Activity status.
|
|
*/
|
|
export type CoreCourseCompletionActivityStatus = {
|
|
cmid: number; // Course module ID.
|
|
modname: string; // Activity module name.
|
|
instance: number; // Instance ID.
|
|
state: number; // Completion state value: 0 means incomplete, 1 complete, 2 complete pass, 3 complete fail.
|
|
timecompleted: number; // Timestamp for completed activity.
|
|
tracking: CoreCourseModuleCompletionTracking; // Type of tracking: 0 means none, 1 manual, 2 automatic.
|
|
overrideby?: number | null; // The user id who has overriden the status, or null.
|
|
valueused?: boolean; // Whether the completion status affects the availability of another activity.
|
|
hascompletion?: boolean; // @since 3.11. Whether this activity module has completion enabled.
|
|
isautomatic?: boolean; // @since 3.11. Whether this activity module instance tracks completion automatically.
|
|
istrackeduser?: boolean; // @since 3.11. Whether completion is being tracked for this user.
|
|
uservisible?: boolean; // @since 3.11. Whether this activity is visible to the user.
|
|
details?: { // @since 3.11. An array of completion details containing the description and status.
|
|
rulename: string; // Rule name.
|
|
rulevalue: {
|
|
status: number; // Completion status.
|
|
description: string; // Completion description.
|
|
};
|
|
}[];
|
|
isoverallcomplete?: boolean; // @since 4.4.
|
|
// Whether the overall completion state of this course module should be marked as complete or not.
|
|
offline?: boolean; // Whether the completions is offline and not yet synced.
|
|
};
|
|
|
|
/**
|
|
* Params of core_block_get_course_blocks WS.
|
|
*/
|
|
type CoreBlockGetCourseBlocksWSParams = {
|
|
courseid: number; // Course id.
|
|
returncontents?: boolean; // Whether to return the block contents.
|
|
};
|
|
|
|
/**
|
|
* Data returned by core_block_get_course_blocks WS.
|
|
*/
|
|
export type CoreCourseBlocksWSResponse = {
|
|
blocks: CoreCourseBlock[]; // List of blocks in the course.
|
|
warnings?: CoreStatusWithWarningsWSResponse[];
|
|
};
|
|
|
|
/**
|
|
* Block data type.
|
|
*/
|
|
export type CoreCourseBlock = {
|
|
instanceid: number; // Block instance id.
|
|
name: string; // Block name.
|
|
region: string; // Block region.
|
|
positionid: number; // Position id.
|
|
collapsible: boolean; // Whether the block is collapsible.
|
|
dockable: boolean; // Whether the block is dockable.
|
|
weight?: number; // Used to order blocks within a region.
|
|
visible?: boolean; // Whether the block is visible.
|
|
contents?: {
|
|
title: string; // Block title.
|
|
content: string; // Block contents.
|
|
contentformat: number; // Content format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN).
|
|
footer: string; // Block footer.
|
|
files: CoreWSExternalFile[];
|
|
}; // Block contents (if required).
|
|
configs?: { // Block instance and plugin configuration settings.
|
|
name: string; // Name.
|
|
value: string; // JSON encoded representation of the config value.
|
|
type: string; // Type (instance or plugin).
|
|
}[];
|
|
configsRecord?: Record<string, { // Block instance and plugin configuration settings.
|
|
name: string; // Name.
|
|
value: string; // JSON encoded representation of the config value.
|
|
type: string; // Type (instance or plugin).
|
|
}>;
|
|
};
|
|
|
|
/**
|
|
* Params of core_course_get_contents WS.
|
|
*/
|
|
export type CoreCourseGetContentsParams = {
|
|
courseid: number; // Course id.
|
|
options?: { // Options, used since Moodle 2.9.
|
|
/**
|
|
* The expected keys (value format) are:
|
|
*
|
|
* excludemodules (bool) Do not return modules, return only the sections structure
|
|
* excludecontents (bool) Do not return module contents (i.e: files inside a resource)
|
|
* includestealthmodules (bool) Return stealth modules for students in a special
|
|
* section (with id -1)
|
|
* sectionid (int) Return only this section
|
|
* sectionnumber (int) Return only this section with number (order)
|
|
* cmid (int) Return only this module information (among the whole sections structure)
|
|
* modname (string) Return only modules with this name "label, forum, etc..."
|
|
* modid (int) Return only the module with this id (to be used with modname.
|
|
*/
|
|
name: string;
|
|
value: string | number | boolean; // The value of the option, this param is personaly validated in the external function.
|
|
}[];
|
|
};
|
|
|
|
/**
|
|
* Data returned by core_course_get_contents WS.
|
|
*/
|
|
type CoreCourseGetContentsWSResponse = CoreCourseGetContentsWSSection[];
|
|
|
|
/**
|
|
* Section data returned by core_course_get_contents WS.
|
|
*/
|
|
type CoreCourseGetContentsWSSection = {
|
|
id: number; // Section ID.
|
|
name: string; // Section name.
|
|
visible?: number; // Is the section visible.
|
|
summary: string; // Section description.
|
|
summaryformat: number; // Summary format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN).
|
|
section?: number; // Section number inside the course.
|
|
hiddenbynumsections?: number; // Whether is a section hidden in the course format.
|
|
uservisible?: boolean; // Is the section visible for the user?.
|
|
availabilityinfo?: string; // Availability information.
|
|
modules: CoreCourseGetContentsWSModule[]; // List of module.
|
|
component?: string; // @since 4.5 The delegate component of this section if any.
|
|
itemid?: number; // @since 4.5 The optional item id delegate component can use to identify its instance.
|
|
};
|
|
|
|
/**
|
|
* Module data returned by core_course_get_contents WS.
|
|
*/
|
|
export type CoreCourseGetContentsWSModule = {
|
|
id: number; // Activity id.
|
|
url?: string; // Activity url.
|
|
name: string; // Activity module name.
|
|
instance: number; // Instance id. Cannot be undefined.
|
|
contextid?: number; // @since 3.10. Activity context id.
|
|
description?: string; // Activity description.
|
|
visible: number; // Is the module visible. Cannot be undefined.
|
|
uservisible: boolean; // Is the module visible for the user?. Cannot be undefined.
|
|
availabilityinfo?: string; // Availability information.
|
|
visibleoncoursepage: number; // Is the module visible on course page. Cannot be undefined.
|
|
modicon: string; // Activity icon url.
|
|
modname: string; // Activity module type.
|
|
purpose?: string; // @since 4.4 The module purpose.
|
|
branded?: boolean; // @since 4.4 Whether the module is branded or not.
|
|
modplural: string; // Activity module plural name.
|
|
availability?: string; // Module availability settings.
|
|
indent: number; // Number of identation in the site.
|
|
onclick?: string; // Onclick action.
|
|
afterlink?: string; // After link info to be displayed.
|
|
customdata?: string; // Custom data (JSON encoded).
|
|
noviewlink?: boolean; // Whether the module has no view page.
|
|
completion?: CoreCourseModuleCompletionTracking; // Type of completion tracking: 0 means none, 1 manual, 2 automatic.
|
|
completiondata?: CoreCourseModuleWSCompletionData; // Module completion data.
|
|
contents?: CoreCourseModuleContentFile[];
|
|
groupmode?: number; // @since 4.3. Group mode value
|
|
downloadcontent?: number; // @since 4.0 The download content value.
|
|
dates?: {
|
|
label: string;
|
|
timestamp: number;
|
|
}[]; // @since 3.11. Activity dates.
|
|
contentsinfo?: { // @since v3.7.6 Contents summary information.
|
|
filescount: number; // Total number of files.
|
|
filessize: number; // Total files size.
|
|
lastmodified: number; // Last time files were modified.
|
|
mimetypes: string[]; // Files mime types.
|
|
repositorytype?: string; // The repository type for the main file.
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Data returned by core_course_get_contents WS.
|
|
*/
|
|
export type CoreCourseWSSection = Omit<CoreCourseGetContentsWSSection, 'modules'> & {
|
|
contents: CoreCourseModuleOrSection[]; // List of modules and subsections.
|
|
|
|
/**
|
|
* List of modules
|
|
*
|
|
* @deprecated since 4.5. Use contents instead.
|
|
*/
|
|
modules: CoreCourseModuleData[];
|
|
};
|
|
|
|
/**
|
|
* Module or subsection.
|
|
*/
|
|
export type CoreCourseModuleOrSection = CoreCourseModuleData | CoreCourseWSSection;
|
|
|
|
/**
|
|
* Params of core_course_get_course_module WS.
|
|
*/
|
|
type CoreCourseGetCourseModuleWSParams = {
|
|
cmid: number; // The course module id.
|
|
};
|
|
|
|
/**
|
|
* Params of core_course_get_course_module_by_instance WS.
|
|
*/
|
|
type CoreCourseGetCourseModuleByInstanceWSParams = {
|
|
module: string; // The module name.
|
|
instance: number; // The module instance id.
|
|
};
|
|
|
|
/**
|
|
* Data returned by core_course_get_course_module and core_course_get_course_module_by_instance WS.
|
|
*/
|
|
type CoreCourseGetCourseModuleWSResponse = {
|
|
cm: CoreCourseModuleBasicInfo;
|
|
warnings?: CoreWSExternalWarning[];
|
|
};
|
|
|
|
/**
|
|
* Module completion data.
|
|
*/
|
|
export type CoreCourseModuleWSCompletionData = {
|
|
state: CoreCourseModuleCompletionStatus; // Completion state value.
|
|
timecompleted: number; // Timestamp for completion status.
|
|
overrideby: number | null; // The user id who has overriden the status.
|
|
valueused?: boolean; // Whether the completion status affects the availability of another activity.
|
|
hascompletion?: boolean; // @since 3.11. Whether this activity module has completion enabled.
|
|
isautomatic?: boolean; // @since 3.11. Whether this activity module instance tracks completion automatically.
|
|
istrackeduser?: boolean; // @since 3.11. Whether completion is being tracked for this user.
|
|
uservisible?: boolean; // @since 3.11. Whether this activity is visible to the user.
|
|
details?: CoreCourseModuleWSRuleDetails[]; // @since 3.11. An array of completion details.
|
|
isoverallcomplete?: boolean; // @since 4.4.
|
|
// Whether the overall completion state of this course module should be marked as complete or not.
|
|
};
|
|
|
|
/**
|
|
* Module completion rule details.
|
|
*/
|
|
export type CoreCourseModuleWSRuleDetails = {
|
|
rulename: string; // Rule name.
|
|
rulevalue: {
|
|
status: number; // Completion status.
|
|
description: string; // Completion description.
|
|
};
|
|
};
|
|
|
|
export type CoreCourseModuleContentFile = {
|
|
// Common properties with CoreWSExternalFile.
|
|
filename: string; // Filename.
|
|
filepath: string; // Filepath.
|
|
filesize: number; // Filesize.
|
|
fileurl: string; // Downloadable file url.
|
|
timemodified: number; // Time modified.
|
|
mimetype?: string; // File mime type.
|
|
isexternalfile?: number; // Whether is an external file.
|
|
repositorytype?: string; // The repository type for external files.
|
|
|
|
type: string; // A file or a folder or external link.
|
|
content?: string; // Raw content, will be used when type is content.
|
|
timecreated: number; // Time created.
|
|
sortorder: number; // Content sort order.
|
|
userid: number; // User who added this content to moodle.
|
|
author: string; // Content owner.
|
|
license: string; // Content license.
|
|
tags?: CoreTagItem[]; // Tags.
|
|
};
|
|
|
|
/**
|
|
* Course module basic info type.
|
|
*/
|
|
export type CoreCourseModuleGradeInfo = {
|
|
grade?: number; // Grade (max value or scale id).
|
|
scale?: string; // Scale items (if used).
|
|
gradepass?: string; // Grade to pass (float).
|
|
gradecat?: number; // Grade category.
|
|
advancedgrading?: CoreCourseModuleAdvancedGradingSetting[]; // Advanced grading settings.
|
|
outcomes?: CoreCourseModuleGradeOutcome[];
|
|
};
|
|
|
|
/**
|
|
* Advanced grading settings.
|
|
*/
|
|
export type CoreCourseModuleAdvancedGradingSetting = {
|
|
area: string; // Gradable area name.
|
|
method: string; // Grading method.
|
|
};
|
|
|
|
/**
|
|
* Grade outcome information.
|
|
*/
|
|
export type CoreCourseModuleGradeOutcome = {
|
|
id: string; // Outcome id.
|
|
name: string; // Outcome full name.
|
|
scale: string; // Scale items.
|
|
};
|
|
|
|
/**
|
|
* Course module basic info type.
|
|
*/
|
|
export type CoreCourseModuleBasicInfo = CoreCourseModuleGradeInfo & {
|
|
id: number; // The course module id.
|
|
course: number; // The course id.
|
|
module: number; // The module type id.
|
|
name: string; // The activity name.
|
|
modname: string; // The module component name (forum, assign, etc..).
|
|
instance: number; // The activity instance id.
|
|
section: number; // The module section id.
|
|
sectionnum: number; // The module section number.
|
|
groupmode: number; // Group mode.
|
|
groupingid: number; // Grouping id.
|
|
completion: number; // If completion is enabled.
|
|
idnumber?: string; // Module id number.
|
|
added?: number; // Time added.
|
|
score?: number; // Score.
|
|
indent?: number; // Indentation.
|
|
visible?: number; // If visible.
|
|
visibleoncoursepage?: number; // If visible on course page.
|
|
visibleold?: number; // Visible old.
|
|
completiongradeitemnumber?: number; // Completion grade item.
|
|
completionpassgrade?: number; // @since 4.0. Completion pass grade setting.
|
|
completionview?: number; // Completion view setting.
|
|
completionexpected?: number; // Completion time expected.
|
|
showdescription?: number; // If the description is showed.
|
|
downloadcontent?: number; // @since 4.0. The download content value.
|
|
availability?: string; // Availability settings.
|
|
};
|
|
|
|
/**
|
|
* Params of core_course_view_course WS.
|
|
*/
|
|
type CoreCourseViewCourseWSParams = {
|
|
courseid: number; // Id of the course.
|
|
sectionnumber?: number; // Section number.
|
|
};
|
|
|
|
/**
|
|
* Params of core_completion_update_activity_completion_status_manually WS.
|
|
*/
|
|
type CoreCompletionUpdateActivityCompletionStatusManuallyWSParams = {
|
|
cmid: number; // Course module id.
|
|
completed: boolean; // Activity completed or not.
|
|
};
|
|
|
|
/**
|
|
* Any of the possible module WS data.
|
|
*/
|
|
export type CoreCourseAnyModuleData = CoreCourseModuleData | CoreCourseModuleBasicInfo & {
|
|
contents?: CoreCourseModuleContentFile[]; // If needed, calculated in the app in loadModuleContents.
|
|
};
|
|
|
|
/**
|
|
* Options for storeModuleViewed.
|
|
*/
|
|
export type CoreCourseStoreModuleViewedOptions = {
|
|
sectionId?: number;
|
|
timeaccess?: number;
|
|
siteId?: string;
|
|
};
|
|
|
|
/**
|
|
* Options for getSections.
|
|
*/
|
|
export type CoreCourseGetSectionsOptions = CoreSitesCommonWSOptions & {
|
|
excludeModules?: boolean;
|
|
excludeContents?: boolean;
|
|
includeStealthModules?: boolean; // Defaults to true.
|
|
preSets?: CoreSiteWSPreSets;
|
|
};
|
|
|
|
/**
|
|
* Options for get sections modules.
|
|
*/
|
|
export type CoreCourseGetSectionsModulesOptions<Section, Module> = {
|
|
ignoreSection?: (section: Section) => boolean; // Function to filter sections. Return true to ignore it, false to use it.
|
|
ignoreModule?: (module: Module) => boolean; // Function to filter module. Return true to ignore it, false to use it.
|
|
};
|