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_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", | ||||
|  | ||||
| @ -5,6 +5,10 @@ | ||||
|             [priority]="1000" [content]="'addon.mod_h5pactivity.review_my_attempts' | translate" (action)="viewMyAttempts()" | ||||
|             iconAction="stats-chart"> | ||||
|         </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" | ||||
|             [href]="externalUrl" iconAction="fas-external-link-alt"> | ||||
|         </core-context-menu-item> | ||||
|  | ||||
| @ -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<void> { | ||||
|         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. | ||||
|      * | ||||
|  | ||||
| @ -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({ | ||||
|  | ||||
| @ -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,12 +23,15 @@ | ||||
|     "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", | ||||
|  | ||||
| @ -17,7 +17,7 @@ | ||||
|     <core-loading [hideUntil]="loaded"> | ||||
|         <!-- User viewed. --> | ||||
|         <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> | ||||
|             <ion-label> | ||||
|                 <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 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. | ||||
| @ -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. | ||||
|      * | ||||
| @ -455,26 +605,56 @@ export class AddonModH5PActivityProvider { | ||||
|     ): Promise<AddonModH5PActivityUserAttempts> { | ||||
| 
 | ||||
|         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<AddonModH5pactivityGetAttemptsWSResponse>('mod_h5pactivity_get_attempts', params, preSets); | ||||
|             const response = await site.read<AddonModH5pactivityGetAttemptsWSResponse>( | ||||
|                 '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<void> { | ||||
|     async invalidateAllUsersAttempts(id: number, siteId?: string): Promise<void> { | ||||
|         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' { | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -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); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user