MOBILE-3817 courses: Implement getUserCoursesWithOptionsObservable
parent
73b108e5c5
commit
fbe46ee895
|
@ -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.
|
||||
};
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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.
|
||||
};
|
||||
|
|
|
@ -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).
|
||||
|
|
Loading…
Reference in New Issue