diff --git a/src/addons/block/recentlyaccessedcourses/components/recentlyaccessedcourses/recentlyaccessedcourses.ts b/src/addons/block/recentlyaccessedcourses/components/recentlyaccessedcourses/recentlyaccessedcourses.ts index f786edbc2..de9d5a8c9 100644 --- a/src/addons/block/recentlyaccessedcourses/components/recentlyaccessedcourses/recentlyaccessedcourses.ts +++ b/src/addons/block/recentlyaccessedcourses/components/recentlyaccessedcourses/recentlyaccessedcourses.ts @@ -16,7 +16,7 @@ import { Component, OnInit, OnDestroy, Input, OnChanges, SimpleChange } from '@a import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreSites } from '@services/sites'; import { CoreCoursesProvider, CoreCoursesMyCoursesUpdatedEventData, CoreCourses } from '@features/courses/services/courses'; -import { CoreCoursesHelper, CoreEnrolledCourseDataWithOptions } from '@features/courses/services/courses-helper'; +import { CoreCourseSearchedDataWithExtraInfoAndOptions, CoreCoursesHelper } from '@features/courses/services/courses-helper'; import { CoreCourseHelper, CorePrefetchStatusInfo } from '@features/course/services/course-helper'; import { CoreCourseOptionsDelegate } from '@features/course/services/course-options-delegate'; import { AddonCourseCompletion } from '@/addons/coursecompletion/services/coursecompletion'; @@ -35,7 +35,7 @@ export class AddonBlockRecentlyAccessedCoursesComponent extends CoreBlockBaseCom @Input() downloadEnabled = false; - courses: CoreEnrolledCourseDataWithOptions [] = []; + courses: CoreCourseSearchedDataWithExtraInfoAndOptions[] = []; prefetchCoursesData: CorePrefetchStatusInfo = { icon: '', statusTranslatable: 'core.loading', @@ -112,7 +112,7 @@ export class AddonBlockRecentlyAccessedCoursesComponent extends CoreBlockBaseCom protected async invalidateContent(): Promise { const promises: Promise[] = []; - promises.push(CoreCourses.invalidateUserCourses().finally(() => + promises.push(CoreCourses.invalidateRecentCourses().finally(() => // Invalidate course completion data. CoreUtils.allPromises(this.courseIds.map((courseId) => AddonCourseCompletion.invalidateCourseCompletion(courseId))))); @@ -136,7 +136,33 @@ export class AddonBlockRecentlyAccessedCoursesComponent extends CoreBlockBaseCom const showCategories = this.block.configsRecord && this.block.configsRecord.displaycategories && this.block.configsRecord.displaycategories.value == '1'; - this.courses = await CoreCoursesHelper.getUserCoursesWithOptions('lastaccess', 10, undefined, showCategories); + const recentCourses = await CoreCourses.getRecentCourses(); + const courseIds = recentCourses.map((course) => course.id); + + // Get the courses using getCoursesByField to get more info about each course. + const courses: CoreCourseSearchedDataWithExtraInfoAndOptions[] = await CoreCourses.getCoursesByField( + 'ids', + courseIds.join(','), + ); + + // Sort them in the original order. + courses.sort((courseA, courseB) => courseIds.indexOf(courseA.id) - courseIds.indexOf(courseB.id)); + + // Get course options and extra info. + const options = await CoreCourses.getCoursesAdminAndNavOptions(courseIds); + courses.forEach((course) => { + course.navOptions = options.navOptions[course.id]; + course.admOptions = options.admOptions[course.id]; + + if (!showCategories) { + course.categoryname = ''; + } + }); + + await CoreCoursesHelper.loadCoursesColorAndImage(courses); + + this.courses = courses; + this.initPrefetchCoursesIcons(); } @@ -148,11 +174,7 @@ export class AddonBlockRecentlyAccessedCoursesComponent extends CoreBlockBaseCom protected async refreshCourseList(): Promise { CoreEvents.trigger(CoreCoursesProvider.EVENT_MY_COURSES_REFRESHED); - try { - await CoreCourses.invalidateUserCourses(); - } catch (error) { - // Ignore errors. - } + await CoreUtils.ignoreErrors(CoreCourses.invalidateRecentCourses()); await this.loadContent(true); } diff --git a/src/core/features/course/services/course-helper.ts b/src/core/features/course/services/course-helper.ts index 9a8d1ffc4..5b322d60d 100644 --- a/src/core/features/course/services/course-helper.ts +++ b/src/core/features/course/services/course-helper.ts @@ -39,7 +39,6 @@ import { CoreCourseSearchedData, CoreEnrolledCourseData, } from '@features/courses/services/courses'; -import { CoreEnrolledCourseDataWithExtraInfoAndOptions } from '@features/courses/services/courses-helper'; import { CoreArray } from '@singletons/array'; import { CoreIonLoadingElement } from '@classes/ion-loading'; import { CoreCourseOffline } from './course-offline'; @@ -424,7 +423,7 @@ export class CoreCourseHelperProvider { * @return Resolved when downloaded, rejected if error or canceled. */ async confirmAndPrefetchCourses( - courses: CoreEnrolledCourseDataWithExtraInfoAndOptions[], + courses: CoreCourseAnyCourseData[], options: CoreCourseConfirmPrefetchCoursesOptions = {}, ): Promise { const siteId = CoreSites.getCurrentSiteId(); @@ -1302,7 +1301,7 @@ export class CoreCourseHelperProvider { * @return Promise resolved when done. */ async prefetchCourses( - courses: CoreEnrolledCourseDataWithExtraInfoAndOptions[], + courses: CoreCourseAnyCourseData[], prefetch: CorePrefetchStatusInfo, options: CoreCoursePrefetchCoursesOptions = {}, ): Promise { diff --git a/src/core/features/courses/components/course-progress/core-courses-course-progress.html b/src/core/features/courses/components/course-progress/core-courses-course-progress.html index 33cd0cfd4..d36b8cb38 100644 --- a/src/core/features/courses/components/course-progress/core-courses-course-progress.html +++ b/src/core/features/courses/components/course-progress/core-courses-course-progress.html @@ -5,7 +5,7 @@

- + - {{ 'core.courses.aria:favourite' | translate }} + {{ 'core.courses.aria:favourite' | translate }} {{ 'core.courses.aria:coursename' | translate }}

@@ -61,10 +61,10 @@
- - + diff --git a/src/core/features/courses/components/course-progress/course-progress.ts b/src/core/features/courses/components/course-progress/course-progress.ts index 2c4b9db69..c4892208b 100644 --- a/src/core/features/courses/components/course-progress/course-progress.ts +++ b/src/core/features/courses/components/course-progress/course-progress.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, Input, OnInit, OnDestroy } from '@angular/core'; +import { Component, Input, OnInit, OnDestroy, OnChanges } from '@angular/core'; import { CoreEventCourseStatusChanged, CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; @@ -21,7 +21,10 @@ import { CoreCourse, CoreCourseProvider } from '@features/course/services/course import { CoreCourseHelper, CorePrefetchStatusInfo } from '@features/course/services/course-helper'; import { Translate } from '@singletons'; import { CoreConstants } from '@/core/constants'; -import { CoreEnrolledCourseDataWithExtraInfoAndOptions } from '../../services/courses-helper'; +import { + CoreCourseAnyCourseDataWithExtraInfoAndOptions, + CoreEnrolledCourseDataWithExtraInfoAndOptions, +} from '../../services/courses-helper'; import { CoreCoursesCourseOptionsMenuComponent } from '../course-options-menu/course-options-menu'; import { CoreUser } from '@features/user/services/user'; @@ -38,9 +41,10 @@ import { CoreUser } from '@features/user/services/user'; templateUrl: 'core-courses-course-progress.html', styleUrls: ['course-progress.scss'], }) -export class CoreCoursesCourseProgressComponent implements OnInit, OnDestroy { +export class CoreCoursesCourseProgressComponent implements OnInit, OnDestroy, OnChanges { - @Input() course!: CoreEnrolledCourseDataWithExtraInfoAndOptions; // The course to render. + // The course to render. + @Input() course!: CoreCourseAnyCourseDataWithExtraInfoAndOptions; @Input() showAll = false; // If true, will show all actions, options, star and progress. @Input() showDownload = true; // If true, will show download button. Only works if the options menu is not shown. @@ -56,13 +60,16 @@ export class CoreCoursesCourseProgressComponent implements OnInit, OnDestroy { showSpinner = false; downloadCourseEnabled = false; courseOptionMenuEnabled = false; + isFavourite = false; + progress = -1; + completionUserTracked: boolean | undefined = false; protected isDestroyed = false; protected courseStatusObserver?: CoreEventObserver; protected siteUpdatedObserver?: CoreEventObserver; /** - * Component being initialized. + * @inheritdoc */ ngOnInit(): void { @@ -73,7 +80,8 @@ export class CoreCoursesCourseProgressComponent implements OnInit, OnDestroy { } // This field is only available from 3.6 onwards. - this.courseOptionMenuEnabled = this.showAll && typeof this.course.isfavourite != 'undefined'; + this.courseOptionMenuEnabled = this.showAll && 'isfavourite' in this.course && + typeof this.course.isfavourite != 'undefined'; // Refresh the enabled flag if site is updated. this.siteUpdatedObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => { @@ -88,6 +96,15 @@ export class CoreCoursesCourseProgressComponent implements OnInit, OnDestroy { }, CoreSites.getCurrentSiteId()); } + /** + * @inheritdoc + */ + ngOnChanges(): void { + this.isFavourite = 'isfavourite' in this.course && !!this.course.isfavourite; + this.progress = 'progress' in this.course ? this.course.progress || -1 : -1; + this.completionUserTracked = 'completionusertracked' in this.course && this.course.completionusertracked; + } + /** * Initialize prefetch course. */ @@ -255,7 +272,7 @@ export class CoreCoursesCourseProgressComponent implements OnInit, OnDestroy { hide ? '1' : undefined, ); - this.course.hidden = hide; + ( this.course).hidden = hide; CoreEvents.trigger(CoreCoursesProvider.EVENT_MY_COURSES_UPDATED, { courseId: this.course.id, course: this.course, @@ -284,7 +301,8 @@ export class CoreCoursesCourseProgressComponent implements OnInit, OnDestroy { try { await CoreCourses.setFavouriteCourse(this.course.id, favourite); - this.course.isfavourite = favourite; + ( this.course).isfavourite = favourite; + this.isFavourite = favourite; CoreEvents.trigger(CoreCoursesProvider.EVENT_MY_COURSES_UPDATED, { courseId: this.course.id, course: this.course, diff --git a/src/core/features/courses/services/courses-helper.ts b/src/core/features/courses/services/courses-helper.ts index f5e1aaec5..150bfb05a 100644 --- a/src/core/features/courses/services/courses-helper.ts +++ b/src/core/features/courses/services/courses-helper.ts @@ -15,7 +15,13 @@ import { Injectable } from '@angular/core'; import { CoreUtils } from '@services/utils/utils'; import { CoreSites } from '@services/sites'; -import { CoreCourses, CoreCourseSearchedData, CoreCourseUserAdminOrNavOptionIndexed, CoreEnrolledCourseData } from './courses'; +import { + CoreCourseAnyCourseDataWithOptions, + CoreCourses, + CoreCourseSearchedData, + CoreCourseUserAdminOrNavOptionIndexed, + CoreEnrolledCourseData, +} from './courses'; import { makeSingleton, Translate } from '@singletons'; import { CoreWSExternalFile } from '@services/ws'; import { AddonCourseCompletion } from '@/addons/coursecompletion/services/coursecompletion'; @@ -83,6 +89,31 @@ export class CoreCoursesHelperProvider { this.loadCourseColorAndImage(course, colors); } + /** + * 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. + */ + /** + * Loads the color of courses or the thumb image. + * + * @param courses List of courses. + * @return Promise resolved when done. + */ + async loadCoursesColorAndImage(courses: CoreCourseSearchedData[]): Promise { + if (!courses.length) { + return; + } + const colors = await this.loadCourseSiteColors(); + + courses.forEach((course) => { + this.loadCourseColorAndImage(course, colors); + }); + } + /** * 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. @@ -301,7 +332,27 @@ export type CoreEnrolledCourseDataWithOptions = CoreEnrolledCourseData & { admOptions?: CoreCourseUserAdminOrNavOptionIndexed; }; +/** + * Course summary data with admin and navigation option availability. + */ +export type CoreCourseSearchedDataWithOptions = CoreCourseSearchedData & { + navOptions?: CoreCourseUserAdminOrNavOptionIndexed; + admOptions?: CoreCourseUserAdminOrNavOptionIndexed; +}; + /** * Enrolled course data with admin and navigation option availability and extra rendering info. */ export type CoreEnrolledCourseDataWithExtraInfoAndOptions = CoreEnrolledCourseDataWithExtraInfo & CoreEnrolledCourseDataWithOptions; + +/** + * 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, +}; diff --git a/src/core/features/courses/services/courses.ts b/src/core/features/courses/services/courses.ts index f16b0e41a..e05585344 100644 --- a/src/core/features/courses/services/courses.ts +++ b/src/core/features/courses/services/courses.ts @@ -14,7 +14,7 @@ import { Injectable } from '@angular/core'; import { CoreLogger } from '@singletons/logger'; -import { CoreSites, CoreSitesReadingStrategy } from '@services/sites'; +import { CoreSites, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@services/sites'; import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; import { makeSingleton } from '@singletons'; import { CoreStatusWithWarningsWSResponse, CoreWarningsWSResponse, CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws'; @@ -45,6 +45,7 @@ declare module '@singletons/events' { export class CoreCoursesProvider { static readonly SEARCH_PER_PAGE = 20; + static readonly RECENT_PER_PAGE = 10; static readonly ENROL_INVALID_KEY = 'CoreCoursesEnrolInvalidKey'; static readonly EVENT_MY_COURSES_CHANGED = 'courses_my_courses_changed'; // User course list changed while app is running. // A course was hidden/favourite, or user enroled in a course. @@ -566,7 +567,7 @@ export class CoreCoursesProvider { customFieldName: string, customFieldValue: string, siteId?: string, - ): Promise { + ): Promise { const site = await CoreSites.getSite(siteId); const params: CoreCourseGetEnrolledCoursesByTimelineClassificationWSParams = { classification: 'customfield', @@ -648,6 +649,40 @@ export class CoreCoursesProvider { return ({ navOptions: navOptions, admOptions: admOptions }); } + /** + * Get cache key for get recent courses WS call. + * + * @param userId User ID. + * @return Cache key. + */ + protected getRecentCoursesCacheKey(userId: number): string { + return `${ROOT_CACHE_KEY}:recentcourses:${userId}`; + } + + /** + * Get recent courses. + * + * @param options Options. + * @return Promise resolved with courses. + * @since 3.6 + */ + async getRecentCourses(options: CoreCourseGetRecentCoursesOptions = {}): Promise { + const site = await CoreSites.getSite(options.siteId); + + const userId = options.userId || site.getUserId(); + const params: CoreCourseGetRecentCoursesWSParams = { + userid: userId, + offset: options.offset || 0, + limit: options.limit || CoreCoursesProvider.RECENT_PER_PAGE, + sort: options.sort, + }; + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getRecentCoursesCacheKey(userId), + }; + + return await site.read('core_course_get_recent_courses', params, preSets); + } + /** * Get the common part of the cache keys for user administration options WS calls. * @@ -995,6 +1030,19 @@ export class CoreCoursesProvider { return site.invalidateWsCacheForKey(this.getCoursesByFieldCacheKey(field, value)); } + /** + * Invalidates get recent courses WS call. + * + * @param userId User ID. If not defined, current user. + * @param siteId Site Id. If not defined, use current site. + * @return Promise resolved when the data is invalidated. + */ + async invalidateRecentCourses(userId?: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + await site.invalidateWsCacheForKey(this.getRecentCoursesCacheKey(userId || site.getUserId())); + } + /** * Invalidates all user administration options. * @@ -1428,13 +1476,15 @@ type CoreCourseGetCoursesWSParams = { export type CoreCourseGetCoursesWSResponse = CoreCourseGetCoursesData[]; /** - * Course type exported in CoreCourseGetEnrolledCoursesByTimelineClassificationWSResponse; + * Course data exported by course_summary_exporter; */ -export type CoreCourseGetEnrolledCoursesByTimelineClassification = CoreCourseBasicData & { // Course. +export type CoreCourseSummaryData = CoreCourseBasicData & { // Course. idnumber: string; // Idnumber. startdate: number; // Startdate. enddate: number; // Enddate. visible: boolean; // Visible. + showactivitydates: boolean; // Showactivitydates. + showcompletionconditions: boolean; // Showcompletionconditions. fullnamedisplay: string; // Fullnamedisplay. viewurl: string; // Viewurl. courseimage: string; // Courseimage. @@ -1463,7 +1513,7 @@ type CoreCourseGetEnrolledCoursesByTimelineClassificationWSParams = { * Data returned by core_course_get_enrolled_courses_by_timeline_classification WS. */ export type CoreCourseGetEnrolledCoursesByTimelineClassificationWSResponse = { - courses: CoreCourseGetEnrolledCoursesByTimelineClassification[]; + courses: CoreCourseSummaryData[]; nextoffset: number; // Offset for the next request. }; @@ -1588,6 +1638,26 @@ export type EnrolGuestGetInstanceInfoWSResponse = { warnings?: CoreWSExternalWarning[]; }; +/** + * Params of core_course_get_recent_courses WS. + */ +export type CoreCourseGetRecentCoursesWSParams = { + userid?: number; // Id of the user, default to current user. + limit?: number; // Result set limit. + offset?: number; // Result set offset. + sort?: string; // Sort string. +}; + +/** + * Options for getRecentCourses. + */ +export type CoreCourseGetRecentCoursesOptions = CoreSitesCommonWSOptions & { + userId?: number; // Id of the user, default to current user. + limit?: number; // Result set limit. + offset?: number; // Result set offset. + sort?: string; // Sort string. +}; + /** * Course guest enrolment method. */