Merge pull request #2907 from dpalou/MOBILE-3799
MOBILE-3799 h5p: Let teachers view users attempts in activity
This commit is contained in:
		
						commit
						0872130a8d
					
				| @ -681,6 +681,7 @@ | |||||||
|   "addon.mod_h5pactivity.attempt_success_fail": "h5pactivity", |   "addon.mod_h5pactivity.attempt_success_fail": "h5pactivity", | ||||||
|   "addon.mod_h5pactivity.attempt_success_pass": "h5pactivity", |   "addon.mod_h5pactivity.attempt_success_pass": "h5pactivity", | ||||||
|   "addon.mod_h5pactivity.attempt_success_unknown": "h5pactivity", |   "addon.mod_h5pactivity.attempt_success_unknown": "h5pactivity", | ||||||
|  |   "addon.mod_h5pactivity.attempts": "h5pactivity", | ||||||
|   "addon.mod_h5pactivity.attempts_none": "h5pactivity", |   "addon.mod_h5pactivity.attempts_none": "h5pactivity", | ||||||
|   "addon.mod_h5pactivity.completion": "h5pactivity", |   "addon.mod_h5pactivity.completion": "h5pactivity", | ||||||
|   "addon.mod_h5pactivity.downloadh5pfile": "local_moodlemobileapp", |   "addon.mod_h5pactivity.downloadh5pfile": "local_moodlemobileapp", | ||||||
| @ -692,12 +693,15 @@ | |||||||
|   "addon.mod_h5pactivity.modulenameplural": "h5pactivity", |   "addon.mod_h5pactivity.modulenameplural": "h5pactivity", | ||||||
|   "addon.mod_h5pactivity.myattempts": "h5pactivity", |   "addon.mod_h5pactivity.myattempts": "h5pactivity", | ||||||
|   "addon.mod_h5pactivity.no_compatible_track": "h5pactivity", |   "addon.mod_h5pactivity.no_compatible_track": "h5pactivity", | ||||||
|  |   "addon.mod_h5pactivity.noparticipants": "h5pactivity", | ||||||
|   "addon.mod_h5pactivity.offlinedisabledwarning": "local_moodlemobileapp", |   "addon.mod_h5pactivity.offlinedisabledwarning": "local_moodlemobileapp", | ||||||
|   "addon.mod_h5pactivity.outcome": "h5pactivity", |   "addon.mod_h5pactivity.outcome": "h5pactivity", | ||||||
|   "addon.mod_h5pactivity.previewmode": "h5pactivity", |   "addon.mod_h5pactivity.previewmode": "h5pactivity", | ||||||
|   "addon.mod_h5pactivity.result_fill-in": "h5pactivity", |   "addon.mod_h5pactivity.result_fill-in": "h5pactivity", | ||||||
|   "addon.mod_h5pactivity.result_other": "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_my_attempts": "h5pactivity", | ||||||
|  |   "addon.mod_h5pactivity.review_user_attempts": "h5pactivity", | ||||||
|   "addon.mod_h5pactivity.score": "h5pactivity", |   "addon.mod_h5pactivity.score": "h5pactivity", | ||||||
|   "addon.mod_h5pactivity.score_out_of": "h5pactivity", |   "addon.mod_h5pactivity.score_out_of": "h5pactivity", | ||||||
|   "addon.mod_h5pactivity.startdate": "h5pactivity", |   "addon.mod_h5pactivity.startdate": "h5pactivity", | ||||||
|  | |||||||
| @ -5,6 +5,10 @@ | |||||||
|             [priority]="1000" [content]="'addon.mod_h5pactivity.review_my_attempts' | translate" (action)="viewMyAttempts()" |             [priority]="1000" [content]="'addon.mod_h5pactivity.review_my_attempts' | translate" (action)="viewMyAttempts()" | ||||||
|             iconAction="stats-chart"> |             iconAction="stats-chart"> | ||||||
|         </core-context-menu-item> |         </core-context-menu-item> | ||||||
|  |         <core-context-menu-item *ngIf="canViewAllAttempts" | ||||||
|  |             [priority]="1000" [content]="'addon.mod_h5pactivity.review_attempts' | translate" (action)="viewAllAttempts()" | ||||||
|  |             iconAction="stats-chart"> | ||||||
|  |         </core-context-menu-item> | ||||||
|         <core-context-menu-item *ngIf="externalUrl" [priority]="900" [content]="'core.openinbrowser' | translate" |         <core-context-menu-item *ngIf="externalUrl" [priority]="900" [content]="'core.openinbrowser' | translate" | ||||||
|             [href]="externalUrl" iconAction="fas-external-link-alt"> |             [href]="externalUrl" iconAction="fas-external-link-alt"> | ||||||
|         </core-context-menu-item> |         </core-context-menu-item> | ||||||
|  | |||||||
| @ -79,6 +79,7 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv | |||||||
|     trackComponent?: string; // Component for tracking.
 |     trackComponent?: string; // Component for tracking.
 | ||||||
|     hasOffline = false; |     hasOffline = false; | ||||||
|     isOpeningPage = false; |     isOpeningPage = false; | ||||||
|  |     canViewAllAttempts = false; | ||||||
| 
 | 
 | ||||||
|     protected listeningResize = false; |     protected listeningResize = false; | ||||||
|     protected fetchContentDefaultError = 'addon.mod_h5pactivity.errorgetactivity'; |     protected fetchContentDefaultError = 'addon.mod_h5pactivity.errorgetactivity'; | ||||||
| @ -137,6 +138,8 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv | |||||||
|             ]); |             ]); | ||||||
| 
 | 
 | ||||||
|             this.trackComponent = this.accessInfo?.cansubmit ? AddonModH5PActivityProvider.TRACK_COMPONENT : ''; |             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]) { |             if (this.h5pActivity.package && this.h5pActivity.package[0]) { | ||||||
|                 // The online player should use the original file, not the trusted one.
 |                 // 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<void> { |     async viewMyAttempts(): Promise<void> { | ||||||
|         this.isOpeningPage = true; |         this.isOpeningPage = true; | ||||||
| @ -392,6 +395,21 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * Go to view all user attempts. | ||||||
|  |      */ | ||||||
|  |     async viewAllAttempts(): Promise<void> { | ||||||
|  |         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. |      * Treat an iframe message event. | ||||||
|      * |      * | ||||||
|  | |||||||
| @ -36,6 +36,11 @@ const routes: Routes = [ | |||||||
|         loadChildren: () => import('./pages/attempt-results/attempt-results.module') |         loadChildren: () => import('./pages/attempt-results/attempt-results.module') | ||||||
|             .then( m => m.AddonModH5PActivityAttemptResultsPageModule), |             .then( m => m.AddonModH5PActivityAttemptResultsPageModule), | ||||||
|     }, |     }, | ||||||
|  |     { | ||||||
|  |         path: ':courseId/:cmId/users', | ||||||
|  |         loadChildren: () => import('./pages/users-attempts/users-attempts.module') | ||||||
|  |             .then( m => m.AddonModH5PActivityUsersAttemptsPageModule), | ||||||
|  |     }, | ||||||
| ]; | ]; | ||||||
| 
 | 
 | ||||||
| @NgModule({ | @NgModule({ | ||||||
|  | |||||||
| @ -11,6 +11,7 @@ | |||||||
|     "attempt_success_fail": "Fail", |     "attempt_success_fail": "Fail", | ||||||
|     "attempt_success_pass": "Pass", |     "attempt_success_pass": "Pass", | ||||||
|     "attempt_success_unknown": "Not reported", |     "attempt_success_unknown": "Not reported", | ||||||
|  |     "attempts": "Attempts", | ||||||
|     "attempts_none": "This user has no attempts to display.", |     "attempts_none": "This user has no attempts to display.", | ||||||
|     "completion": "Completion", |     "completion": "Completion", | ||||||
|     "downloadh5pfile": "Download H5P file", |     "downloadh5pfile": "Download H5P file", | ||||||
| @ -22,15 +23,18 @@ | |||||||
|     "modulenameplural": "H5P", |     "modulenameplural": "H5P", | ||||||
|     "myattempts": "My attempts", |     "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.", |     "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.", |     "offlinedisabledwarning": "You need to be online to view the H5P package.", | ||||||
|     "outcome": "Outcome", |     "outcome": "Outcome", | ||||||
|     "previewmode": "This content is displayed in preview mode. No attempt tracking will be stored.", |     "previewmode": "This content is displayed in preview mode. No attempt tracking will be stored.", | ||||||
|     "result_fill-in": "Fill-in text", |     "result_fill-in": "Fill-in text", | ||||||
|     "result_other": "Unknown interaction type", |     "result_other": "Unknown interaction type", | ||||||
|  |     "review_attempts": "View all attempts", | ||||||
|     "review_my_attempts": "View my attempts", |     "review_my_attempts": "View my attempts", | ||||||
|  |     "review_user_attempts": "View user attempts ({{$a}})", | ||||||
|     "score": "Score", |     "score": "Score", | ||||||
|     "score_out_of": "{{$a.rawscore}} out of {{$a.maxscore}}", |     "score_out_of": "{{$a.rawscore}} out of {{$a.maxscore}}", | ||||||
|     "startdate": "Start date", |     "startdate": "Start date", | ||||||
|     "totalscore": "Total score", |     "totalscore": "Total score", | ||||||
|     "viewattempt": "View attempt {{$a}}" |     "viewattempt": "View attempt {{$a}}" | ||||||
| } | } | ||||||
|  | |||||||
| @ -17,7 +17,7 @@ | |||||||
|     <core-loading [hideUntil]="loaded"> |     <core-loading [hideUntil]="loaded"> | ||||||
|         <!-- User viewed. --> |         <!-- User viewed. --> | ||||||
|         <ion-item class="ion-text-wrap" *ngIf="user && !isCurrentUser" core-user-link [userId]="user.id" [courseId]="courseId" |         <ion-item class="ion-text-wrap" *ngIf="user && !isCurrentUser" core-user-link [userId]="user.id" [courseId]="courseId" | ||||||
|             [attr.aria-label]="user.fullname"> |             [attr.aria-label]="user.fullname" button detail="true"> | ||||||
|             <core-user-avatar [user]="user" slot="start" [courseId]="courseId"></core-user-avatar> |             <core-user-avatar [user]="user" slot="start" [courseId]="courseId"></core-user-avatar> | ||||||
|             <ion-label> |             <ion-label> | ||||||
|                 <h2>{{ user.fullname }}</h2> |                 <h2>{{ user.fullname }}</h2> | ||||||
|  | |||||||
| @ -0,0 +1,70 @@ | |||||||
|  | <ion-header> | ||||||
|  |     <ion-toolbar> | ||||||
|  |         <ion-buttons slot="start"> | ||||||
|  |             <ion-back-button [text]="'core.back' | translate"></ion-back-button> | ||||||
|  |         </ion-buttons> | ||||||
|  |         <h1> | ||||||
|  |             <core-format-text *ngIf="h5pActivity" [text]="h5pActivity.name" contextLevel="module" | ||||||
|  |                 [contextInstanceId]="h5pActivity.coursemodule" [courseId]="courseId"> | ||||||
|  |             </core-format-text> | ||||||
|  |         </h1> | ||||||
|  |     </ion-toolbar> | ||||||
|  | </ion-header> | ||||||
|  | <ion-content> | ||||||
|  |     <ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="doRefresh($event.target)"> | ||||||
|  |         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> | ||||||
|  |     </ion-refresher> | ||||||
|  |     <core-loading [hideUntil]="loaded"> | ||||||
|  |         <ion-list *ngIf="users.length"> | ||||||
|  |             <!-- "Header" of the table --> | ||||||
|  |             <ion-item class="addon-mod_h5pactivity-table-header hide-detail font-bold" detail="true"> | ||||||
|  |                 <ion-label> | ||||||
|  |                     <ion-row class="ion-align-items-center"> | ||||||
|  |                         <ion-col class="ion-text-center" size="4">{{ 'core.user' | translate }}</ion-col> | ||||||
|  |                         <ion-col class="ion-text-center" size="4">{{ 'core.date' | translate }}</ion-col> | ||||||
|  |                         <ion-col class="ion-text-center" size="2">{{ 'addon.mod_h5pactivity.score' | translate }}</ion-col> | ||||||
|  |                         <ion-col class="ion-text-center" size="2">{{ 'addon.mod_h5pactivity.attempts' | translate }}</ion-col> | ||||||
|  |                     </ion-row> | ||||||
|  |                 </ion-label> | ||||||
|  |             </ion-item> | ||||||
|  | 
 | ||||||
|  |             <!-- List of users. --> | ||||||
|  |             <ion-item class="ion-text-wrap addon-mod_h5pactivity-table-row" *ngFor="let user of users" detail="true" button | ||||||
|  |                 [attr.aria-label]="'addon.mod_h5pactivity.review_user_attempts' | translate:{$a: user.attempts.length}" | ||||||
|  |                 (click)="openUser(user)"> | ||||||
|  | 
 | ||||||
|  |                 <ion-label> | ||||||
|  |                     <ion-row class="ion-align-items-center"> | ||||||
|  |                         <ion-col class="ion-text-center" size="4"> | ||||||
|  |                             <p> | ||||||
|  |                                 <core-user-avatar [user]="user.user" [courseId]="courseId"></core-user-avatar> | ||||||
|  |                             </p> | ||||||
|  |                             {{ user.user.fullname }} | ||||||
|  |                         </ion-col> | ||||||
|  |                         <ion-col class="ion-text-center" size="4"> | ||||||
|  |                             <span *ngIf="user.attempts.length"> | ||||||
|  |                                 {{ user.attempts[user.attempts.length - 1].timemodified | coreFormatDate:'strftimedatetimeshort' }} | ||||||
|  |                             </span> | ||||||
|  |                         </ion-col> | ||||||
|  |                         <ion-col class="ion-text-center" size="2"> | ||||||
|  |                             <span *ngIf="user.score !== undefined"> | ||||||
|  |                                 {{ 'core.percentagenumber' | translate: {$a: user.score} }} | ||||||
|  |                             </span> | ||||||
|  |                         </ion-col> | ||||||
|  |                         <ion-col class="ion-text-center" size="2"> | ||||||
|  |                             <span *ngIf="user.attempts.length">{{user.attempts.length}}</span> | ||||||
|  |                         </ion-col> | ||||||
|  |                     </ion-row> | ||||||
|  |                 </ion-label> | ||||||
|  |             </ion-item> | ||||||
|  |         </ion-list> | ||||||
|  | 
 | ||||||
|  |         <!-- No attempts. --> | ||||||
|  |         <core-empty-box *ngIf="!users.length && !canLoadMore" icon="fas-chart-bar" | ||||||
|  |             [message]="'addon.mod_h5pactivity.noparticipants' | translate"> | ||||||
|  |         </core-empty-box> | ||||||
|  | 
 | ||||||
|  |         <core-infinite-loading [enabled]="loaded && canLoadMore" [error]="fetchMoreUsersFailed" (action)="fetchMoreUsers($event)"> | ||||||
|  |         </core-infinite-loading> | ||||||
|  |     </core-loading> | ||||||
|  | </ion-content> | ||||||
| @ -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 {} | ||||||
| @ -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<void> { | ||||||
|  |         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<void> { | ||||||
|  |         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<void> { | ||||||
|  |         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<AddonModH5PActivityUserAttemptsFormatted[]> { | ||||||
|  |         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<void> { | ||||||
|  |         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<void> { | ||||||
|  |         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; | ||||||
|  | }; | ||||||
| @ -38,6 +38,40 @@ export class AddonModH5PActivityProvider { | |||||||
| 
 | 
 | ||||||
|     static readonly COMPONENT = 'mmaModH5PActivity'; |     static readonly COMPONENT = 'mmaModH5PActivity'; | ||||||
|     static readonly TRACK_COMPONENT = 'mod_h5pactivity'; // Component for tracking.
 |     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<boolean> { | ||||||
|  |         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. |      * 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<AddonModH5PActivityUserAttempts[]> { | ||||||
|  | 
 | ||||||
|  |         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<AddonModH5pactivityGetUserAttemptsWSResponse>( | ||||||
|  |             '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. |      * Get cache key for results WS calls. | ||||||
|      * |      * | ||||||
| @ -455,26 +605,56 @@ export class AddonModH5PActivityProvider { | |||||||
|     ): Promise<AddonModH5PActivityUserAttempts> { |     ): Promise<AddonModH5PActivityUserAttempts> { | ||||||
| 
 | 
 | ||||||
|         const site = await CoreSites.getSite(options.siteId); |         const site = await CoreSites.getSite(options.siteId); | ||||||
|  |         const userId = options.userId || site.getUserId(); | ||||||
| 
 | 
 | ||||||
|         const params: AddonModH5pactivityGetAttemptsWSParams = { |         try { | ||||||
|             h5pactivityid: id, |             const params: AddonModH5pactivityGetAttemptsWSParams = { | ||||||
|             userids: [options.userId || site.getUserId()], |                 h5pactivityid: id, | ||||||
|         }; |                 userids: [userId], | ||||||
|         const preSets: CoreSiteWSPreSets = { |             }; | ||||||
|             cacheKey: this.getUserAttemptsCacheKey(id, params.userids!), |             const preSets: CoreSiteWSPreSets = { | ||||||
|             updateFrequency: CoreSite.FREQUENCY_SOMETIMES, |                 cacheKey: this.getUserAttemptsCacheKey(id, params.userids!), | ||||||
|             component: AddonModH5PActivityProvider.COMPONENT, |                 updateFrequency: CoreSite.FREQUENCY_SOMETIMES, | ||||||
|             componentId: options.cmId, |                 component: AddonModH5PActivityProvider.COMPONENT, | ||||||
|             ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
 |                 componentId: options.cmId, | ||||||
|         }; |                 ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
 | ||||||
|  |             }; | ||||||
| 
 | 
 | ||||||
|         const response = await site.read<AddonModH5pactivityGetAttemptsWSResponse>('mod_h5pactivity_get_attempts', params, preSets); |             const response = await site.read<AddonModH5pactivityGetAttemptsWSResponse>( | ||||||
|  |                 'mod_h5pactivity_get_attempts', | ||||||
|  |                 params, | ||||||
|  |                 preSets, | ||||||
|  |             ); | ||||||
| 
 | 
 | ||||||
|         if (response.warnings?.[0]) { |             if (response.warnings?.[0]) { | ||||||
|             throw new CoreWSError(response.warnings[0]); // Cannot view user attempts.
 |                 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 id Activity ID. | ||||||
|      * @param siteId Site ID. If not defined, current site. |      * @param siteId Site ID. If not defined, current site. | ||||||
|      * @return Promise resolved when the data is invalidated. |      * @return Promise resolved when the data is invalidated. | ||||||
|      */ |      */ | ||||||
|     async invalidateAllUserAttempts(id: number, siteId?: string): Promise<void> { |     async invalidateAllUsersAttempts(id: number, siteId?: string): Promise<void> { | ||||||
|         const site = await CoreSites.getSite(siteId); |         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.
 |     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' { | declare module '@singletons/events' { | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | |||||||
| @ -147,20 +147,31 @@ export class AddonModH5PActivityPrefetchHandlerService extends CoreCourseActivit | |||||||
|             siteId, |             siteId, | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|  |         const options = { | ||||||
|  |             cmId: h5pActivity.coursemodule, | ||||||
|  |             readingStrategy: CoreSitesReadingStrategy.ONLY_NETWORK, | ||||||
|  |             siteId: siteId, | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|         if (!accessInfo.canreviewattempts) { |         if (!accessInfo.canreviewattempts) { | ||||||
|             // Not a teacher, prefetch user attempts and the current user profile.
 |             // Not a teacher, prefetch user attempts and the current user profile.
 | ||||||
|             const site = await CoreSites.getSite(siteId); |             const site = await CoreSites.getSite(siteId); | ||||||
| 
 | 
 | ||||||
|             const options = { |  | ||||||
|                 cmId: h5pActivity.coursemodule, |  | ||||||
|                 readingStrategy: CoreSitesReadingStrategy.ONLY_NETWORK, |  | ||||||
|                 siteId: siteId, |  | ||||||
|             }; |  | ||||||
| 
 |  | ||||||
|             await Promise.all([ |             await Promise.all([ | ||||||
|                 AddonModH5PActivity.getAllAttemptsResults(h5pActivity.id, options), |                 AddonModH5PActivity.getAllAttemptsResults(h5pActivity.id, options), | ||||||
|                 CoreUser.prefetchProfiles([site.getUserId()], h5pActivity.course, siteId), |                 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); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user