2020-11-20 11:08:30 +00:00
|
|
|
// (C) Copyright 2015 Moodle Pty Ltd.
|
|
|
|
//
|
|
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
// you may not use this file except in compliance with the License.
|
|
|
|
// You may obtain a copy of the License at
|
|
|
|
//
|
|
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
//
|
|
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
// See the License for the specific language governing permissions and
|
|
|
|
// limitations under the License.
|
|
|
|
|
|
|
|
import { Injectable } from '@angular/core';
|
|
|
|
import { CoreUtils } from '@services/utils/utils';
|
2020-12-24 10:47:03 +00:00
|
|
|
import { CoreSites, CoreSitesCommonWSOptions } from '@services/sites';
|
2021-09-20 07:47:27 +00:00
|
|
|
import {
|
|
|
|
CoreCourseAnyCourseDataWithOptions,
|
|
|
|
CoreCourses,
|
|
|
|
CoreCourseSearchedData,
|
|
|
|
CoreCourseUserAdminOrNavOptionIndexed,
|
|
|
|
CoreEnrolledCourseData,
|
|
|
|
} from './courses';
|
2020-12-14 13:50:37 +00:00
|
|
|
import { makeSingleton, Translate } from '@singletons';
|
2020-11-19 15:37:46 +00:00
|
|
|
import { CoreWSExternalFile } from '@services/ws';
|
2022-02-01 15:50:03 +00:00
|
|
|
import { AddonCourseCompletion } from '@addons/coursecompletion/services/coursecompletion';
|
2020-11-20 11:08:30 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Helper to gather some common courses functions.
|
|
|
|
*/
|
2020-12-09 13:38:53 +00:00
|
|
|
@Injectable({ providedIn: 'root' })
|
2020-11-20 11:08:30 +00:00
|
|
|
export class CoreCoursesHelperProvider {
|
|
|
|
|
2021-11-17 16:26:22 +00:00
|
|
|
protected courseSiteColors: Record<string, (string | undefined)[]> = {};
|
|
|
|
|
2020-11-20 11:08:30 +00:00
|
|
|
/**
|
|
|
|
* Get the courses to display the course picker popover. If a courseId is specified, it will also return its categoryId.
|
|
|
|
*
|
|
|
|
* @param courseId Course ID to get the category.
|
|
|
|
* @return Promise resolved with the list of courses and the category.
|
|
|
|
*/
|
2020-12-14 13:50:37 +00:00
|
|
|
async getCoursesForPopover(courseId?: number): Promise<{courses: Partial<CoreEnrolledCourseData>[]; categoryId?: number}> {
|
2021-03-02 10:41:04 +00:00
|
|
|
const courses: Partial<CoreEnrolledCourseData>[] = await CoreCourses.getUserCourses(false);
|
2020-12-14 13:50:37 +00:00
|
|
|
|
|
|
|
// Add "All courses".
|
|
|
|
courses.unshift({
|
|
|
|
id: -1,
|
2021-03-02 10:41:04 +00:00
|
|
|
fullname: Translate.instant('core.fulllistofcourses'),
|
2020-12-14 13:50:37 +00:00
|
|
|
categoryid: -1,
|
|
|
|
});
|
|
|
|
|
|
|
|
let categoryId: number | undefined;
|
|
|
|
if (courseId) {
|
|
|
|
// Search the course to get the category.
|
|
|
|
const course = courses.find((course) => course.id == courseId);
|
|
|
|
|
|
|
|
if (course) {
|
|
|
|
categoryId = course.categoryid;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
courses: courses,
|
|
|
|
categoryId: categoryId,
|
|
|
|
};
|
2020-11-20 11:08:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Given a course object returned by core_enrol_get_users_courses and another one returned by core_course_get_courses_by_field,
|
|
|
|
* load some extra data to the first one.
|
|
|
|
*
|
|
|
|
* @param course Course returned by core_enrol_get_users_courses.
|
|
|
|
* @param courseByField Course returned by core_course_get_courses_by_field.
|
|
|
|
* @param addCategoryName Whether add category name or not.
|
|
|
|
*/
|
2021-11-17 16:26:22 +00:00
|
|
|
protected loadCourseExtraInfo(
|
2020-11-20 11:08:30 +00:00
|
|
|
course: CoreEnrolledCourseDataWithExtraInfo,
|
|
|
|
courseByField: CoreCourseSearchedData,
|
|
|
|
addCategoryName: boolean = false,
|
|
|
|
): void {
|
|
|
|
if (courseByField) {
|
|
|
|
course.displayname = courseByField.displayname;
|
|
|
|
course.categoryname = addCategoryName ? courseByField.categoryname : undefined;
|
2020-11-19 15:37:46 +00:00
|
|
|
course.overviewfiles = course.overviewfiles || courseByField.overviewfiles;
|
2020-11-20 11:08:30 +00:00
|
|
|
} else {
|
|
|
|
delete course.displayname;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-20 07:47:27 +00:00
|
|
|
/**
|
|
|
|
* Loads the color of courses or the thumb image.
|
|
|
|
*
|
|
|
|
* @param courses List of courses.
|
|
|
|
* @return Promise resolved when done.
|
|
|
|
*/
|
|
|
|
async loadCoursesColorAndImage(courses: CoreCourseSearchedData[]): Promise<void> {
|
|
|
|
if (!courses.length) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-11-17 16:26:22 +00:00
|
|
|
await Promise.all(courses.map((course) => this.loadCourseColorAndImage(course)));
|
2021-09-20 07:47:27 +00:00
|
|
|
}
|
|
|
|
|
2020-11-20 11:08:30 +00:00
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*/
|
|
|
|
async loadCoursesExtraInfo(courses: CoreEnrolledCourseDataWithExtraInfo[], loadCategoryNames: boolean = false): Promise<void> {
|
|
|
|
if (!courses.length ) {
|
|
|
|
// No courses or cannot get the data, stop.
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let coursesInfo = {};
|
|
|
|
let courseInfoAvailable = false;
|
|
|
|
|
2021-09-03 09:51:26 +00:00
|
|
|
if (loadCategoryNames || (courses[0].overviewfiles === undefined && courses[0].displayname === undefined)) {
|
2020-11-20 11:08:30 +00:00
|
|
|
const courseIds = courses.map((course) => course.id).join(',');
|
|
|
|
|
|
|
|
courseInfoAvailable = true;
|
|
|
|
|
|
|
|
// Get the extra data for the courses.
|
2021-11-17 16:26:22 +00:00
|
|
|
const coursesInfosArray = await CoreCourses.getCoursesByField('ids', courseIds);
|
2020-11-20 11:08:30 +00:00
|
|
|
|
2021-11-17 16:26:22 +00:00
|
|
|
coursesInfo = CoreUtils.arrayToObject(coursesInfosArray, 'id');
|
2020-11-20 11:08:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
courses.forEach((course) => {
|
2021-11-17 16:26:22 +00:00
|
|
|
this.loadCourseExtraInfo(course, courseInfoAvailable ? coursesInfo[course.id] : course, loadCategoryNames);
|
2020-11-19 15:37:46 +00:00
|
|
|
});
|
|
|
|
}
|
2020-11-20 11:08:30 +00:00
|
|
|
|
2020-11-19 15:37:46 +00:00
|
|
|
/**
|
|
|
|
* Load course colors from site config.
|
|
|
|
*
|
|
|
|
* @return course colors RGB.
|
|
|
|
*/
|
|
|
|
protected async loadCourseSiteColors(): Promise<(string | undefined)[]> {
|
2021-11-17 16:26:22 +00:00
|
|
|
const site = CoreSites.getRequiredCurrentSite();
|
|
|
|
const siteId = site.getId();
|
|
|
|
|
|
|
|
if (this.courseSiteColors[siteId] !== undefined) {
|
|
|
|
return this.courseSiteColors[siteId];
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!site.isVersionGreaterEqualThan('3.8')) {
|
|
|
|
this.courseSiteColors[siteId] = [];
|
|
|
|
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
|
2020-11-19 15:37:46 +00:00
|
|
|
const colors: (string | undefined)[] = [];
|
|
|
|
|
2021-11-17 16:26:22 +00:00
|
|
|
try {
|
|
|
|
const configs = await site.getConfig();
|
|
|
|
for (let x = 0; x < 10; x++) {
|
|
|
|
colors[x] = configs['core_admin_coursecolor' + (x + 1)] || undefined;
|
2020-11-20 11:08:30 +00:00
|
|
|
}
|
2021-11-17 16:26:22 +00:00
|
|
|
|
|
|
|
this.courseSiteColors[siteId] = colors;
|
|
|
|
} catch {
|
|
|
|
// Ignore errors.
|
2020-11-19 15:37:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return colors;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Loads the color of the course or the thumb image.
|
|
|
|
*
|
|
|
|
* @param course Course data.
|
|
|
|
*/
|
2021-11-17 16:26:22 +00:00
|
|
|
async loadCourseColorAndImage(course: CoreCourseWithImageAndColor): Promise<void> {
|
2020-11-19 15:37:46 +00:00
|
|
|
if (course.overviewfiles && course.overviewfiles[0]) {
|
|
|
|
course.courseImage = course.overviewfiles[0].fileurl;
|
2021-11-17 16:26:22 +00:00
|
|
|
|
|
|
|
return;
|
2020-11-19 15:37:46 +00:00
|
|
|
}
|
2021-11-17 16:26:22 +00:00
|
|
|
|
|
|
|
const colors = await this.loadCourseSiteColors();
|
|
|
|
|
|
|
|
course.colorNumber = course.id % 10;
|
|
|
|
course.color = colors.length ? colors[course.colorNumber] : undefined;
|
2020-11-20 11:08:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get user courses with admin and nav options.
|
|
|
|
*
|
|
|
|
* @param sort Sort courses after get them. If sort is not defined it won't be sorted.
|
|
|
|
* @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.
|
|
|
|
* @return Courses filled with options.
|
|
|
|
*/
|
2020-12-10 10:45:27 +00:00
|
|
|
async getUserCoursesWithOptions(
|
|
|
|
sort: string = 'fullname',
|
|
|
|
slice: number = 0,
|
|
|
|
filter?: string,
|
|
|
|
loadCategoryNames: boolean = false,
|
2020-12-24 10:47:03 +00:00
|
|
|
options: CoreSitesCommonWSOptions = {},
|
2020-12-10 10:45:27 +00:00
|
|
|
): Promise<CoreEnrolledCourseDataWithOptions[]> {
|
|
|
|
|
2020-12-24 10:47:03 +00:00
|
|
|
let courses: CoreEnrolledCourseDataWithOptions[] = await CoreCourses.getUserCourses(
|
|
|
|
false,
|
|
|
|
options.siteId,
|
|
|
|
options.readingStrategy,
|
|
|
|
);
|
2020-12-10 10:45:27 +00:00
|
|
|
if (courses.length <= 0) {
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
|
|
|
|
const promises: Promise<void>[] = [];
|
|
|
|
const courseIds = courses.map((course) => course.id);
|
|
|
|
|
2021-09-03 09:51:26 +00:00
|
|
|
// Load course options of the course.
|
2020-12-24 10:47:03 +00:00
|
|
|
promises.push(CoreCourses.getCoursesAdminAndNavOptions(courseIds, options.siteId).then((options) => {
|
2021-09-03 09:51:26 +00:00
|
|
|
courses.forEach((course) => {
|
|
|
|
course.navOptions = options.navOptions[course.id];
|
|
|
|
course.admOptions = options.admOptions[course.id];
|
|
|
|
});
|
2020-12-10 10:45:27 +00:00
|
|
|
|
2021-09-03 09:51:26 +00:00
|
|
|
return;
|
|
|
|
}));
|
2020-12-10 10:45:27 +00:00
|
|
|
|
|
|
|
promises.push(this.loadCoursesExtraInfo(courses, loadCategoryNames));
|
|
|
|
|
|
|
|
await Promise.all(promises);
|
|
|
|
|
|
|
|
switch (filter) {
|
|
|
|
case 'isfavourite':
|
|
|
|
courses = courses.filter((course) => !!course.isfavourite);
|
|
|
|
break;
|
|
|
|
default:
|
2021-12-03 10:10:44 +00:00
|
|
|
// Filter not implemented.
|
2020-12-10 10:45:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
switch (sort) {
|
|
|
|
case 'fullname':
|
|
|
|
courses.sort((a, b) => {
|
|
|
|
const compareA = a.fullname.toLowerCase();
|
|
|
|
const compareB = b.fullname.toLowerCase();
|
|
|
|
|
|
|
|
return compareA.localeCompare(compareB);
|
|
|
|
});
|
|
|
|
break;
|
|
|
|
case 'lastaccess':
|
|
|
|
courses.sort((a, b) => (b.lastaccess || 0) - (a.lastaccess || 0));
|
|
|
|
break;
|
2021-12-03 10:10:44 +00:00
|
|
|
// Time modified property is defined on Moodle 4.0.
|
|
|
|
case 'timemodified':
|
|
|
|
courses.sort((a, b) => (b.timemodified || 0) - (a.timemodified || 0));
|
|
|
|
break;
|
2020-12-10 10:45:27 +00:00
|
|
|
case 'shortname':
|
|
|
|
courses.sort((a, b) => {
|
|
|
|
const compareA = a.shortname.toLowerCase();
|
|
|
|
const compareB = b.shortname.toLowerCase();
|
|
|
|
|
|
|
|
return compareA.localeCompare(compareB);
|
|
|
|
});
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
// Sort not implemented. Do not sort.
|
|
|
|
}
|
|
|
|
|
|
|
|
courses = slice > 0 ? courses.slice(0, slice) : courses;
|
|
|
|
|
|
|
|
return Promise.all(courses.map(async (course) => {
|
2021-12-16 09:46:40 +00:00
|
|
|
if (course.completed !== undefined) {
|
2020-12-10 10:45:27 +00:00
|
|
|
// The WebService already returns the completed status, no need to fetch it.
|
|
|
|
return course;
|
|
|
|
}
|
|
|
|
|
2021-12-16 09:46:40 +00:00
|
|
|
if (course.enablecompletion !== undefined && !course.enablecompletion) {
|
2020-12-10 10:45:27 +00:00
|
|
|
// Completion is disabled for this course, there is no need to fetch the completion status.
|
|
|
|
return course;
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
2020-12-24 10:47:03 +00:00
|
|
|
const completion = await AddonCourseCompletion.getCompletion(course.id, undefined, undefined, options.siteId);
|
2020-12-10 10:45:27 +00:00
|
|
|
|
|
|
|
course.completed = completion?.completed;
|
|
|
|
} catch {
|
|
|
|
// Ignore error, maybe course completion is disabled or user has no permission.
|
|
|
|
course.completed = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return course;
|
|
|
|
}));
|
2020-11-20 11:08:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2021-03-02 10:41:04 +00:00
|
|
|
export const CoreCoursesHelper = makeSingleton(CoreCoursesHelperProvider);
|
2020-11-20 11:08:30 +00:00
|
|
|
|
|
|
|
/**
|
2020-11-19 15:37:46 +00:00
|
|
|
* Course with colors info and course image.
|
2020-11-20 11:08:30 +00:00
|
|
|
*/
|
2020-11-19 15:37:46 +00:00
|
|
|
export type CoreCourseWithImageAndColor = {
|
|
|
|
id: number; // Course id.
|
|
|
|
overviewfiles?: CoreWSExternalFile[];
|
2020-11-20 11:08:30 +00:00
|
|
|
colorNumber?: number; // Color index number.
|
|
|
|
color?: string; // Color RGB.
|
|
|
|
courseImage?: string; // Course thumbnail.
|
2020-11-19 15:37:46 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Enrolled course data with extra rendering info.
|
|
|
|
*/
|
|
|
|
export type CoreEnrolledCourseDataWithExtraInfo = CoreCourseWithImageAndColor & CoreEnrolledCourseData & {
|
2020-11-20 11:08:30 +00:00
|
|
|
categoryname?: string; // Category name,
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Enrolled course data with admin and navigation option availability.
|
|
|
|
*/
|
|
|
|
export type CoreEnrolledCourseDataWithOptions = CoreEnrolledCourseData & {
|
|
|
|
navOptions?: CoreCourseUserAdminOrNavOptionIndexed;
|
|
|
|
admOptions?: CoreCourseUserAdminOrNavOptionIndexed;
|
|
|
|
};
|
|
|
|
|
2021-09-20 07:47:27 +00:00
|
|
|
/**
|
|
|
|
* Course summary data with admin and navigation option availability.
|
|
|
|
*/
|
|
|
|
export type CoreCourseSearchedDataWithOptions = CoreCourseSearchedData & {
|
|
|
|
navOptions?: CoreCourseUserAdminOrNavOptionIndexed;
|
|
|
|
admOptions?: CoreCourseUserAdminOrNavOptionIndexed;
|
|
|
|
};
|
|
|
|
|
2020-11-20 11:08:30 +00:00
|
|
|
/**
|
|
|
|
* Enrolled course data with admin and navigation option availability and extra rendering info.
|
|
|
|
*/
|
|
|
|
export type CoreEnrolledCourseDataWithExtraInfoAndOptions = CoreEnrolledCourseDataWithExtraInfo & CoreEnrolledCourseDataWithOptions;
|
2021-09-20 07:47:27 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Searched course data with admin and navigation option availability and extra rendering info.
|
|
|
|
*/
|
|
|
|
export type CoreCourseSearchedDataWithExtraInfoAndOptions = CoreCourseWithImageAndColor & CoreCourseSearchedDataWithOptions;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Any course data with admin and navigation option availability and extra rendering info.
|
|
|
|
*/
|
|
|
|
export type CoreCourseAnyCourseDataWithExtraInfoAndOptions = CoreCourseWithImageAndColor & CoreCourseAnyCourseDataWithOptions & {
|
|
|
|
categoryname?: string; // Category name,
|
|
|
|
};
|