Merge pull request #3025 from dpalou/MOBILE-3946

Mobile 3946
This commit is contained in:
Pau Ferrer Ocaña 2021-12-15 12:07:16 +01:00 committed by GitHub
commit be68ff374e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 147 additions and 43 deletions

View File

@ -37,6 +37,7 @@ import {
AddonModH5PActivityAccessInfo, AddonModH5PActivityAccessInfo,
AddonModH5PActivityData, AddonModH5PActivityData,
AddonModH5PActivityProvider, AddonModH5PActivityProvider,
AddonModH5PActivityXAPIData,
} from '../../services/h5pactivity'; } from '../../services/h5pactivity';
import { import {
AddonModH5PActivitySync, AddonModH5PActivitySync,
@ -165,7 +166,11 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
protected async checkHasOffline(): Promise<void> { protected async checkHasOffline(): Promise<void> {
this.hasOffline = await CoreXAPIOffline.contextHasStatements(this.h5pActivity!.context, this.siteId); if (!this.h5pActivity) {
return;
}
this.hasOffline = await CoreXAPIOffline.contextHasStatements(this.h5pActivity.context, this.siteId);
} }
/** /**
@ -174,7 +179,11 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
protected async fetchAccessInfo(): Promise<void> { protected async fetchAccessInfo(): Promise<void> {
this.accessInfo = await AddonModH5PActivity.getAccessInformation(this.h5pActivity!.id, { if (!this.h5pActivity) {
return;
}
this.accessInfo = await AddonModH5PActivity.getAccessInformation(this.h5pActivity.id, {
cmId: this.module.id, cmId: this.module.id,
siteId: this.siteId, siteId: this.siteId,
}); });
@ -186,12 +195,12 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
protected async fetchDeployedFileData(): Promise<void> { protected async fetchDeployedFileData(): Promise<void> {
if (!this.siteCanDownload) { if (!this.siteCanDownload || !this.h5pActivity) {
// Cannot download the file, no need to fetch the file data. // Cannot download the file, no need to fetch the file data.
return; return;
} }
this.deployedFile = await AddonModH5PActivity.getDeployedFile(this.h5pActivity!, { this.deployedFile = await AddonModH5PActivity.getDeployedFile(this.h5pActivity, {
displayOptions: this.displayOptions, displayOptions: this.displayOptions,
siteId: this.siteId, siteId: this.siteId,
}); });
@ -216,10 +225,14 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
protected async calculateFileState(): Promise<void> { protected async calculateFileState(): Promise<void> {
if (!this.fileUrl || !this.deployedFile) {
return;
}
this.state = await CoreFilepool.getFileStateByUrl( this.state = await CoreFilepool.getFileStateByUrl(
this.site.getId(), this.site.getId(),
this.fileUrl!, this.fileUrl,
this.deployedFile!.timemodified, this.deployedFile.timemodified,
); );
this.showFileState(); this.showFileState();
@ -267,6 +280,10 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
event?.preventDefault(); event?.preventDefault();
event?.stopPropagation(); event?.stopPropagation();
if (!this.deployedFile) {
return;
}
if (!CoreApp.isOnline()) { if (!CoreApp.isOnline()) {
CoreDomUtils.showErrorModal('core.networkerrormsg', true); CoreDomUtils.showErrorModal('core.networkerrormsg', true);
@ -275,7 +292,7 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
try { try {
// Confirm the download if needed. // Confirm the download if needed.
await CoreDomUtils.confirmDownloadSize({ size: this.deployedFile!.filesize!, total: true }); await CoreDomUtils.confirmDownloadSize({ size: this.deployedFile.filesize || 0, total: true });
await this.downloadDeployedFile(); await this.downloadDeployedFile();
@ -316,17 +333,22 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
protected async downloadDeployedFile(): Promise<void> { protected async downloadDeployedFile(): Promise<void> {
if (!this.fileUrl || !this.deployedFile) {
return;
}
const deployedFile = this.deployedFile;
this.downloading = true; this.downloading = true;
this.progressMessage = 'core.downloading'; this.progressMessage = 'core.downloading';
try { try {
await CoreFilepool.downloadUrl( await CoreFilepool.downloadUrl(
this.site.getId(), this.site.getId(),
this.fileUrl!, this.fileUrl,
false, false,
this.component, this.component,
this.componentId, this.componentId,
this.deployedFile!.timemodified, deployedFile.timemodified,
(data: DownloadProgressData) => { (data: DownloadProgressData) => {
if (!data) { if (!data) {
return; return;
@ -340,7 +362,7 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
this.progressMessage = data.message; this.progressMessage = data.message;
} else if (data.loaded !== undefined) { } else if (data.loaded !== undefined) {
// Downloading or unzipping. // Downloading or unzipping.
const totalSize = this.progressMessage == 'core.downloading' ? this.deployedFile!.filesize : data.total; const totalSize = this.progressMessage == 'core.downloading' ? deployedFile.filesize : data.total;
if (totalSize !== undefined) { if (totalSize !== undefined) {
const percentageNumber = (Number(data.loaded / totalSize) * 100); const percentageNumber = (Number(data.loaded / totalSize) * 100);
@ -362,11 +384,15 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
/** /**
* Play the package. * Play the package.
*/ */
play(): void { async play(): Promise<void> {
if (!this.h5pActivity) {
return;
}
this.playing = true; this.playing = true;
// Mark the activity as viewed. // Mark the activity as viewed.
AddonModH5PActivity.logView(this.h5pActivity!.id, this.h5pActivity!.name, this.siteId); await AddonModH5PActivity.logView(this.h5pActivity.id, this.h5pActivity.name, this.siteId);
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata); CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
} }
@ -409,7 +435,8 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
protected async onIframeMessage(event: MessageEvent): Promise<void> { protected async onIframeMessage(event: MessageEvent): Promise<void> {
if (!event.data || !CoreXAPI.canPostStatementsInSite(this.site) || !this.isCurrentXAPIPost(event.data)) { const data = event.data;
if (!data || !this.h5pActivity || !CoreXAPI.canPostStatementsInSite(this.site) || !this.isCurrentXAPIPost(data)) {
return; return;
} }
@ -417,14 +444,14 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
const options = { const options = {
offline: this.hasOffline, offline: this.hasOffline,
courseId: this.courseId, courseId: this.courseId,
extra: this.h5pActivity!.name, extra: this.h5pActivity.name,
siteId: this.site.getId(), siteId: this.site.getId(),
}; };
const sent = await CoreXAPI.postStatements( const sent = await CoreXAPI.postStatements(
this.h5pActivity!.context, this.h5pActivity.context,
event.data.component, data.component,
JSON.stringify(event.data.statements), JSON.stringify(data.statements),
options, options,
); );
@ -433,10 +460,16 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
if (sent) { if (sent) {
try { try {
// Invalidate attempts. // Invalidate attempts.
await AddonModH5PActivity.invalidateUserAttempts(this.h5pActivity!.id, undefined, this.siteId); await AddonModH5PActivity.invalidateUserAttempts(this.h5pActivity.id, undefined, this.siteId);
} catch (error) { } catch (error) {
// Ignore errors. // Ignore errors.
} }
// Check if the H5P has ended. Final statements don't include a subContentId.
const hasEnded = data.statements.some(statement => !statement.object.id.includes('subContentId='));
if (hasEnded) {
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
}
} }
} catch (error) { } catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'Error sending tracking data.'); CoreDomUtils.showErrorModalDefault(error, 'Error sending tracking data.');
@ -450,7 +483,11 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
* @return Whether it's an XAPI post statement of the current activity. * @return Whether it's an XAPI post statement of the current activity.
*/ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
protected isCurrentXAPIPost(data: any): boolean { protected isCurrentXAPIPost(data: any): data is AddonModH5PActivityXAPIData {
if (!this.h5pActivity) {
return false;
}
if (data.environment != 'moodleapp' || data.context != 'h5p' || data.action != 'xapi_post_statement' || !data.statements) { if (data.environment != 'moodleapp' || data.context != 'h5p' || data.action != 'xapi_post_statement' || !data.statements) {
return false; return false;
} }
@ -468,14 +505,21 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
const match = trackingUrl.match(/xapi\/activity\/(\d+)/); const match = trackingUrl.match(/xapi\/activity\/(\d+)/);
return match && match[1] == this.h5pActivity!.context; return match && match[1] == this.h5pActivity.context;
} }
/** /**
* @inheritdoc * @inheritdoc
*/ */
protected sync(): Promise<AddonModH5PActivitySyncResult> { protected async sync(): Promise<AddonModH5PActivitySyncResult> {
return AddonModH5PActivitySync.syncActivity(this.h5pActivity!.context, this.site.getId()); if (!this.h5pActivity) {
return {
updated: false,
warnings: [],
};
}
return await AddonModH5PActivitySync.syncActivity(this.h5pActivity.context, this.site.getId());
} }
/** /**

View File

@ -65,7 +65,9 @@ export class AddonModH5PActivityAttemptResultsPage implements OnInit {
try { try {
await this.fetchData(); await this.fetchData();
await AddonModH5PActivity.logViewReport(this.h5pActivity!.id, this.h5pActivity!.name, { attemptId: this.attemptId }); if (this.h5pActivity) {
await AddonModH5PActivity.logViewReport(this.h5pActivity.id, this.h5pActivity.name, { attemptId: this.attemptId });
}
} catch (error) { } catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'Error loading attempt.'); CoreDomUtils.showErrorModalDefault(error, 'Error loading attempt.');
} finally { } finally {
@ -105,8 +107,12 @@ export class AddonModH5PActivityAttemptResultsPage implements OnInit {
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
protected async fetchUserProfile(): Promise<void> { protected async fetchUserProfile(): Promise<void> {
if (!this.attempt) {
return;
}
try { try {
this.user = await CoreUser.getProfile(this.attempt!.userid, this.courseId, true); this.user = await CoreUser.getProfile(this.attempt.userid, this.courseId, true);
} catch (error) { } catch (error) {
// Ignore errors. // Ignore errors.
} }

View File

@ -68,7 +68,9 @@ export class AddonModH5PActivityUserAttemptsPage implements OnInit {
try { try {
await this.fetchData(); await this.fetchData();
await AddonModH5PActivity.logViewReport(this.h5pActivity!.id, this.h5pActivity!.name, { userId: this.userId }); if (this.h5pActivity) {
await AddonModH5PActivity.logViewReport(this.h5pActivity.id, this.h5pActivity.name, { userId: this.userId });
}
} catch (error) { } catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'Error loading attempts.'); CoreDomUtils.showErrorModalDefault(error, 'Error loading attempts.');
} finally { } finally {
@ -107,7 +109,11 @@ export class AddonModH5PActivityUserAttemptsPage implements OnInit {
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
protected async fetchAttempts(): Promise<void> { protected async fetchAttempts(): Promise<void> {
this.attemptsData = await AddonModH5PActivity.getUserAttempts(this.h5pActivity!.id, { if (!this.h5pActivity) {
return;
}
this.attemptsData = await AddonModH5PActivity.getUserAttempts(this.h5pActivity.id, {
cmId: this.cmId, cmId: this.cmId,
userId: this.userId, userId: this.userId,
}); });

View File

@ -63,7 +63,9 @@ export class AddonModH5PActivityUsersAttemptsPage implements OnInit {
try { try {
await this.fetchData(); await this.fetchData();
await AddonModH5PActivity.logViewReport(this.h5pActivity!.id, this.h5pActivity!.name); if (this.h5pActivity) {
await AddonModH5PActivity.logViewReport(this.h5pActivity.id, this.h5pActivity.name);
}
} catch (error) { } catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'Error loading attempts.'); CoreDomUtils.showErrorModalDefault(error, 'Error loading attempts.');
} finally { } finally {
@ -103,16 +105,20 @@ export class AddonModH5PActivityUsersAttemptsPage implements OnInit {
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
protected async fetchUsers(refresh?: boolean): Promise<void> { protected async fetchUsers(refresh?: boolean): Promise<void> {
if (!this.h5pActivity) {
return;
}
if (refresh) { if (refresh) {
this.page = 0; this.page = 0;
} }
const result = await AddonModH5PActivity.getUsersAttempts(this.h5pActivity!.id, { const result = await AddonModH5PActivity.getUsersAttempts(this.h5pActivity.id, {
cmId: this.cmId, cmId: this.cmId,
page: this.page, page: this.page,
}); });
const formattedUsers = await this.formatUsers(result.users); const formattedUsers = await this.formatUsers(this.h5pActivity, result.users);
if (this.page === 0) { if (this.page === 0) {
this.users = formattedUsers; this.users = formattedUsers;
@ -127,17 +133,21 @@ export class AddonModH5PActivityUsersAttemptsPage implements OnInit {
/** /**
* Format users data. * Format users data.
* *
* @param h5pActivity Activity data.
* @param users Users to format. * @param users Users to format.
* @return Formatted users. * @return Formatted users.
*/ */
protected async formatUsers(users: AddonModH5PActivityUserAttempts[]): Promise<AddonModH5PActivityUserAttemptsFormatted[]> { protected async formatUsers(
h5pActivity: AddonModH5PActivityData,
users: AddonModH5PActivityUserAttempts[],
): Promise<AddonModH5PActivityUserAttemptsFormatted[]> {
return await Promise.all(users.map(async (user: AddonModH5PActivityUserAttemptsFormatted) => { return await Promise.all(users.map(async (user: AddonModH5PActivityUserAttemptsFormatted) => {
user.user = await CoreUser.getProfile(user.userid, this.courseId, true); user.user = await CoreUser.getProfile(user.userid, this.courseId, true);
// Calculate the score of the user. // Calculate the score of the user.
if (this.h5pActivity!.grademethod === AddonModH5PActivityProvider.GRADEMANUAL) { if (h5pActivity.grademethod === AddonModH5PActivityProvider.GRADEMANUAL) {
// No score. // No score.
} else if (this.h5pActivity!.grademethod === AddonModH5PActivityProvider.GRADEAVERAGEATTEMPT) { } else if (h5pActivity.grademethod === AddonModH5PActivityProvider.GRADEAVERAGEATTEMPT) {
if (user.attempts.length) { if (user.attempts.length) {
// Calculate the average. // Calculate the average.
const sumScores = user.attempts.reduce((sumScores, attempt) => const sumScores = user.attempts.reduce((sumScores, attempt) =>

View File

@ -216,8 +216,8 @@ export class AddonModH5PActivityProvider {
const optionsWithPage: AddonModH5PActivityGetAllUsersAttemptsOptions = { const optionsWithPage: AddonModH5PActivityGetAllUsersAttemptsOptions = {
...options, ...options,
page: 0,
}; };
optionsWithPage.page = 0;
let canLoadMore = true; let canLoadMore = true;
let users: AddonModH5PActivityUserAttempts[] = []; let users: AddonModH5PActivityUserAttempts[] = [];
@ -225,7 +225,7 @@ export class AddonModH5PActivityProvider {
try { try {
const result = await this.getUsersAttempts(id, optionsWithPage); const result = await this.getUsersAttempts(id, optionsWithPage);
optionsWithPage.page = optionsWithPage.page! + 1; optionsWithPage.page = optionsWithPage.page + 1;
users = users.concat(result.users); users = users.concat(result.users);
canLoadMore = result.canLoadMore; canLoadMore = result.canLoadMore;
} catch (error) { } catch (error) {
@ -359,10 +359,11 @@ export class AddonModH5PActivityProvider {
const params: AddonModH5pactivityGetResultsWSParams = { const params: AddonModH5pactivityGetResultsWSParams = {
h5pactivityid: id, h5pactivityid: id,
attemptids: [attemptId],
}; };
params.attemptids = [attemptId];
const preSets: CoreSiteWSPreSets = { const preSets: CoreSiteWSPreSets = {
cacheKey: this.getAttemptResultsCacheKey(id, params.attemptids!), cacheKey: this.getAttemptResultsCacheKey(id, params.attemptids),
updateFrequency: CoreSite.FREQUENCY_SOMETIMES, updateFrequency: CoreSite.FREQUENCY_SOMETIMES,
component: AddonModH5PActivityProvider.COMPONENT, component: AddonModH5PActivityProvider.COMPONENT,
componentId: options.cmId, componentId: options.cmId,
@ -610,10 +611,11 @@ export class AddonModH5PActivityProvider {
try { try {
const params: AddonModH5pactivityGetAttemptsWSParams = { const params: AddonModH5pactivityGetAttemptsWSParams = {
h5pactivityid: id, h5pactivityid: id,
userids: [userId],
}; };
params.userids = [userId];
const preSets: CoreSiteWSPreSets = { const preSets: CoreSiteWSPreSets = {
cacheKey: this.getUserAttemptsCacheKey(id, params.userids!), cacheKey: this.getUserAttemptsCacheKey(id, params.userids),
updateFrequency: CoreSite.FREQUENCY_SOMETIMES, updateFrequency: CoreSite.FREQUENCY_SOMETIMES,
component: AddonModH5PActivityProvider.COMPONENT, component: AddonModH5PActivityProvider.COMPONENT,
componentId: options.cmId, componentId: options.cmId,
@ -1128,3 +1130,43 @@ declare module '@singletons/events' {
} }
} }
/**
* Data to be sent using xAPI.
*/
export type AddonModH5PActivityXAPIData = {
action: string;
component: string;
context: string;
environment: string;
statements: AddonModH5PActivityStatement[];
};
/**
* xAPI statement.
*/
export type AddonModH5PActivityStatement = {
actor: Record<string, string>;
context: Record<string, unknown>;
object: {
id: string;
definition: Record<string, unknown>;
objectType: string;
};
result: {
completion: boolean;
duration: string;
score: {
min: number;
max: number;
raw: number;
scaled: number;
};
success?: boolean;
response?: string;
};
verb: {
id: string;
display: Record<string, string>;
};
};

View File

@ -203,11 +203,7 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR
try { try {
if (refresh && this.showCompletion) { if (refresh && this.showCompletion) {
try { await CoreUtils.ignoreErrors(this.fetchModule());
this.module = await CoreCourse.getModule(this.module.id, this.courseId);
} catch {
// Ignore errors.
}
} }
await this.fetchContent(refresh, sync, showErrors); await this.fetchContent(refresh, sync, showErrors);