MOBILE-3799 h5p: Let teachers view users attempts in activity
parent
36f4d33f6b
commit
85ac2b0bb5
|
@ -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,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}}"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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…
Reference in New Issue