From 85ac2b0bb54ecff1bdffc82129717a7754c325a1 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Mon, 2 Aug 2021 15:58:33 +0200 Subject: [PATCH] MOBILE-3799 h5p: Let teachers view users attempts in activity --- scripts/langindex.json | 4 + .../index/addon-mod-h5pactivity-index.html | 4 + .../mod/h5pactivity/components/index/index.ts | 20 +- .../h5pactivity/h5pactivity-lazy.module.ts | 5 + src/addons/mod/h5pactivity/lang.json | 6 +- .../pages/user-attempts/user-attempts.html | 2 +- .../pages/users-attempts/users-attempts.html | 70 +++++ .../users-attempts/users-attempts.module.ts | 38 +++ .../pages/users-attempts/users-attempts.ts | 202 ++++++++++++++ .../mod/h5pactivity/services/h5pactivity.ts | 255 ++++++++++++++++-- .../h5pactivity/services/handlers/prefetch.ts | 23 +- 11 files changed, 602 insertions(+), 27 deletions(-) create mode 100644 src/addons/mod/h5pactivity/pages/users-attempts/users-attempts.html create mode 100644 src/addons/mod/h5pactivity/pages/users-attempts/users-attempts.module.ts create mode 100644 src/addons/mod/h5pactivity/pages/users-attempts/users-attempts.ts diff --git a/scripts/langindex.json b/scripts/langindex.json index 57ee82499..df750b6bf 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -681,6 +681,7 @@ "addon.mod_h5pactivity.attempt_success_fail": "h5pactivity", "addon.mod_h5pactivity.attempt_success_pass": "h5pactivity", "addon.mod_h5pactivity.attempt_success_unknown": "h5pactivity", + "addon.mod_h5pactivity.attempts": "h5pactivity", "addon.mod_h5pactivity.attempts_none": "h5pactivity", "addon.mod_h5pactivity.completion": "h5pactivity", "addon.mod_h5pactivity.downloadh5pfile": "local_moodlemobileapp", @@ -692,12 +693,15 @@ "addon.mod_h5pactivity.modulenameplural": "h5pactivity", "addon.mod_h5pactivity.myattempts": "h5pactivity", "addon.mod_h5pactivity.no_compatible_track": "h5pactivity", + "addon.mod_h5pactivity.noparticipants": "h5pactivity", "addon.mod_h5pactivity.offlinedisabledwarning": "local_moodlemobileapp", "addon.mod_h5pactivity.outcome": "h5pactivity", "addon.mod_h5pactivity.previewmode": "h5pactivity", "addon.mod_h5pactivity.result_fill-in": "h5pactivity", "addon.mod_h5pactivity.result_other": "h5pactivity", + "addon.mod_h5pactivity.review_attempts": "local_moodlemobileapp", "addon.mod_h5pactivity.review_my_attempts": "h5pactivity", + "addon.mod_h5pactivity.review_user_attempts": "h5pactivity", "addon.mod_h5pactivity.score": "h5pactivity", "addon.mod_h5pactivity.score_out_of": "h5pactivity", "addon.mod_h5pactivity.startdate": "h5pactivity", diff --git a/src/addons/mod/h5pactivity/components/index/addon-mod-h5pactivity-index.html b/src/addons/mod/h5pactivity/components/index/addon-mod-h5pactivity-index.html index a3a164dfb..f88b40b88 100644 --- a/src/addons/mod/h5pactivity/components/index/addon-mod-h5pactivity-index.html +++ b/src/addons/mod/h5pactivity/components/index/addon-mod-h5pactivity-index.html @@ -5,6 +5,10 @@ [priority]="1000" [content]="'addon.mod_h5pactivity.review_my_attempts' | translate" (action)="viewMyAttempts()" iconAction="stats-chart"> + + diff --git a/src/addons/mod/h5pactivity/components/index/index.ts b/src/addons/mod/h5pactivity/components/index/index.ts index e2f69620e..37989271f 100644 --- a/src/addons/mod/h5pactivity/components/index/index.ts +++ b/src/addons/mod/h5pactivity/components/index/index.ts @@ -79,6 +79,7 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv trackComponent?: string; // Component for tracking. hasOffline = false; isOpeningPage = false; + canViewAllAttempts = false; protected listeningResize = false; protected fetchContentDefaultError = 'addon.mod_h5pactivity.errorgetactivity'; @@ -137,6 +138,8 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv ]); this.trackComponent = this.accessInfo?.cansubmit ? AddonModH5PActivityProvider.TRACK_COMPONENT : ''; + this.canViewAllAttempts = !!this.h5pActivity.enabletracking && !!this.accessInfo?.canreviewattempts && + AddonModH5PActivity.canGetUsersAttemptsInSite(); if (this.h5pActivity.package && this.h5pActivity.package[0]) { // The online player should use the original file, not the trusted one. @@ -377,7 +380,7 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv } /** - * Go to view user events. + * Go to view user attempts. */ async viewMyAttempts(): Promise { this.isOpeningPage = true; @@ -392,6 +395,21 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv } } + /** + * Go to view all user attempts. + */ + async viewAllAttempts(): Promise { + this.isOpeningPage = true; + + try { + await CoreNavigator.navigateToSitePath( + `${AddonModH5PActivityModuleHandlerService.PAGE_NAME}/${this.courseId}/${this.module.id}/users`, + ); + } finally { + this.isOpeningPage = false; + } + } + /** * Treat an iframe message event. * diff --git a/src/addons/mod/h5pactivity/h5pactivity-lazy.module.ts b/src/addons/mod/h5pactivity/h5pactivity-lazy.module.ts index 477958871..3ac3e9562 100644 --- a/src/addons/mod/h5pactivity/h5pactivity-lazy.module.ts +++ b/src/addons/mod/h5pactivity/h5pactivity-lazy.module.ts @@ -36,6 +36,11 @@ const routes: Routes = [ loadChildren: () => import('./pages/attempt-results/attempt-results.module') .then( m => m.AddonModH5PActivityAttemptResultsPageModule), }, + { + path: ':courseId/:cmId/users', + loadChildren: () => import('./pages/users-attempts/users-attempts.module') + .then( m => m.AddonModH5PActivityUsersAttemptsPageModule), + }, ]; @NgModule({ diff --git a/src/addons/mod/h5pactivity/lang.json b/src/addons/mod/h5pactivity/lang.json index 3ac109635..55044c8dd 100644 --- a/src/addons/mod/h5pactivity/lang.json +++ b/src/addons/mod/h5pactivity/lang.json @@ -11,6 +11,7 @@ "attempt_success_fail": "Fail", "attempt_success_pass": "Pass", "attempt_success_unknown": "Not reported", + "attempts": "Attempts", "attempts_none": "This user has no attempts to display.", "completion": "Completion", "downloadh5pfile": "Download H5P file", @@ -22,15 +23,18 @@ "modulenameplural": "H5P", "myattempts": "My attempts", "no_compatible_track": "This interaction ({{$a}}) does not provide tracking information or the tracking\n provided is not compatible with the current activity version.", + "noparticipants": "No participants to display", "offlinedisabledwarning": "You need to be online to view the H5P package.", "outcome": "Outcome", "previewmode": "This content is displayed in preview mode. No attempt tracking will be stored.", "result_fill-in": "Fill-in text", "result_other": "Unknown interaction type", + "review_attempts": "View all attempts", "review_my_attempts": "View my attempts", + "review_user_attempts": "View user attempts ({{$a}})", "score": "Score", "score_out_of": "{{$a.rawscore}} out of {{$a.maxscore}}", "startdate": "Start date", "totalscore": "Total score", "viewattempt": "View attempt {{$a}}" -} \ No newline at end of file +} diff --git a/src/addons/mod/h5pactivity/pages/user-attempts/user-attempts.html b/src/addons/mod/h5pactivity/pages/user-attempts/user-attempts.html index 01bcfe080..31ca8fcd6 100644 --- a/src/addons/mod/h5pactivity/pages/user-attempts/user-attempts.html +++ b/src/addons/mod/h5pactivity/pages/user-attempts/user-attempts.html @@ -17,7 +17,7 @@ + [attr.aria-label]="user.fullname" button detail="true">

{{ user.fullname }}

diff --git a/src/addons/mod/h5pactivity/pages/users-attempts/users-attempts.html b/src/addons/mod/h5pactivity/pages/users-attempts/users-attempts.html new file mode 100644 index 000000000..15a9557e6 --- /dev/null +++ b/src/addons/mod/h5pactivity/pages/users-attempts/users-attempts.html @@ -0,0 +1,70 @@ + + + + + +

+ + +

+
+
+ + + + + + + + + + + {{ 'core.user' | translate }} + {{ 'core.date' | translate }} + {{ 'addon.mod_h5pactivity.score' | translate }} + {{ 'addon.mod_h5pactivity.attempts' | translate }} + + + + + + + + + + +

+ +

+ {{ user.user.fullname }} +
+ + + {{ user.attempts[user.attempts.length - 1].timemodified | coreFormatDate:'strftimedatetimeshort' }} + + + + + {{ 'core.percentagenumber' | translate: {$a: user.score} }} + + + + {{user.attempts.length}} + +
+
+
+
+ + + + + + + +
+
diff --git a/src/addons/mod/h5pactivity/pages/users-attempts/users-attempts.module.ts b/src/addons/mod/h5pactivity/pages/users-attempts/users-attempts.module.ts new file mode 100644 index 000000000..c09f58954 --- /dev/null +++ b/src/addons/mod/h5pactivity/pages/users-attempts/users-attempts.module.ts @@ -0,0 +1,38 @@ +// (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 { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +import { CoreSharedModule } from '@/core/shared.module'; +import { AddonModH5PActivityUsersAttemptsPage } from './users-attempts'; + +const routes: Routes = [ + { + path: '', + component: AddonModH5PActivityUsersAttemptsPage, + }, +]; + +@NgModule({ + imports: [ + RouterModule.forChild(routes), + CoreSharedModule, + ], + declarations: [ + AddonModH5PActivityUsersAttemptsPage, + ], + exports: [RouterModule], +}) +export class AddonModH5PActivityUsersAttemptsPageModule {} diff --git a/src/addons/mod/h5pactivity/pages/users-attempts/users-attempts.ts b/src/addons/mod/h5pactivity/pages/users-attempts/users-attempts.ts new file mode 100644 index 000000000..7dee400fe --- /dev/null +++ b/src/addons/mod/h5pactivity/pages/users-attempts/users-attempts.ts @@ -0,0 +1,202 @@ +// (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 { Component, OnInit } from '@angular/core'; +import { CoreUser, CoreUserProfile } from '@features/user/services/user'; +import { IonRefresher } from '@ionic/angular'; + +import { CoreNavigator } from '@services/navigator'; +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreUtils } from '@services/utils/utils'; +import { + AddonModH5PActivity, + AddonModH5PActivityData, + AddonModH5PActivityProvider, + AddonModH5PActivityUserAttempts, +} from '../../services/h5pactivity'; + +/** + * Page that displays all users that can attempt an H5P activity. + */ +@Component({ + selector: 'page-addon-mod-h5pactivity-users-attempts', + templateUrl: 'users-attempts.html', +}) +export class AddonModH5PActivityUsersAttemptsPage implements OnInit { + + loaded = false; + courseId!: number; + cmId!: number; + h5pActivity?: AddonModH5PActivityData; + users: AddonModH5PActivityUserAttemptsFormatted[] = []; + fetchMoreUsersFailed = false; + canLoadMore = false; + + protected page = 0; + + /** + * @inheritdoc + */ + async ngOnInit(): Promise { + this.courseId = CoreNavigator.getRouteNumberParam('courseId')!; + this.cmId = CoreNavigator.getRouteNumberParam('cmId')!; + + try { + await this.fetchData(); + + await AddonModH5PActivity.logViewReport(this.h5pActivity!.id, this.h5pActivity!.name); + } catch (error) { + CoreDomUtils.showErrorModalDefault(error, 'Error loading attempts.'); + } finally { + this.loaded = true; + } + } + + /** + * Refresh the data. + * + * @param refresher Refresher. + */ + doRefresh(refresher: IonRefresher): void { + this.refreshData().finally(() => { + refresher.complete(); + }); + } + + /** + * Get quiz data and attempt data. + * + * @param refresh Whether user is refreshing data. + * @return Promise resolved when done. + */ + protected async fetchData(refresh?: boolean): Promise { + this.h5pActivity = await AddonModH5PActivity.getH5PActivity(this.courseId, this.cmId); + + await Promise.all([ + this.fetchUsers(refresh), + ]); + } + + /** + * Get users. + * + * @param refresh Whether user is refreshing data. + * @return Promise resolved when done. + */ + protected async fetchUsers(refresh?: boolean): Promise { + if (refresh) { + this.page = 0; + } + + const result = await AddonModH5PActivity.getUsersAttempts(this.h5pActivity!.id, { + cmId: this.cmId, + page: this.page, + }); + + const formattedUsers = await this.formatUsers(result.users); + + if (this.page === 0) { + this.users = formattedUsers; + } else { + this.users = this.users.concat(formattedUsers); + } + + this.canLoadMore = result.canLoadMore; + this.page++; + } + + /** + * Format users data. + * + * @param users Users to format. + * @return Formatted users. + */ + protected async formatUsers(users: AddonModH5PActivityUserAttempts[]): Promise { + return await Promise.all(users.map(async (user: AddonModH5PActivityUserAttemptsFormatted) => { + user.user = await CoreUser.getProfile(user.userid, this.courseId, true); + + // Calculate the score of the user. + if (this.h5pActivity!.grademethod === AddonModH5PActivityProvider.GRADEMANUAL) { + // No score. + } else if (this.h5pActivity!.grademethod === AddonModH5PActivityProvider.GRADEAVERAGEATTEMPT) { + if (user.attempts.length) { + // Calculate the average. + const sumScores = user.attempts.reduce((sumScores, attempt) => + sumScores + attempt.rawscore * 100 / attempt.maxscore, 0); + + user.score = Math.round(sumScores / user.attempts.length); + } + } else if (user.scored?.attempts[0]) { + // Only a single attempt used to calculate the grade. Use it. + user.score = Math.round(user.scored.attempts[0].rawscore * 100 / user.scored.attempts[0].maxscore); + } + + return user; + })); + } + + /** + * Load a new batch of users. + * + * @param complete Completion callback. + */ + async fetchMoreUsers(complete: () => void): Promise { + try { + await this.fetchUsers(false); + } catch (error) { + CoreDomUtils.showErrorModalDefault(error, 'Error loading more users'); + + this.fetchMoreUsersFailed = true; + } + + complete(); + } + + /** + * Refresh the data. + * + * @return Promise resolved when done. + */ + protected async refreshData(): Promise { + const promises = [ + AddonModH5PActivity.invalidateActivityData(this.courseId), + ]; + + if (this.h5pActivity) { + promises.push(AddonModH5PActivity.invalidateAllUsersAttempts(this.h5pActivity.id)); + } + + await CoreUtils.ignoreErrors(Promise.all(promises)); + + await this.fetchData(true); + } + + /** + * Open the page to view a user attempts. + * + * @param user User to open. + */ + openUser(user: AddonModH5PActivityUserAttemptsFormatted): void { + CoreNavigator.navigate(`../userattempts/${user.userid}`); + } + +} + +/** + * User attempts data with some calculated data. + */ +type AddonModH5PActivityUserAttemptsFormatted = AddonModH5PActivityUserAttempts & { + user?: CoreUserProfile; + score?: number; +}; diff --git a/src/addons/mod/h5pactivity/services/h5pactivity.ts b/src/addons/mod/h5pactivity/services/h5pactivity.ts index 28927c099..7f16095fc 100644 --- a/src/addons/mod/h5pactivity/services/h5pactivity.ts +++ b/src/addons/mod/h5pactivity/services/h5pactivity.ts @@ -38,6 +38,40 @@ export class AddonModH5PActivityProvider { static readonly COMPONENT = 'mmaModH5PActivity'; static readonly TRACK_COMPONENT = 'mod_h5pactivity'; // Component for tracking. + static readonly USERS_PER_PAGE = 20; + + // Grade type constants. + static readonly GRADEMANUAL = 0; // No automathic grading using attempt results. + static readonly GRADEHIGHESTATTEMPT = 1; // Use highest attempt results for grading. + static readonly GRADEAVERAGEATTEMPT = 2; // Use average attempt results for grading. + static readonly GRADELASTATTEMPT = 3; // Use last attempt results for grading. + static readonly GRADEFIRSTATTEMPT = 4; // Use first attempt results for grading. + + /** + * Check if a certain site allows viewing list of users and their attempts. + * + * @param site Site ID. If not defined, use current site. + * @return Whether can view users. + * @since 3.11 + */ + async canGetUsersAttempts(siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + return this.canGetUsersAttemptsInSite(site); + } + + /** + * Check if a certain site allows viewing list of users and their attempts. + * + * @param site Site. If not defined, use current site. + * @return Whether can view users. + * @since 3.11 + */ + canGetUsersAttemptsInSite(site?: CoreSite): boolean { + site = site || CoreSites.getCurrentSite(); + + return !!site?.wsAvailable('mod_h5pactivity_get_user_attempts'); + } /** * Format an attempt's data. @@ -168,6 +202,122 @@ export class AddonModH5PActivityProvider { } } + /** + * Get all pages of users attempts. + * + * @param id Activity ID. + * @param options Other options. + * @return Promise resolved with the list of user. + */ + async getAllUsersAttempts( + id: number, + options?: AddonModH5PActivityGetAllUsersAttemptsOptions, + ): Promise { + + const optionsWithPage: AddonModH5PActivityGetAllUsersAttemptsOptions = { + ...options, + page: 0, + }; + let canLoadMore = true; + let users: AddonModH5PActivityUserAttempts[] = []; + + while (canLoadMore) { + try { + const result = await this.getUsersAttempts(id, optionsWithPage); + + optionsWithPage.page = optionsWithPage.page! + 1; + users = users.concat(result.users); + canLoadMore = result.canLoadMore; + } catch (error) { + if (optionsWithPage.dontFailOnError) { + return users; + } + + throw error; + } + } + + return users; + } + + /** + * Get list of users and their attempts. + * + * @param id H5P Activity ID. + * @param options Options. + * @return Promise resolved with list of users and whether can load more attempts. + * @since 3.11 + */ + async getUsersAttempts( + id: number, + options?: AddonModH5PActivityGetUsersAttemptsOptions, + ): Promise<{users: AddonModH5PActivityUserAttempts[]; canLoadMore: boolean}> { + options = options || {}; + options.page = options.page || 0; + options.perPage = options.perPage ?? AddonModH5PActivityProvider.USERS_PER_PAGE; + + const site = await CoreSites.getSite(options.siteId); + + const params: AddonModH5pactivityGetUserAttemptsWSParams = { + h5pactivityid: id, + page: options.page, + perpage: options.perPage === 0 ? 0 : options.perPage + 1, // Get 1 more to be able to know if there are more to load. + sortorder: options.sortOrder, + firstinitial: options.firstInitial, + lastinitial: options.lastInitial, + }; + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getUsersAttemptsCacheKey(id, options), + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + component: AddonModH5PActivityProvider.COMPONENT, + componentId: options.cmId, + ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; + + const response = await site.read( + 'mod_h5pactivity_get_user_attempts', + params, + preSets, + ); + + if (response.warnings?.[0]) { + throw new CoreWSError(response.warnings[0]); + } + + let canLoadMore = false; + if (options.perPage > 0) { + canLoadMore = response.usersattempts.length > options.perPage; + response.usersattempts = response.usersattempts.slice(0, options.perPage); + } + + return { + canLoadMore: canLoadMore, + users: response.usersattempts.map(userAttempts => this.formatUserAttempts(userAttempts)), + }; + } + + /** + * Get cache key for get users attempts WS calls. + * + * @param id Instance ID. + * @param attemptsIds Attempts IDs. + * @return Cache key. + */ + protected getUsersAttemptsCacheKey(id: number, options: AddonModH5PActivityGetUsersAttemptsOptions): string { + return this.getUsersAttemptsCommonCacheKey(id) + `:${options.page}:${options.perPage}` + + `:${options.sortOrder || ''}:${options.firstInitial || ''}:${options.lastInitial || ''}`; + } + + /** + * Get common cache key for get users attempts WS calls. + * + * @param id Instance ID. + * @return Cache key. + */ + protected getUsersAttemptsCommonCacheKey(id: number): string { + return ROOT_CACHE_KEY + 'userAttempts:' + id; + } + /** * Get cache key for results WS calls. * @@ -455,26 +605,56 @@ export class AddonModH5PActivityProvider { ): Promise { const site = await CoreSites.getSite(options.siteId); + const userId = options.userId || site.getUserId(); - const params: AddonModH5pactivityGetAttemptsWSParams = { - h5pactivityid: id, - userids: [options.userId || site.getUserId()], - }; - const preSets: CoreSiteWSPreSets = { - cacheKey: this.getUserAttemptsCacheKey(id, params.userids!), - updateFrequency: CoreSite.FREQUENCY_SOMETIMES, - component: AddonModH5PActivityProvider.COMPONENT, - componentId: options.cmId, - ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. - }; + try { + const params: AddonModH5pactivityGetAttemptsWSParams = { + h5pactivityid: id, + userids: [userId], + }; + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getUserAttemptsCacheKey(id, params.userids!), + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + component: AddonModH5PActivityProvider.COMPONENT, + componentId: options.cmId, + ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; - const response = await site.read('mod_h5pactivity_get_attempts', params, preSets); + const response = await site.read( + 'mod_h5pactivity_get_attempts', + params, + preSets, + ); - if (response.warnings?.[0]) { - throw new CoreWSError(response.warnings[0]); // Cannot view user attempts. + if (response.warnings?.[0]) { + throw new CoreWSError(response.warnings[0]); // Cannot view user attempts. + } + + return this.formatUserAttempts(response.usersattempts[0]); + } catch (error) { + if (CoreUtils.isWebServiceError(error)) { + throw error; + } + + try { + // Check if the full list of users is cached. If so, get the user attempts from there. + const users = await this.getAllUsersAttempts(id, { + ...options, + readingStrategy: CoreSitesReadingStrategy.ONLY_CACHE, + dontFailOnError: true, + }); + + const user = users.find(user => user.userid === userId); + if (!user) { + throw error; + } + + return this.formatUserAttempts(user); + } catch { + throw error; + } } - return this.formatUserAttempts(response.usersattempts[0]); } /** @@ -532,16 +712,16 @@ export class AddonModH5PActivityProvider { } /** - * Invalidates all users attempts for H5P activity. + * Invalidates list of users for H5P activity. * * @param id Activity ID. * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the data is invalidated. */ - async invalidateAllUserAttempts(id: number, siteId?: string): Promise { + async invalidateAllUsersAttempts(id: number, siteId?: string): Promise { const site = await CoreSites.getSite(siteId); - await site.invalidateWsCacheForKey(this.getUserAttemptsCommonCacheKey(id)); + await site.invalidateWsCacheForKeyStartingWith(this.getUsersAttemptsCommonCacheKey(id)); } /** @@ -896,6 +1076,45 @@ export type AddonModH5PActivityViewReportOptions = { siteId?: string; // Site ID. If not defined, current site. }; +/** + * Params of mod_h5pactivity_get_user_attempts WS. + */ +export type AddonModH5pactivityGetUserAttemptsWSParams = { + h5pactivityid: number; // H5p activity instance id. + sortorder?: string; // Sort by this element: id, firstname. + page?: number; // Current page. + perpage?: number; // Items per page. + firstinitial?: string; // Users whose first name starts with firstinitial. + lastinitial?: string; // Users whose last name starts with lastinitial. +}; + +/** + * Data returned by mod_h5pactivity_get_user_attempts WS. + */ +export type AddonModH5pactivityGetUserAttemptsWSResponse = { + activityid: number; // Activity course module ID. + usersattempts: AddonModH5PActivityWSUserAttempts[]; // The complete users attempts list. + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Options for getUsersAttempts. + */ +export type AddonModH5PActivityGetUsersAttemptsOptions = CoreCourseCommonModWSOptions & { + sortOrder?: string; // Sort by this element: id, firstname. + page?: number; // Current page. Defaults to 0. + perPage?: number; // Items per page. Defaults to USERS_PER_PAGE. + firstInitial?: string; // Users whose first name starts with firstInitial. + lastInitial?: string; // Users whose last name starts with lastInitial. +}; + +/** + * Options for getAllUsersAttempts. + */ +export type AddonModH5PActivityGetAllUsersAttemptsOptions = AddonModH5PActivityGetUsersAttemptsOptions & { + dontFailOnError?: boolean; // If true the function will return the users it's able to retrieve, until a call fails. +}; + declare module '@singletons/events' { /** diff --git a/src/addons/mod/h5pactivity/services/handlers/prefetch.ts b/src/addons/mod/h5pactivity/services/handlers/prefetch.ts index df1f199ef..14ecc15cb 100644 --- a/src/addons/mod/h5pactivity/services/handlers/prefetch.ts +++ b/src/addons/mod/h5pactivity/services/handlers/prefetch.ts @@ -147,20 +147,31 @@ export class AddonModH5PActivityPrefetchHandlerService extends CoreCourseActivit siteId, }); + const options = { + cmId: h5pActivity.coursemodule, + readingStrategy: CoreSitesReadingStrategy.ONLY_NETWORK, + siteId: siteId, + }; + if (!accessInfo.canreviewattempts) { // Not a teacher, prefetch user attempts and the current user profile. const site = await CoreSites.getSite(siteId); - const options = { - cmId: h5pActivity.coursemodule, - readingStrategy: CoreSitesReadingStrategy.ONLY_NETWORK, - siteId: siteId, - }; - await Promise.all([ AddonModH5PActivity.getAllAttemptsResults(h5pActivity.id, options), CoreUser.prefetchProfiles([site.getUserId()], h5pActivity.course, siteId), ]); + } else { + // It's a teacher, get all attempts if possible. + const canGetUsers = await AddonModH5PActivity.canGetUsersAttempts(siteId); + if (!canGetUsers) { + return; + } + + const users = await AddonModH5PActivity.getAllUsersAttempts(h5pActivity.id, options); + + const userIds = users.map(user => user.userid); + await CoreUser.prefetchProfiles(userIds, h5pActivity.course, siteId); } }