MOBILE-3817 courses: Implement getUserCoursesWithOptionsObservable
parent
73b108e5c5
commit
fbe46ee895
|
@ -14,13 +14,16 @@
|
||||||
|
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { CoreLogger } from '@singletons/logger';
|
import { CoreLogger } from '@singletons/logger';
|
||||||
import { CoreSites } from '@services/sites';
|
import { CoreSites, CoreSitesCommonWSOptions } from '@services/sites';
|
||||||
import { CoreUtils } from '@services/utils/utils';
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
import { CoreCourses } from '@features/courses/services/courses';
|
import { CoreCourses } from '@features/courses/services/courses';
|
||||||
import { CoreSite, CoreSiteWSPreSets } from '@classes/site';
|
import { CoreSite, CoreSiteWSPreSets } from '@classes/site';
|
||||||
import { CoreStatusWithWarningsWSResponse, CoreWSExternalWarning } from '@services/ws';
|
import { CoreStatusWithWarningsWSResponse, CoreWSExternalWarning } from '@services/ws';
|
||||||
import { makeSingleton } from '@singletons';
|
import { makeSingleton } from '@singletons';
|
||||||
import { CoreError } from '@classes/errors/error';
|
import { CoreError } from '@classes/errors/error';
|
||||||
|
import { asyncObservable, firstValueFrom } from '@/core/utils/observables';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
|
||||||
const ROOT_CACHE_KEY = 'mmaCourseCompletion:';
|
const ROOT_CACHE_KEY = 'mmaCourseCompletion:';
|
||||||
|
|
||||||
|
@ -93,33 +96,55 @@ export class AddonCourseCompletionProvider {
|
||||||
* @param siteId Site ID. If not defined, use current site.
|
* @param siteId Site ID. If not defined, use current site.
|
||||||
* @return Promise to be resolved when the completion is retrieved.
|
* @return Promise to be resolved when the completion is retrieved.
|
||||||
*/
|
*/
|
||||||
async getCompletion(
|
getCompletion(
|
||||||
courseId: number,
|
courseId: number,
|
||||||
userId?: number,
|
userId?: number,
|
||||||
preSets: CoreSiteWSPreSets = {},
|
preSets: CoreSiteWSPreSets = {},
|
||||||
siteId?: string,
|
siteId?: string,
|
||||||
): Promise<AddonCourseCompletionCourseCompletionStatus> {
|
): Promise<AddonCourseCompletionCourseCompletionStatus> {
|
||||||
|
return firstValueFrom(this.getCompletionObservable(courseId, {
|
||||||
|
userId,
|
||||||
|
preSets,
|
||||||
|
siteId,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
const site = await CoreSites.getSite(siteId);
|
/**
|
||||||
userId = userId || site.getUserId();
|
* Get course completion status for a certain course and user.
|
||||||
this.logger.debug('Get completion for course ' + courseId + ' and user ' + userId);
|
*
|
||||||
|
* @param courseId Course ID.
|
||||||
|
* @param options Options.
|
||||||
|
* @return Observable returning the completion.
|
||||||
|
*/
|
||||||
|
getCompletionObservable(
|
||||||
|
courseId: number,
|
||||||
|
options: AddonCourseCompletionGetCompletionOptions = {},
|
||||||
|
): Observable<AddonCourseCompletionCourseCompletionStatus> {
|
||||||
|
return asyncObservable(async () => {
|
||||||
|
const site = await CoreSites.getSite(options.siteId);
|
||||||
|
|
||||||
const data: AddonCourseCompletionGetCourseCompletionStatusWSParams = {
|
const userId = options.userId || site.getUserId();
|
||||||
courseid: courseId,
|
this.logger.debug('Get completion for course ' + courseId + ' and user ' + userId);
|
||||||
userid: userId,
|
|
||||||
};
|
|
||||||
|
|
||||||
preSets.cacheKey = this.getCompletionCacheKey(courseId, userId);
|
const data: AddonCourseCompletionGetCourseCompletionStatusWSParams = {
|
||||||
preSets.updateFrequency = preSets.updateFrequency || CoreSite.FREQUENCY_SOMETIMES;
|
courseid: courseId,
|
||||||
preSets.cacheErrors = ['notenroled'];
|
userid: userId,
|
||||||
|
};
|
||||||
|
|
||||||
const result: AddonCourseCompletionGetCourseCompletionStatusWSResponse =
|
const preSets = {
|
||||||
await site.read('core_completion_get_course_completion_status', data, preSets);
|
...(options.preSets ?? {}),
|
||||||
if (result.completionstatus) {
|
cacheKey: this.getCompletionCacheKey(courseId, userId),
|
||||||
return result.completionstatus;
|
updateFrequency: CoreSite.FREQUENCY_SOMETIMES,
|
||||||
}
|
cacheErrors: ['notenroled'],
|
||||||
|
...CoreSites.getReadingStrategyPreSets(options.readingStrategy),
|
||||||
|
};
|
||||||
|
|
||||||
throw new CoreError('Cannot fetch course completion status');
|
return site.readObservable<AddonCourseCompletionGetCourseCompletionStatusWSResponse>(
|
||||||
|
'core_completion_get_course_completion_status',
|
||||||
|
data,
|
||||||
|
preSets,
|
||||||
|
).pipe(map(result => result.completionstatus));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -312,3 +337,11 @@ export type AddonCourseCompletionGetCourseCompletionStatusWSResponse = {
|
||||||
export type AddonCourseCompletionMarkCourseSelfCompletedWSParams = {
|
export type AddonCourseCompletionMarkCourseSelfCompletedWSParams = {
|
||||||
courseid: number; // Course ID.
|
courseid: number; // Course ID.
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for getCompletionObservable.
|
||||||
|
*/
|
||||||
|
export type AddonCourseCompletionGetCompletionOptions = CoreSitesCommonWSOptions & {
|
||||||
|
userId?: number; // Id of the user, default to current user.
|
||||||
|
preSets?: CoreSiteWSPreSets; // Presets to use when calling the WebService.
|
||||||
|
};
|
||||||
|
|
|
@ -57,8 +57,8 @@ import {
|
||||||
WSGroups,
|
WSGroups,
|
||||||
WS_CACHE_TABLES_PREFIX,
|
WS_CACHE_TABLES_PREFIX,
|
||||||
} from '@services/database/sites';
|
} from '@services/database/sites';
|
||||||
import { Observable, Subject } from 'rxjs';
|
import { Observable, ObservableInput, ObservedValueOf, OperatorFunction, Subject } from 'rxjs';
|
||||||
import { finalize, map } from 'rxjs/operators';
|
import { finalize, map, mergeMap } from 'rxjs/operators';
|
||||||
import { firstValueFrom } from '../utils/observables';
|
import { firstValueFrom } from '../utils/observables';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -2379,6 +2379,70 @@ export class CoreSite {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Operator to chain requests when using observables.
|
||||||
|
*
|
||||||
|
* @param readingStrategy Reading strategy used for the current request.
|
||||||
|
* @param callback Callback called with the result of current request and the reading strategy to use in next requests.
|
||||||
|
* @return Operator.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
export function chainRequests<T, O extends ObservableInput<any>>(
|
||||||
|
readingStrategy: CoreSitesReadingStrategy | undefined,
|
||||||
|
callback: (data: T, readingStrategy?: CoreSitesReadingStrategy) => O,
|
||||||
|
): OperatorFunction<T, ObservedValueOf<O>> {
|
||||||
|
return (source: Observable<T>) => new Observable<{ data: T; readingStrategy?: CoreSitesReadingStrategy }>(subscriber => {
|
||||||
|
let firstValue = true;
|
||||||
|
let isCompleted = false;
|
||||||
|
|
||||||
|
return source.subscribe({
|
||||||
|
next: async (value) => {
|
||||||
|
if (readingStrategy !== CoreSitesReadingStrategy.UPDATE_IN_BACKGROUND) {
|
||||||
|
// Just use same strategy.
|
||||||
|
subscriber.next({ data: value, readingStrategy });
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!firstValue) {
|
||||||
|
// Second (last) value. Chained requests should have used cached data already, just return 1 value now.
|
||||||
|
subscriber.next({
|
||||||
|
data: value,
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
firstValue = false;
|
||||||
|
|
||||||
|
// Wait to see if the observable is completed (no more values).
|
||||||
|
await CoreUtils.nextTick();
|
||||||
|
|
||||||
|
if (isCompleted) {
|
||||||
|
// Current request only returns cached data. Let chained requests update in background.
|
||||||
|
subscriber.next({ data: value, readingStrategy });
|
||||||
|
} else {
|
||||||
|
// Current request will update in background. Prefer cached data in the chained requests.
|
||||||
|
subscriber.next({
|
||||||
|
data: value,
|
||||||
|
readingStrategy: CoreSitesReadingStrategy.PREFER_CACHE,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: (error) => subscriber.error(error),
|
||||||
|
complete: async () => {
|
||||||
|
isCompleted = true;
|
||||||
|
|
||||||
|
await CoreUtils.nextTick();
|
||||||
|
|
||||||
|
subscriber.complete();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}).pipe(
|
||||||
|
mergeMap(({ data, readingStrategy }) => callback(data, readingStrategy)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PreSets accepted by the WS call.
|
* PreSets accepted by the WS call.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -26,6 +26,10 @@ import { makeSingleton, Translate } from '@singletons';
|
||||||
import { CoreWSExternalFile } from '@services/ws';
|
import { CoreWSExternalFile } from '@services/ws';
|
||||||
import { AddonCourseCompletion } from '@addons/coursecompletion/services/coursecompletion';
|
import { AddonCourseCompletion } from '@addons/coursecompletion/services/coursecompletion';
|
||||||
import moment from 'moment-timezone';
|
import moment from 'moment-timezone';
|
||||||
|
import { Observable, of } from 'rxjs';
|
||||||
|
import { firstValueFrom, zipIncudingComplete } from '@/core/utils/observables';
|
||||||
|
import { catchError, map } from 'rxjs/operators';
|
||||||
|
import { chainRequests } from '@classes/site';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper to gather some common courses functions.
|
* Helper to gather some common courses functions.
|
||||||
|
@ -111,29 +115,47 @@ export class CoreCoursesHelperProvider {
|
||||||
* @param loadCategoryNames Whether load category names or not.
|
* @param loadCategoryNames Whether load category names or not.
|
||||||
* @return Promise resolved when done.
|
* @return Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
async loadCoursesExtraInfo(courses: CoreEnrolledCourseDataWithExtraInfo[], loadCategoryNames: boolean = false): Promise<void> {
|
loadCoursesExtraInfo(
|
||||||
if (!courses.length ) {
|
courses: CoreEnrolledCourseDataWithExtraInfo[],
|
||||||
// No courses or cannot get the data, stop.
|
loadCategoryNames: boolean = false,
|
||||||
return;
|
): Promise<CoreEnrolledCourseDataWithExtraInfo[]> {
|
||||||
|
return firstValueFrom(this.loadCoursesExtraInfoObservable(courses, loadCategoryNames));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a list of courses returned by core_enrol_get_users_courses, load some extra data using the WebService
|
||||||
|
* core_course_get_courses_by_field if available.
|
||||||
|
*
|
||||||
|
* @param courses List of courses.
|
||||||
|
* @param loadCategoryNames Whether load category names or not.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
loadCoursesExtraInfoObservable(
|
||||||
|
courses: CoreEnrolledCourseDataWithExtraInfo[],
|
||||||
|
loadCategoryNames: boolean = false,
|
||||||
|
options: CoreSitesCommonWSOptions = {},
|
||||||
|
): Observable<CoreEnrolledCourseDataWithExtraInfo[]> {
|
||||||
|
if (!courses.length) {
|
||||||
|
return of([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
let coursesInfo = {};
|
if (!loadCategoryNames && (courses[0].overviewfiles !== undefined || courses[0].displayname !== undefined)) {
|
||||||
let courseInfoAvailable = false;
|
// No need to load more data.
|
||||||
|
return of(courses);
|
||||||
if (loadCategoryNames || (courses[0].overviewfiles === undefined && courses[0].displayname === undefined)) {
|
|
||||||
const courseIds = courses.map((course) => course.id).join(',');
|
|
||||||
|
|
||||||
courseInfoAvailable = true;
|
|
||||||
|
|
||||||
// Get the extra data for the courses.
|
|
||||||
const coursesInfosArray = await CoreCourses.getCoursesByField('ids', courseIds);
|
|
||||||
|
|
||||||
coursesInfo = CoreUtils.arrayToObject(coursesInfosArray, 'id');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
courses.forEach((course) => {
|
const courseIds = courses.map((course) => course.id).join(',');
|
||||||
this.loadCourseExtraInfo(course, courseInfoAvailable ? coursesInfo[course.id] : course, loadCategoryNames);
|
|
||||||
});
|
// Get the extra data for the courses.
|
||||||
|
return CoreCourses.getCoursesByFieldObservable('ids', courseIds, options).pipe(map(coursesInfosArray => {
|
||||||
|
const coursesInfo = CoreUtils.arrayToObject(coursesInfosArray, 'id');
|
||||||
|
|
||||||
|
courses.forEach((course) => {
|
||||||
|
this.loadCourseExtraInfo(course, coursesInfo[course.id], loadCategoryNames);
|
||||||
|
});
|
||||||
|
|
||||||
|
return courses;
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -196,43 +218,76 @@ export class CoreCoursesHelperProvider {
|
||||||
* @param slice Slice results to get the X first one. If slice > 0 it will be done after sorting.
|
* @param slice Slice results to get the X first one. If slice > 0 it will be done after sorting.
|
||||||
* @param filter Filter using some field.
|
* @param filter Filter using some field.
|
||||||
* @param loadCategoryNames Whether load category names or not.
|
* @param loadCategoryNames Whether load category names or not.
|
||||||
|
* @param options Options.
|
||||||
* @return Courses filled with options.
|
* @return Courses filled with options.
|
||||||
*/
|
*/
|
||||||
async getUserCoursesWithOptions(
|
getUserCoursesWithOptions(
|
||||||
sort: string = 'fullname',
|
sort: string = 'fullname',
|
||||||
slice: number = 0,
|
slice: number = 0,
|
||||||
filter?: string,
|
filter?: string,
|
||||||
loadCategoryNames: boolean = false,
|
loadCategoryNames: boolean = false,
|
||||||
options: CoreSitesCommonWSOptions = {},
|
options: CoreSitesCommonWSOptions = {},
|
||||||
): Promise<CoreEnrolledCourseDataWithOptions[]> {
|
): Promise<CoreEnrolledCourseDataWithOptions[]> {
|
||||||
|
return firstValueFrom(this.getUserCoursesWithOptionsObservable({
|
||||||
let courses: CoreEnrolledCourseDataWithOptions[] = await CoreCourses.getUserCourses(
|
sort,
|
||||||
false,
|
slice,
|
||||||
options.siteId,
|
filter,
|
||||||
options.readingStrategy,
|
loadCategoryNames,
|
||||||
);
|
...options,
|
||||||
if (courses.length <= 0) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const promises: Promise<void>[] = [];
|
|
||||||
const courseIds = courses.map((course) => course.id);
|
|
||||||
|
|
||||||
// Load course options of the course.
|
|
||||||
promises.push(CoreCourses.getCoursesAdminAndNavOptions(courseIds, options.siteId).then((options) => {
|
|
||||||
courses.forEach((course) => {
|
|
||||||
course.navOptions = options.navOptions[course.id];
|
|
||||||
course.admOptions = options.admOptions[course.id];
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}));
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
promises.push(this.loadCoursesExtraInfo(courses, loadCategoryNames));
|
/**
|
||||||
|
* Get user courses with admin and nav options.
|
||||||
|
*
|
||||||
|
* @param options Options.
|
||||||
|
* @return Courses filled with options.
|
||||||
|
*/
|
||||||
|
getUserCoursesWithOptionsObservable(
|
||||||
|
options: CoreCoursesGetWithOptionsOptions = {},
|
||||||
|
): Observable<CoreEnrolledCourseDataWithOptions[]> {
|
||||||
|
return CoreCourses.getUserCoursesObservable(options).pipe(
|
||||||
|
chainRequests(options.readingStrategy, (courses, newReadingStrategy) => {
|
||||||
|
if (courses.length <= 0) {
|
||||||
|
return of([]);
|
||||||
|
}
|
||||||
|
|
||||||
await Promise.all(promises);
|
const courseIds = courses.map((course) => course.id); // Use all courses to get options, to use cache.
|
||||||
|
const newOptions = {
|
||||||
|
...options,
|
||||||
|
readingStrategy: newReadingStrategy,
|
||||||
|
};
|
||||||
|
courses = this.filterAndSortCoursesWithOptions(courses, options);
|
||||||
|
|
||||||
switch (filter) {
|
return zipIncudingComplete(
|
||||||
|
this.loadCoursesExtraInfoObservable(courses, options.loadCategoryNames, newOptions),
|
||||||
|
CoreCourses.getCoursesAdminAndNavOptionsObservable(courseIds, newOptions).pipe(map(courseOptions => {
|
||||||
|
courses.forEach((course: CoreEnrolledCourseDataWithOptions) => {
|
||||||
|
course.navOptions = courseOptions.navOptions[course.id];
|
||||||
|
course.admOptions = courseOptions.admOptions[course.id];
|
||||||
|
});
|
||||||
|
})),
|
||||||
|
...courses.map(course => this.loadCourseCompletedStatus(course, newOptions)),
|
||||||
|
).pipe(map(() => courses));
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter and sort some courses.
|
||||||
|
*
|
||||||
|
* @param courses Courses.
|
||||||
|
* @param options Options
|
||||||
|
* @return Courses filtered and sorted.
|
||||||
|
*/
|
||||||
|
protected filterAndSortCoursesWithOptions(
|
||||||
|
courses: CoreEnrolledCourseData[],
|
||||||
|
options: CoreCoursesGetWithOptionsOptions = {},
|
||||||
|
): CoreEnrolledCourseData[] {
|
||||||
|
const sort = options.sort ?? 'fullname';
|
||||||
|
const slice = options.slice ?? -1;
|
||||||
|
|
||||||
|
switch (options.filter) {
|
||||||
case 'isfavourite':
|
case 'isfavourite':
|
||||||
courses = courses.filter((course) => !!course.isfavourite);
|
courses = courses.filter((course) => !!course.isfavourite);
|
||||||
break;
|
break;
|
||||||
|
@ -270,28 +325,42 @@ export class CoreCoursesHelperProvider {
|
||||||
|
|
||||||
courses = slice > 0 ? courses.slice(0, slice) : courses;
|
courses = slice > 0 ? courses.slice(0, slice) : courses;
|
||||||
|
|
||||||
return Promise.all(courses.map(async (course) => {
|
return courses;
|
||||||
if (course.completed !== undefined) {
|
}
|
||||||
// The WebService already returns the completed status, no need to fetch it.
|
|
||||||
|
/**
|
||||||
|
* Given a course object, fetch and set its completed status if not present already.
|
||||||
|
*
|
||||||
|
* @param course Course.
|
||||||
|
* @return Observable.
|
||||||
|
*/
|
||||||
|
protected loadCourseCompletedStatus(
|
||||||
|
course: CoreEnrolledCourseDataWithExtraInfo,
|
||||||
|
options: CoreSitesCommonWSOptions = {},
|
||||||
|
): Observable<CoreEnrolledCourseDataWithExtraInfo> {
|
||||||
|
if (course.completed !== undefined) {
|
||||||
|
// The WebService already returns the completed status, no need to fetch it.
|
||||||
|
return of(course);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (course.enablecompletion !== undefined && !course.enablecompletion) {
|
||||||
|
// Completion is disabled for this course, there is no need to fetch the completion status.
|
||||||
|
return of(course);
|
||||||
|
}
|
||||||
|
|
||||||
|
return AddonCourseCompletion.getCompletionObservable(course.id, options).pipe(
|
||||||
|
map(completion => {
|
||||||
|
course.completed = completion.completed;
|
||||||
|
|
||||||
return course;
|
return course;
|
||||||
}
|
}),
|
||||||
|
catchError(() => {
|
||||||
if (course.enablecompletion !== undefined && !course.enablecompletion) {
|
|
||||||
// Completion is disabled for this course, there is no need to fetch the completion status.
|
|
||||||
return course;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const completion = await AddonCourseCompletion.getCompletion(course.id, undefined, undefined, options.siteId);
|
|
||||||
|
|
||||||
course.completed = completion?.completed;
|
|
||||||
} catch {
|
|
||||||
// Ignore error, maybe course completion is disabled or user has no permission.
|
// Ignore error, maybe course completion is disabled or user has no permission.
|
||||||
course.completed = false;
|
course.completed = false;
|
||||||
}
|
|
||||||
|
|
||||||
return course;
|
return of(course);
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -402,3 +471,13 @@ export type CoreCourseSearchedDataWithExtraInfoAndOptions = CoreCourseWithImageA
|
||||||
export type CoreCourseAnyCourseDataWithExtraInfoAndOptions = CoreCourseWithImageAndColor & CoreCourseAnyCourseDataWithOptions & {
|
export type CoreCourseAnyCourseDataWithExtraInfoAndOptions = CoreCourseWithImageAndColor & CoreCourseAnyCourseDataWithOptions & {
|
||||||
categoryname?: string; // Category name,
|
categoryname?: string; // Category name,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for getUserCoursesWithOptionsObservable.
|
||||||
|
*/
|
||||||
|
export type CoreCoursesGetWithOptionsOptions = CoreSitesCommonWSOptions & {
|
||||||
|
sort?: string; // Sort courses after get them. Defaults to 'fullname'.
|
||||||
|
slice?: number; // Slice results to get the X first one. If slice > 0 it will be done after sorting.
|
||||||
|
filter?: string; // Filter using some field.
|
||||||
|
loadCategoryNames?: boolean; // Whether load category names or not.
|
||||||
|
};
|
||||||
|
|
|
@ -20,7 +20,9 @@ import { CoreStatusWithWarningsWSResponse, CoreWarningsWSResponse, CoreWSExterna
|
||||||
import { CoreEvents } from '@singletons/events';
|
import { CoreEvents } from '@singletons/events';
|
||||||
import { CoreWSError } from '@classes/errors/wserror';
|
import { CoreWSError } from '@classes/errors/wserror';
|
||||||
import { CoreCourseAnyCourseDataWithExtraInfoAndOptions, CoreCourseWithImageAndColor } from './courses-helper';
|
import { CoreCourseAnyCourseDataWithExtraInfoAndOptions, CoreCourseWithImageAndColor } from './courses-helper';
|
||||||
import { CoreUtils } from '@services/utils/utils';
|
import { asyncObservable, firstValueFrom, ignoreErrors, zipIncudingComplete } from '@/core/utils/observables';
|
||||||
|
import { of, Observable } from 'rxjs';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
|
||||||
const ROOT_CACHE_KEY = 'mmCourses:';
|
const ROOT_CACHE_KEY = 'mmCourses:';
|
||||||
|
|
||||||
|
@ -63,8 +65,7 @@ export class CoreCoursesProvider {
|
||||||
static readonly STATE_HIDDEN = 'hidden';
|
static readonly STATE_HIDDEN = 'hidden';
|
||||||
static readonly STATE_FAVOURITE = 'favourite';
|
static readonly STATE_FAVOURITE = 'favourite';
|
||||||
|
|
||||||
protected userCoursesIds: { [id: number]: boolean } = {}; // Use an object to make it faster to search.
|
protected userCoursesIds?: Set<number>;
|
||||||
|
|
||||||
protected downloadOptionsEnabled = false;
|
protected downloadOptionsEnabled = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -484,60 +485,91 @@ export class CoreCoursesProvider {
|
||||||
* @param siteId Site ID. If not defined, use current site.
|
* @param siteId Site ID. If not defined, use current site.
|
||||||
* @return Promise resolved with the courses.
|
* @return Promise resolved with the courses.
|
||||||
*/
|
*/
|
||||||
async getCoursesByField(
|
getCoursesByField(
|
||||||
field: string = '',
|
field: string = '',
|
||||||
value: string | number = '',
|
value: string | number = '',
|
||||||
siteId?: string,
|
siteId?: string,
|
||||||
): Promise<CoreCourseSearchedData[]> {
|
): Promise<CoreCourseSearchedData[]> {
|
||||||
siteId = siteId || CoreSites.getCurrentSiteId();
|
return firstValueFrom(this.getCoursesByFieldObservable(field, value, { siteId }));
|
||||||
|
}
|
||||||
|
|
||||||
const originalValue = value;
|
/**
|
||||||
|
* Get courses. They can be filtered by field.
|
||||||
|
*
|
||||||
|
* @param field The field to search. Can be left empty for all courses or:
|
||||||
|
* id: course id.
|
||||||
|
* ids: comma separated course ids.
|
||||||
|
* shortname: course short name.
|
||||||
|
* idnumber: course id number.
|
||||||
|
* category: category id the course belongs to.
|
||||||
|
* @param value The value to match.
|
||||||
|
* @param options Other options.
|
||||||
|
* @return Observable that returns the courses.
|
||||||
|
*/
|
||||||
|
getCoursesByFieldObservable(
|
||||||
|
field: string = '',
|
||||||
|
value: string | number = '',
|
||||||
|
options: CoreSitesCommonWSOptions = {},
|
||||||
|
): Observable<CoreCourseSearchedData[]> {
|
||||||
|
return asyncObservable(async () => {
|
||||||
|
const siteId = options.siteId || CoreSites.getCurrentSiteId();
|
||||||
|
const originalValue = value;
|
||||||
|
|
||||||
const site = await CoreSites.getSite(siteId);
|
const site = await CoreSites.getSite(siteId);
|
||||||
|
|
||||||
const fieldParams = await this.fixCoursesByFieldParams(field, value, siteId);
|
// Fix params. Tries to use cached data, no need to use observer.
|
||||||
|
const fieldParams = await this.fixCoursesByFieldParams(field, value, siteId);
|
||||||
|
|
||||||
const hasChanged = fieldParams.field != field || fieldParams.value != value;
|
const hasChanged = fieldParams.field != field || fieldParams.value != value;
|
||||||
field = fieldParams.field;
|
field = fieldParams.field;
|
||||||
value = fieldParams.value;
|
value = fieldParams.value;
|
||||||
const data: CoreCourseGetCoursesByFieldWSParams = {
|
const data: CoreCourseGetCoursesByFieldWSParams = {
|
||||||
field: field,
|
field: field,
|
||||||
value: field ? value : '',
|
value: field ? value : '',
|
||||||
};
|
};
|
||||||
const preSets: CoreSiteWSPreSets = {
|
const preSets: CoreSiteWSPreSets = {
|
||||||
cacheKey: this.getCoursesByFieldCacheKey(field, value),
|
cacheKey: this.getCoursesByFieldCacheKey(field, value),
|
||||||
updateFrequency: CoreSite.FREQUENCY_RARELY,
|
updateFrequency: CoreSite.FREQUENCY_RARELY,
|
||||||
};
|
...CoreSites.getReadingStrategyPreSets(options.readingStrategy),
|
||||||
|
};
|
||||||
|
|
||||||
const response = await site.read<CoreCourseGetCoursesByFieldWSResponse>('core_course_get_courses_by_field', data, preSets);
|
const observable = site.readObservable<CoreCourseGetCoursesByFieldWSResponse>(
|
||||||
if (!response.courses) {
|
'core_course_get_courses_by_field',
|
||||||
throw Error('WS core_course_get_courses_by_field failed');
|
data,
|
||||||
}
|
preSets,
|
||||||
|
);
|
||||||
|
|
||||||
if (field == 'ids' && hasChanged) {
|
return observable.pipe(map(response => {
|
||||||
// The list of courses requestes was changed to optimize it.
|
if (!response.courses) {
|
||||||
// Return only the ones that were being requested.
|
throw Error('WS core_course_get_courses_by_field failed');
|
||||||
const courseIds = String(originalValue).split(',').map((id) => parseInt(id, 10));
|
}
|
||||||
|
|
||||||
// Only courses from the original selection.
|
if (field == 'ids' && hasChanged) {
|
||||||
response.courses = response.courses.filter((course) => courseIds.indexOf(course.id) >= 0);
|
// The list of courses requestes was changed to optimize it.
|
||||||
}
|
// Return only the ones that were being requested.
|
||||||
|
const courseIds = String(originalValue).split(',').map((id) => parseInt(id, 10));
|
||||||
|
|
||||||
// Courses will be sorted using sortorder if available.
|
// Only courses from the original selection.
|
||||||
return response.courses.sort((a, b) => {
|
response.courses = response.courses.filter((course) => courseIds.indexOf(course.id) >= 0);
|
||||||
if (a.sortorder === undefined && b.sortorder === undefined) {
|
}
|
||||||
return b.id - a.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (a.sortorder === undefined) {
|
// Courses will be sorted using sortorder if available.
|
||||||
return 1;
|
return response.courses.sort((a, b) => {
|
||||||
}
|
if (a.sortorder === undefined && b.sortorder === undefined) {
|
||||||
|
return b.id - a.id;
|
||||||
|
}
|
||||||
|
|
||||||
if (b.sortorder === undefined) {
|
if (a.sortorder === undefined) {
|
||||||
return -1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return a.sortorder - b.sortorder;
|
if (b.sortorder === undefined) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.sortorder - b.sortorder;
|
||||||
|
});
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -614,25 +646,45 @@ export class CoreCoursesProvider {
|
||||||
* @param siteId Site ID. If not defined, current site.
|
* @param siteId Site ID. If not defined, current site.
|
||||||
* @return Promise resolved with the options for each course.
|
* @return Promise resolved with the options for each course.
|
||||||
*/
|
*/
|
||||||
async getCoursesAdminAndNavOptions(
|
getCoursesAdminAndNavOptions(
|
||||||
courseIds: number[],
|
courseIds: number[],
|
||||||
siteId?: string,
|
siteId?: string,
|
||||||
): Promise<{
|
): Promise<{
|
||||||
navOptions: CoreCourseUserAdminOrNavOptionCourseIndexed;
|
navOptions: CoreCourseUserAdminOrNavOptionCourseIndexed;
|
||||||
admOptions: CoreCourseUserAdminOrNavOptionCourseIndexed;
|
admOptions: CoreCourseUserAdminOrNavOptionCourseIndexed;
|
||||||
}> {
|
}> {
|
||||||
siteId = siteId || CoreSites.getCurrentSiteId();
|
return firstValueFrom(this.getCoursesAdminAndNavOptionsObservable(courseIds, { siteId }));
|
||||||
|
}
|
||||||
|
|
||||||
// Get the list of courseIds to use based on the param.
|
/**
|
||||||
courseIds = await this.getCourseIdsForAdminAndNavOptions(courseIds, siteId);
|
* Get the navigation and administration options for the given courses.
|
||||||
|
*
|
||||||
|
* @param courseIds IDs of courses to get.
|
||||||
|
* @param options Options.
|
||||||
|
* @return Observable that returns the options for each course.
|
||||||
|
*/
|
||||||
|
getCoursesAdminAndNavOptionsObservable(
|
||||||
|
courseIds: number[],
|
||||||
|
options: CoreSitesCommonWSOptions = {},
|
||||||
|
): Observable<{
|
||||||
|
navOptions: CoreCourseUserAdminOrNavOptionCourseIndexed;
|
||||||
|
admOptions: CoreCourseUserAdminOrNavOptionCourseIndexed;
|
||||||
|
}> {
|
||||||
|
|
||||||
// Get user navigation and administration options.
|
return asyncObservable(async () => {
|
||||||
const [navOptions, admOptions] = await Promise.all([
|
const siteId = options.siteId || CoreSites.getCurrentSiteId();
|
||||||
CoreUtils.ignoreErrors(this.getUserNavigationOptions(courseIds, siteId), {}),
|
|
||||||
CoreUtils.ignoreErrors(this.getUserAdministrationOptions(courseIds, siteId), {}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return { navOptions: navOptions, admOptions: admOptions };
|
// Get the list of courseIds to use based on the param. Tries to use cached data, no need to use observer.
|
||||||
|
courseIds = await this.getCourseIdsForAdminAndNavOptions(courseIds, siteId);
|
||||||
|
|
||||||
|
// Get user navigation and administration options.
|
||||||
|
return zipIncudingComplete(
|
||||||
|
ignoreErrors(this.getUserNavigationOptionsObservable(courseIds, options), {}),
|
||||||
|
ignoreErrors(this.getUserAdministrationOptionsObservable(courseIds, options), {}),
|
||||||
|
).pipe(
|
||||||
|
map(([navOptions, admOptions]) => ({ navOptions, admOptions })),
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -695,26 +747,46 @@ export class CoreCoursesProvider {
|
||||||
* @param siteId Site ID. If not defined, current site.
|
* @param siteId Site ID. If not defined, current site.
|
||||||
* @return Promise resolved with administration options for each course.
|
* @return Promise resolved with administration options for each course.
|
||||||
*/
|
*/
|
||||||
async getUserAdministrationOptions(courseIds: number[], siteId?: string): Promise<CoreCourseUserAdminOrNavOptionCourseIndexed> {
|
getUserAdministrationOptions(courseIds: number[], siteId?: string): Promise<CoreCourseUserAdminOrNavOptionCourseIndexed> {
|
||||||
|
return firstValueFrom(this.getUserAdministrationOptionsObservable(courseIds, { siteId }));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user administration options for a set of courses.
|
||||||
|
*
|
||||||
|
* @param courseIds IDs of courses to get.
|
||||||
|
* @param options Options.
|
||||||
|
* @return Observable that returns administration options for each course.
|
||||||
|
*/
|
||||||
|
getUserAdministrationOptionsObservable(
|
||||||
|
courseIds: number[],
|
||||||
|
options: CoreSitesCommonWSOptions = {},
|
||||||
|
): Observable<CoreCourseUserAdminOrNavOptionCourseIndexed> {
|
||||||
if (!courseIds || courseIds.length == 0) {
|
if (!courseIds || courseIds.length == 0) {
|
||||||
return {};
|
return of({});
|
||||||
}
|
}
|
||||||
|
|
||||||
const site = await CoreSites.getSite(siteId);
|
return asyncObservable(async () => {
|
||||||
|
const site = await CoreSites.getSite(options.siteId);
|
||||||
|
|
||||||
const params: CoreCourseGetUserAdminOrNavOptionsWSParams = {
|
const params: CoreCourseGetUserAdminOrNavOptionsWSParams = {
|
||||||
courseids: courseIds,
|
courseids: courseIds,
|
||||||
};
|
};
|
||||||
const preSets: CoreSiteWSPreSets = {
|
const preSets: CoreSiteWSPreSets = {
|
||||||
cacheKey: this.getUserAdministrationOptionsCacheKey(courseIds),
|
cacheKey: this.getUserAdministrationOptionsCacheKey(courseIds),
|
||||||
updateFrequency: CoreSite.FREQUENCY_RARELY,
|
updateFrequency: CoreSite.FREQUENCY_RARELY,
|
||||||
};
|
...CoreSites.getReadingStrategyPreSets(options.readingStrategy),
|
||||||
|
};
|
||||||
|
|
||||||
const response: CoreCourseGetUserAdminOrNavOptionsWSResponse =
|
const observable = site.readObservable<CoreCourseGetUserAdminOrNavOptionsWSResponse>(
|
||||||
await site.read('core_course_get_user_administration_options', params, preSets);
|
'core_course_get_user_administration_options',
|
||||||
|
params,
|
||||||
|
preSets,
|
||||||
|
);
|
||||||
|
|
||||||
// Format returned data.
|
// Format returned data.
|
||||||
return this.formatUserAdminOrNavOptions(response.courses);
|
return observable.pipe(map(response => this.formatUserAdminOrNavOptions(response.courses)));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -743,25 +815,45 @@ export class CoreCoursesProvider {
|
||||||
* @return Promise resolved with navigation options for each course.
|
* @return Promise resolved with navigation options for each course.
|
||||||
*/
|
*/
|
||||||
async getUserNavigationOptions(courseIds: number[], siteId?: string): Promise<CoreCourseUserAdminOrNavOptionCourseIndexed> {
|
async getUserNavigationOptions(courseIds: number[], siteId?: string): Promise<CoreCourseUserAdminOrNavOptionCourseIndexed> {
|
||||||
|
return firstValueFrom(this.getUserNavigationOptionsObservable(courseIds, { siteId }));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user navigation options for a set of courses.
|
||||||
|
*
|
||||||
|
* @param courseIds IDs of courses to get.
|
||||||
|
* @param options Options.
|
||||||
|
* @return Observable that returns navigation options for each course.
|
||||||
|
*/
|
||||||
|
getUserNavigationOptionsObservable(
|
||||||
|
courseIds: number[],
|
||||||
|
options: CoreSitesCommonWSOptions = {},
|
||||||
|
): Observable<CoreCourseUserAdminOrNavOptionCourseIndexed> {
|
||||||
if (!courseIds || courseIds.length == 0) {
|
if (!courseIds || courseIds.length == 0) {
|
||||||
return {};
|
return of({});
|
||||||
}
|
}
|
||||||
|
|
||||||
const site = await CoreSites.getSite(siteId);
|
return asyncObservable(async () => {
|
||||||
|
const site = await CoreSites.getSite(options.siteId);
|
||||||
|
|
||||||
const params: CoreCourseGetUserAdminOrNavOptionsWSParams = {
|
const params: CoreCourseGetUserAdminOrNavOptionsWSParams = {
|
||||||
courseids: courseIds,
|
courseids: courseIds,
|
||||||
};
|
};
|
||||||
const preSets: CoreSiteWSPreSets = {
|
const preSets: CoreSiteWSPreSets = {
|
||||||
cacheKey: this.getUserNavigationOptionsCacheKey(courseIds),
|
cacheKey: this.getUserNavigationOptionsCacheKey(courseIds),
|
||||||
updateFrequency: CoreSite.FREQUENCY_RARELY,
|
updateFrequency: CoreSite.FREQUENCY_RARELY,
|
||||||
};
|
...CoreSites.getReadingStrategyPreSets(options.readingStrategy),
|
||||||
|
};
|
||||||
|
|
||||||
const response: CoreCourseGetUserAdminOrNavOptionsWSResponse =
|
const observable = site.readObservable<CoreCourseGetUserAdminOrNavOptionsWSResponse>(
|
||||||
await site.read('core_course_get_user_navigation_options', params, preSets);
|
'core_course_get_user_navigation_options',
|
||||||
|
params,
|
||||||
|
preSets,
|
||||||
|
);
|
||||||
|
|
||||||
// Format returned data.
|
// Format returned data.
|
||||||
return this.formatUserAdminOrNavOptions(response.courses);
|
return observable.pipe(map(response => this.formatUserAdminOrNavOptions(response.courses)));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -818,89 +910,112 @@ export class CoreCoursesProvider {
|
||||||
*
|
*
|
||||||
* @param preferCache True if shouldn't call WS if data is cached, false otherwise.
|
* @param preferCache True if shouldn't call WS if data is cached, false otherwise.
|
||||||
* @param siteId Site to get the courses from. If not defined, use current site.
|
* @param siteId Site to get the courses from. If not defined, use current site.
|
||||||
|
* @param strategy Reading strategy.
|
||||||
* @return Promise resolved with the courses.
|
* @return Promise resolved with the courses.
|
||||||
*/
|
*/
|
||||||
async getUserCourses(
|
getUserCourses(
|
||||||
preferCache: boolean = false,
|
preferCache: boolean = false,
|
||||||
siteId?: string,
|
siteId?: string,
|
||||||
strategy?: CoreSitesReadingStrategy,
|
strategy?: CoreSitesReadingStrategy,
|
||||||
): Promise<CoreEnrolledCourseData[]> {
|
): Promise<CoreEnrolledCourseData[]> {
|
||||||
const site = await CoreSites.getSite(siteId);
|
strategy = strategy ?? (preferCache ? CoreSitesReadingStrategy.PREFER_CACHE : undefined);
|
||||||
|
|
||||||
const userId = site.getUserId();
|
return this.getUserCoursesObservable({
|
||||||
const wsParams: CoreEnrolGetUsersCoursesWSParams = {
|
readingStrategy: strategy,
|
||||||
userid: userId,
|
siteId,
|
||||||
};
|
}).toPromise();
|
||||||
const strategyPreSets = strategy
|
}
|
||||||
? CoreSites.getReadingStrategyPreSets(strategy)
|
|
||||||
: { omitExpires: !!preferCache };
|
|
||||||
|
|
||||||
const preSets = {
|
/**
|
||||||
cacheKey: this.getUserCoursesCacheKey(),
|
* Get user courses.
|
||||||
getCacheUsingCacheKey: true,
|
*
|
||||||
updateFrequency: CoreSite.FREQUENCY_RARELY,
|
* @param options Options.
|
||||||
...strategyPreSets,
|
* @return Observable that returns the courses.
|
||||||
};
|
*/
|
||||||
|
getUserCoursesObservable(options: CoreSitesCommonWSOptions = {}): Observable<CoreEnrolledCourseData[]> {
|
||||||
|
return asyncObservable(async () => {
|
||||||
|
const site = await CoreSites.getSite(options.siteId);
|
||||||
|
|
||||||
if (site.isVersionGreaterEqualThan('3.7')) {
|
const userId = site.getUserId();
|
||||||
wsParams.returnusercount = false;
|
const wsParams: CoreEnrolGetUsersCoursesWSParams = {
|
||||||
}
|
userid: userId,
|
||||||
|
};
|
||||||
|
|
||||||
const courses = await site.read<CoreEnrolGetUsersCoursesWSResponse>('core_enrol_get_users_courses', wsParams, preSets);
|
const preSets: CoreSiteWSPreSets = {
|
||||||
|
cacheKey: this.getUserCoursesCacheKey(),
|
||||||
|
getCacheUsingCacheKey: true,
|
||||||
|
updateFrequency: CoreSite.FREQUENCY_RARELY,
|
||||||
|
...CoreSites.getReadingStrategyPreSets(options.readingStrategy),
|
||||||
|
};
|
||||||
|
|
||||||
if (this.userCoursesIds) {
|
if (site.isVersionGreaterEqualThan('3.7')) {
|
||||||
// Check if the list of courses has changed.
|
wsParams.returnusercount = false;
|
||||||
const added: number[] = [];
|
}
|
||||||
const removed: number[] = [];
|
|
||||||
const previousIds = Object.keys(this.userCoursesIds);
|
|
||||||
const currentIds = {}; // Use an object to make it faster to search.
|
|
||||||
|
|
||||||
courses.forEach((course) => {
|
const observable = site.readObservable<CoreEnrolGetUsersCoursesWSResponse>(
|
||||||
// Move category field to categoryid on a course.
|
'core_enrol_get_users_courses',
|
||||||
course.categoryid = course.category;
|
wsParams,
|
||||||
delete course.category;
|
preSets,
|
||||||
|
);
|
||||||
|
|
||||||
currentIds[course.id] = true;
|
return observable.pipe(map(courses => {
|
||||||
|
if (this.userCoursesIds) {
|
||||||
|
// Check if the list of courses has changed.
|
||||||
|
const added: number[] = [];
|
||||||
|
const removed: number[] = [];
|
||||||
|
const previousIds = this.userCoursesIds;
|
||||||
|
const currentIds = new Set<number>();
|
||||||
|
|
||||||
if (!this.userCoursesIds[course.id]) {
|
courses.forEach((course) => {
|
||||||
// Course added.
|
// Move category field to categoryid on a course.
|
||||||
added.push(course.id);
|
course.categoryid = course.category;
|
||||||
}
|
delete course.category;
|
||||||
});
|
|
||||||
|
|
||||||
if (courses.length - added.length != previousIds.length) {
|
currentIds.add(course.id);
|
||||||
// A course was removed, check which one.
|
|
||||||
previousIds.forEach((id) => {
|
if (!previousIds.has(course.id)) {
|
||||||
if (!currentIds[id]) {
|
// Course added.
|
||||||
// Course removed.
|
added.push(course.id);
|
||||||
removed.push(Number(id));
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (courses.length - added.length !== previousIds.size) {
|
||||||
|
// A course was removed, check which one.
|
||||||
|
previousIds.forEach((id) => {
|
||||||
|
if (!currentIds.has(id)) {
|
||||||
|
// Course removed.
|
||||||
|
removed.push(Number(id));
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (added.length || removed.length) {
|
if (added.length || removed.length) {
|
||||||
// At least 1 course was added or removed, trigger the event.
|
// At least 1 course was added or removed, trigger the event.
|
||||||
CoreEvents.trigger(CoreCoursesProvider.EVENT_MY_COURSES_CHANGED, {
|
CoreEvents.trigger(CoreCoursesProvider.EVENT_MY_COURSES_CHANGED, {
|
||||||
added: added,
|
added: added,
|
||||||
removed: removed,
|
removed: removed,
|
||||||
}, site.getId());
|
}, site.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
this.userCoursesIds = currentIds;
|
this.userCoursesIds = currentIds;
|
||||||
} else {
|
} else {
|
||||||
this.userCoursesIds = {};
|
const coursesIds = new Set<number>();
|
||||||
|
|
||||||
// Store the list of courses.
|
// Store the list of courses.
|
||||||
courses.forEach((course) => {
|
courses.forEach((course) => {
|
||||||
// Move category field to categoryid on a course.
|
coursesIds.add(course.id);
|
||||||
course.categoryid = course.category;
|
|
||||||
delete course.category;
|
|
||||||
|
|
||||||
this.userCoursesIds[course.id] = true;
|
// Move category field to categoryid on a course.
|
||||||
});
|
course.categoryid = course.category;
|
||||||
}
|
delete course.category;
|
||||||
|
});
|
||||||
|
|
||||||
return courses;
|
this.userCoursesIds = coursesIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
return courses;
|
||||||
|
}));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1312,12 +1427,12 @@ export type CoreEnrolledCourseData = CoreEnrolledCourseBasicData & {
|
||||||
completionhascriteria?: boolean; // If completion criteria is set.
|
completionhascriteria?: boolean; // If completion criteria is set.
|
||||||
completionusertracked?: boolean; // If the user is completion tracked.
|
completionusertracked?: boolean; // If the user is completion tracked.
|
||||||
progress?: number | null; // Progress percentage.
|
progress?: number | null; // Progress percentage.
|
||||||
completed?: boolean; // Whether the course is completed.
|
completed?: boolean; // @since 3.6. Whether the course is completed.
|
||||||
marker?: number; // Course section marker.
|
marker?: number; // @since 3.6. Course section marker.
|
||||||
lastaccess?: number; // Last access to the course (timestamp).
|
lastaccess?: number; // @since 3.6. Last access to the course (timestamp).
|
||||||
isfavourite?: boolean; // If the user marked this course a favourite.
|
isfavourite?: boolean; // If the user marked this course a favourite.
|
||||||
hidden?: boolean; // If the user hide the course from the dashboard.
|
hidden?: boolean; // If the user hide the course from the dashboard.
|
||||||
overviewfiles?: CoreWSExternalFile[];
|
overviewfiles?: CoreWSExternalFile[]; // @since 3.6.
|
||||||
showactivitydates?: boolean; // @since 3.11. Whether the activity dates are shown or not.
|
showactivitydates?: boolean; // @since 3.11. Whether the activity dates are shown or not.
|
||||||
showcompletionconditions?: boolean; // @since 3.11. Whether the activity completion conditions are shown or not.
|
showcompletionconditions?: boolean; // @since 3.11. Whether the activity completion conditions are shown or not.
|
||||||
timemodified?: number; // @since 4.0. Last time course settings were updated (timestamp).
|
timemodified?: number; // @since 4.0. Last time course settings were updated (timestamp).
|
||||||
|
|
Loading…
Reference in New Issue