MOBILE-3799 h5p: Let teachers view users attempts in activity

main
Dani Palou 2021-08-02 15:58:33 +02:00
parent 36f4d33f6b
commit 85ac2b0bb5
11 changed files with 602 additions and 27 deletions

View File

@ -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",

View File

@ -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>

View File

@ -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.
*

View File

@ -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({

View File

@ -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}}"
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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 {}

View File

@ -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;
};

View File

@ -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' {
/**

View File

@ -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);
}
}