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 { 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.
};

View File

@ -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.
*/ */

View File

@ -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.
};

View File

@ -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).