MOBILE-3817 courses: Implement getUserCoursesWithOptionsObservable

main
Dani Palou 2022-07-01 14:48:55 +02:00
parent 73b108e5c5
commit fbe46ee895
4 changed files with 518 additions and 227 deletions

View File

@ -14,13 +14,16 @@
import { Injectable } from '@angular/core';
import { CoreLogger } from '@singletons/logger';
import { CoreSites } from '@services/sites';
import { CoreSites, CoreSitesCommonWSOptions } from '@services/sites';
import { CoreUtils } from '@services/utils/utils';
import { CoreCourses } from '@features/courses/services/courses';
import { CoreSite, CoreSiteWSPreSets } from '@classes/site';
import { CoreStatusWithWarningsWSResponse, CoreWSExternalWarning } from '@services/ws';
import { makeSingleton } from '@singletons';
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:';
@ -93,33 +96,55 @@ export class AddonCourseCompletionProvider {
* @param siteId Site ID. If not defined, use current site.
* @return Promise to be resolved when the completion is retrieved.
*/
async getCompletion(
getCompletion(
courseId: number,
userId?: number,
preSets: CoreSiteWSPreSets = {},
siteId?: string,
): Promise<AddonCourseCompletionCourseCompletionStatus> {
return firstValueFrom(this.getCompletionObservable(courseId, {
userId,
preSets,
siteId,
}));
}
const site = await CoreSites.getSite(siteId);
userId = userId || site.getUserId();
this.logger.debug('Get completion for course ' + courseId + ' and user ' + userId);
/**
* Get course completion status for a certain course and user.
*
* @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 = {
courseid: courseId,
userid: userId,
};
const userId = options.userId || site.getUserId();
this.logger.debug('Get completion for course ' + courseId + ' and user ' + userId);
preSets.cacheKey = this.getCompletionCacheKey(courseId, userId);
preSets.updateFrequency = preSets.updateFrequency || CoreSite.FREQUENCY_SOMETIMES;
preSets.cacheErrors = ['notenroled'];
const data: AddonCourseCompletionGetCourseCompletionStatusWSParams = {
courseid: courseId,
userid: userId,
};
const result: AddonCourseCompletionGetCourseCompletionStatusWSResponse =
await site.read('core_completion_get_course_completion_status', data, preSets);
if (result.completionstatus) {
return result.completionstatus;
}
const preSets = {
...(options.preSets ?? {}),
cacheKey: this.getCompletionCacheKey(courseId, userId),
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 = {
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.
};

View File

@ -57,8 +57,8 @@ import {
WSGroups,
WS_CACHE_TABLES_PREFIX,
} from '@services/database/sites';
import { Observable, Subject } from 'rxjs';
import { finalize, map } from 'rxjs/operators';
import { Observable, ObservableInput, ObservedValueOf, OperatorFunction, Subject } from 'rxjs';
import { finalize, map, mergeMap } from 'rxjs/operators';
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.
*/

View File

@ -26,6 +26,10 @@ import { makeSingleton, Translate } from '@singletons';
import { CoreWSExternalFile } from '@services/ws';
import { AddonCourseCompletion } from '@addons/coursecompletion/services/coursecompletion';
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.
@ -111,29 +115,47 @@ export class CoreCoursesHelperProvider {
* @param loadCategoryNames Whether load category names or not.
* @return Promise resolved when done.
*/
async loadCoursesExtraInfo(courses: CoreEnrolledCourseDataWithExtraInfo[], loadCategoryNames: boolean = false): Promise<void> {
if (!courses.length ) {
// No courses or cannot get the data, stop.
return;
loadCoursesExtraInfo(
courses: CoreEnrolledCourseDataWithExtraInfo[],
loadCategoryNames: boolean = false,
): 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 = {};
let courseInfoAvailable = false;
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');
if (!loadCategoryNames && (courses[0].overviewfiles !== undefined || courses[0].displayname !== undefined)) {
// No need to load more data.
return of(courses);
}
courses.forEach((course) => {
this.loadCourseExtraInfo(course, courseInfoAvailable ? coursesInfo[course.id] : course, loadCategoryNames);
});
const courseIds = courses.map((course) => course.id).join(',');
// 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 filter Filter using some field.
* @param loadCategoryNames Whether load category names or not.
* @param options Options.
* @return Courses filled with options.
*/
async getUserCoursesWithOptions(
getUserCoursesWithOptions(
sort: string = 'fullname',
slice: number = 0,
filter?: string,
loadCategoryNames: boolean = false,
options: CoreSitesCommonWSOptions = {},
): Promise<CoreEnrolledCourseDataWithOptions[]> {
let courses: CoreEnrolledCourseDataWithOptions[] = await CoreCourses.getUserCourses(
false,
options.siteId,
options.readingStrategy,
);
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;
return firstValueFrom(this.getUserCoursesWithOptionsObservable({
sort,
slice,
filter,
loadCategoryNames,
...options,
}));
}
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':
courses = courses.filter((course) => !!course.isfavourite);
break;
@ -270,28 +325,42 @@ export class CoreCoursesHelperProvider {
courses = slice > 0 ? courses.slice(0, slice) : courses;
return Promise.all(courses.map(async (course) => {
if (course.completed !== undefined) {
// The WebService already returns the completed status, no need to fetch it.
return courses;
}
/**
* 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;
}
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 {
}),
catchError(() => {
// Ignore error, maybe course completion is disabled or user has no permission.
course.completed = false;
}
return course;
}));
return of(course);
}),
);
}
/**
@ -402,3 +471,13 @@ export type CoreCourseSearchedDataWithExtraInfoAndOptions = CoreCourseWithImageA
export type CoreCourseAnyCourseDataWithExtraInfoAndOptions = CoreCourseWithImageAndColor & CoreCourseAnyCourseDataWithOptions & {
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.
};

View File

@ -20,7 +20,9 @@ import { CoreStatusWithWarningsWSResponse, CoreWarningsWSResponse, CoreWSExterna
import { CoreEvents } from '@singletons/events';
import { CoreWSError } from '@classes/errors/wserror';
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:';
@ -63,8 +65,7 @@ export class CoreCoursesProvider {
static readonly STATE_HIDDEN = 'hidden';
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;
/**
@ -484,60 +485,91 @@ export class CoreCoursesProvider {
* @param siteId Site ID. If not defined, use current site.
* @return Promise resolved with the courses.
*/
async getCoursesByField(
getCoursesByField(
field: string = '',
value: string | number = '',
siteId?: string,
): 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;
field = fieldParams.field;
value = fieldParams.value;
const data: CoreCourseGetCoursesByFieldWSParams = {
field: field,
value: field ? value : '',
};
const preSets: CoreSiteWSPreSets = {
cacheKey: this.getCoursesByFieldCacheKey(field, value),
updateFrequency: CoreSite.FREQUENCY_RARELY,
};
const hasChanged = fieldParams.field != field || fieldParams.value != value;
field = fieldParams.field;
value = fieldParams.value;
const data: CoreCourseGetCoursesByFieldWSParams = {
field: field,
value: field ? value : '',
};
const preSets: CoreSiteWSPreSets = {
cacheKey: this.getCoursesByFieldCacheKey(field, value),
updateFrequency: CoreSite.FREQUENCY_RARELY,
...CoreSites.getReadingStrategyPreSets(options.readingStrategy),
};
const response = await site.read<CoreCourseGetCoursesByFieldWSResponse>('core_course_get_courses_by_field', data, preSets);
if (!response.courses) {
throw Error('WS core_course_get_courses_by_field failed');
}
const observable = site.readObservable<CoreCourseGetCoursesByFieldWSResponse>(
'core_course_get_courses_by_field',
data,
preSets,
);
if (field == 'ids' && hasChanged) {
// 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));
return observable.pipe(map(response => {
if (!response.courses) {
throw Error('WS core_course_get_courses_by_field failed');
}
// Only courses from the original selection.
response.courses = response.courses.filter((course) => courseIds.indexOf(course.id) >= 0);
}
if (field == 'ids' && hasChanged) {
// 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.
return response.courses.sort((a, b) => {
if (a.sortorder === undefined && b.sortorder === undefined) {
return b.id - a.id;
}
// Only courses from the original selection.
response.courses = response.courses.filter((course) => courseIds.indexOf(course.id) >= 0);
}
if (a.sortorder === undefined) {
return 1;
}
// Courses will be sorted using sortorder if available.
return response.courses.sort((a, b) => {
if (a.sortorder === undefined && b.sortorder === undefined) {
return b.id - a.id;
}
if (b.sortorder === undefined) {
return -1;
}
if (a.sortorder === undefined) {
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.
* @return Promise resolved with the options for each course.
*/
async getCoursesAdminAndNavOptions(
getCoursesAdminAndNavOptions(
courseIds: number[],
siteId?: string,
): Promise<{
navOptions: 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.
const [navOptions, admOptions] = await Promise.all([
CoreUtils.ignoreErrors(this.getUserNavigationOptions(courseIds, siteId), {}),
CoreUtils.ignoreErrors(this.getUserAdministrationOptions(courseIds, siteId), {}),
]);
return asyncObservable(async () => {
const siteId = options.siteId || CoreSites.getCurrentSiteId();
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.
* @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) {
return {};
return of({});
}
const site = await CoreSites.getSite(siteId);
return asyncObservable(async () => {
const site = await CoreSites.getSite(options.siteId);
const params: CoreCourseGetUserAdminOrNavOptionsWSParams = {
courseids: courseIds,
};
const preSets: CoreSiteWSPreSets = {
cacheKey: this.getUserAdministrationOptionsCacheKey(courseIds),
updateFrequency: CoreSite.FREQUENCY_RARELY,
};
const params: CoreCourseGetUserAdminOrNavOptionsWSParams = {
courseids: courseIds,
};
const preSets: CoreSiteWSPreSets = {
cacheKey: this.getUserAdministrationOptionsCacheKey(courseIds),
updateFrequency: CoreSite.FREQUENCY_RARELY,
...CoreSites.getReadingStrategyPreSets(options.readingStrategy),
};
const response: CoreCourseGetUserAdminOrNavOptionsWSResponse =
await site.read('core_course_get_user_administration_options', params, preSets);
const observable = site.readObservable<CoreCourseGetUserAdminOrNavOptionsWSResponse>(
'core_course_get_user_administration_options',
params,
preSets,
);
// Format returned data.
return this.formatUserAdminOrNavOptions(response.courses);
// Format returned data.
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.
*/
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) {
return {};
return of({});
}
const site = await CoreSites.getSite(siteId);
return asyncObservable(async () => {
const site = await CoreSites.getSite(options.siteId);
const params: CoreCourseGetUserAdminOrNavOptionsWSParams = {
courseids: courseIds,
};
const preSets: CoreSiteWSPreSets = {
cacheKey: this.getUserNavigationOptionsCacheKey(courseIds),
updateFrequency: CoreSite.FREQUENCY_RARELY,
};
const params: CoreCourseGetUserAdminOrNavOptionsWSParams = {
courseids: courseIds,
};
const preSets: CoreSiteWSPreSets = {
cacheKey: this.getUserNavigationOptionsCacheKey(courseIds),
updateFrequency: CoreSite.FREQUENCY_RARELY,
...CoreSites.getReadingStrategyPreSets(options.readingStrategy),
};
const response: CoreCourseGetUserAdminOrNavOptionsWSResponse =
await site.read('core_course_get_user_navigation_options', params, preSets);
const observable = site.readObservable<CoreCourseGetUserAdminOrNavOptionsWSResponse>(
'core_course_get_user_navigation_options',
params,
preSets,
);
// Format returned data.
return this.formatUserAdminOrNavOptions(response.courses);
// Format returned data.
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 siteId Site to get the courses from. If not defined, use current site.
* @param strategy Reading strategy.
* @return Promise resolved with the courses.
*/
async getUserCourses(
getUserCourses(
preferCache: boolean = false,
siteId?: string,
strategy?: CoreSitesReadingStrategy,
): Promise<CoreEnrolledCourseData[]> {
const site = await CoreSites.getSite(siteId);
strategy = strategy ?? (preferCache ? CoreSitesReadingStrategy.PREFER_CACHE : undefined);
const userId = site.getUserId();
const wsParams: CoreEnrolGetUsersCoursesWSParams = {
userid: userId,
};
const strategyPreSets = strategy
? CoreSites.getReadingStrategyPreSets(strategy)
: { omitExpires: !!preferCache };
return this.getUserCoursesObservable({
readingStrategy: strategy,
siteId,
}).toPromise();
}
const preSets = {
cacheKey: this.getUserCoursesCacheKey(),
getCacheUsingCacheKey: true,
updateFrequency: CoreSite.FREQUENCY_RARELY,
...strategyPreSets,
};
/**
* Get user courses.
*
* @param options Options.
* @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')) {
wsParams.returnusercount = false;
}
const userId = site.getUserId();
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) {
// Check if the list of courses has changed.
const added: number[] = [];
const removed: number[] = [];
const previousIds = Object.keys(this.userCoursesIds);
const currentIds = {}; // Use an object to make it faster to search.
if (site.isVersionGreaterEqualThan('3.7')) {
wsParams.returnusercount = false;
}
courses.forEach((course) => {
// Move category field to categoryid on a course.
course.categoryid = course.category;
delete course.category;
const observable = site.readObservable<CoreEnrolGetUsersCoursesWSResponse>(
'core_enrol_get_users_courses',
wsParams,
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]) {
// Course added.
added.push(course.id);
}
});
courses.forEach((course) => {
// Move category field to categoryid on a course.
course.categoryid = course.category;
delete course.category;
if (courses.length - added.length != previousIds.length) {
// A course was removed, check which one.
previousIds.forEach((id) => {
if (!currentIds[id]) {
// Course removed.
removed.push(Number(id));
currentIds.add(course.id);
if (!previousIds.has(course.id)) {
// Course added.
added.push(course.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) {
// At least 1 course was added or removed, trigger the event.
CoreEvents.trigger(CoreCoursesProvider.EVENT_MY_COURSES_CHANGED, {
added: added,
removed: removed,
}, site.getId());
}
if (added.length || removed.length) {
// At least 1 course was added or removed, trigger the event.
CoreEvents.trigger(CoreCoursesProvider.EVENT_MY_COURSES_CHANGED, {
added: added,
removed: removed,
}, site.getId());
}
this.userCoursesIds = currentIds;
} else {
this.userCoursesIds = {};
this.userCoursesIds = currentIds;
} else {
const coursesIds = new Set<number>();
// Store the list of courses.
courses.forEach((course) => {
// Move category field to categoryid on a course.
course.categoryid = course.category;
delete course.category;
// Store the list of courses.
courses.forEach((course) => {
coursesIds.add(course.id);
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.
completionusertracked?: boolean; // If the user is completion tracked.
progress?: number | null; // Progress percentage.
completed?: boolean; // Whether the course is completed.
marker?: number; // Course section marker.
lastaccess?: number; // Last access to the course (timestamp).
completed?: boolean; // @since 3.6. Whether the course is completed.
marker?: number; // @since 3.6. Course section marker.
lastaccess?: number; // @since 3.6. Last access to the course (timestamp).
isfavourite?: boolean; // If the user marked this course a favourite.
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.
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).