2024-11-18 14:24:18 +01:00

315 lines
11 KiB
TypeScript

// (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 { CoreError } from '@classes/errors/error';
import { CoreUser } from '@features/user/services/user';
import { CoreNavigator } from '@services/navigator';
import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreText } from '@singletons/text';
import { CoreErrorHelper } from '@services/error-helper';
import { Translate } from '@singletons';
import {
AddonModLesson,
AddonModLessonAttemptsOverviewsAttemptWSData,
AddonModLessonAttemptsOverviewsStudentWSData,
AddonModLessonGetUserAttemptWSResponse,
AddonModLessonLessonWSData,
AddonModLessonUserAttemptAnswerData,
AddonModLessonUserAttemptAnswerPageWSData,
} from '../../services/lesson';
import { AddonModLessonAnswerData, AddonModLessonHelper } from '../../services/lesson-helper';
import { CoreTime } from '@singletons/time';
import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics';
import { ADDON_MOD_LESSON_COMPONENT } from '../../constants';
import { CorePromiseUtils } from '@singletons/promise-utils';
/**
* Page that displays a retake made by a certain user.
*/
@Component({
selector: 'page-addon-mod-lesson-user-retake',
templateUrl: 'user-retake.html',
styleUrl: 'user-retake.scss',
})
export class AddonModLessonUserRetakePage implements OnInit {
component = ADDON_MOD_LESSON_COMPONENT;
lesson?: AddonModLessonLessonWSData; // The lesson the retake belongs to.
courseId!: number; // Course ID the lesson belongs to.
selectedRetake?: number; // The retake to see.
student?: StudentData; // Data about the student and his retakes.
retake?: RetakeToDisplay; // Data about the retake.
loaded?: boolean; // Whether the data has been loaded.
timeTakenReadable?: string; // Time taken in a readable format.
protected cmId!: number; // The lesson ID the retake belongs to.
protected userId?: number; // User ID to see the retakes.
protected retakeNumber?: number; // Number of the initial retake to see.
protected previousSelectedRetake?: number; // To be able to detect the previous selected retake when it has changed.
protected logView: () => void;
constructor() {
this.logView = CoreTime.once(() => this.performLogView());
}
/**
* @inheritdoc
*/
ngOnInit(): void {
try {
this.cmId = CoreNavigator.getRequiredRouteNumberParam('cmId');
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
this.userId = CoreNavigator.getRouteNumberParam('userId') || CoreSites.getCurrentSiteUserId();
this.retakeNumber = CoreNavigator.getRouteNumberParam('retake');
} catch (error) {
CoreDomUtils.showErrorModal(error);
CoreNavigator.back();
return;
}
// Fetch the data.
this.fetchData().finally(() => {
this.loaded = true;
});
}
/**
* Change the retake displayed.
*
* @param retakeNumber The new retake number.
*/
async changeRetake(retakeNumber: number): Promise<void> {
this.loaded = false;
try {
await this.setRetake(retakeNumber);
this.performLogView();
} catch (error) {
this.selectedRetake = this.previousSelectedRetake ?? this.selectedRetake;
CoreDomUtils.showErrorModal(CoreErrorHelper.addDataNotDownloadedError(error, 'Error getting attempt.'));
} finally {
this.loaded = true;
}
}
/**
* Pull to refresh.
*
* @param refresher Refresher.
*/
doRefresh(refresher: HTMLIonRefresherElement): void {
this.refreshData().finally(() => {
refresher?.complete();
});
}
/**
* Get lesson and retake data.
*
* @returns Promise resolved when done.
*/
protected async fetchData(): Promise<void> {
try {
this.lesson = await AddonModLesson.getLesson(this.courseId, this.cmId);
// Get the retakes overview for all participants.
const data = await AddonModLesson.getRetakesOverview(this.lesson.id, {
cmId: this.cmId,
});
// Search the student.
const student: StudentData | undefined = data?.students?.find(student => student.id == this.userId);
if (!student) {
// Student not found.
throw new CoreError(Translate.instant('addon.mod_lesson.cannotfinduser'));
}
if (!student.attempts.length) {
// No retakes.
throw new CoreError(Translate.instant('addon.mod_lesson.cannotfindattempt'));
}
student.bestgrade = CoreText.roundToDecimals(student.bestgrade, 2);
student.attempts.forEach((retake) => {
if (!this.selectedRetake && this.retakeNumber == retake.try) {
// The retake specified as parameter exists. Use it.
this.selectedRetake = this.retakeNumber;
}
retake.label = AddonModLessonHelper.getRetakeLabel(retake);
});
if (!this.selectedRetake) {
// Retake number not specified or not valid, use the last retake.
this.selectedRetake = student.attempts[student.attempts.length - 1].try;
}
// Get the profile image of the user.
const user = await CorePromiseUtils.ignoreErrors(CoreUser.getProfile(student.id, this.courseId, true));
this.student = student;
this.student.profileimageurl = user?.profileimageurl;
await this.setRetake(this.selectedRetake);
this.logView();
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'Error getting data.', true);
}
}
/**
* Refreshes data.
*
* @returns Promise resolved when done.
*/
protected async refreshData(): Promise<void> {
const promises: Promise<void>[] = [];
promises.push(AddonModLesson.invalidateLessonData(this.courseId));
if (this.lesson) {
promises.push(AddonModLesson.invalidateRetakesOverview(this.lesson.id));
promises.push(AddonModLesson.invalidateUserRetakesForUser(this.lesson.id, this.userId));
}
await CorePromiseUtils.ignoreErrors(Promise.all(promises));
await this.fetchData();
}
/**
* Set the retake to view and load its data.
*
* @param retakeNumber Retake number to set.
* @returns Promise resolved when done.
*/
protected async setRetake(retakeNumber: number): Promise<void> {
this.selectedRetake = retakeNumber;
const retakeData = await AddonModLesson.getUserRetake(this.lesson!.id, retakeNumber, {
cmId: this.cmId,
userId: this.userId,
});
this.retake = this.formatRetake(retakeData);
this.previousSelectedRetake = this.selectedRetake;
}
/**
* Format retake data, adding some calculated data.
*
* @param retakeData Retake data.
* @returns Formatted data.
*/
protected formatRetake(retakeData: AddonModLessonGetUserAttemptWSResponse): RetakeToDisplay {
const formattedData = retakeData;
if (formattedData.userstats.gradeinfo) {
// Completed.
formattedData.userstats.grade = CoreText.roundToDecimals(formattedData.userstats.grade, 2);
this.timeTakenReadable = CoreTime.formatTime(formattedData.userstats.timetotake);
}
// Format pages data.
formattedData.answerpages.forEach((page) => {
if (AddonModLesson.answerPageIsContent(page)) {
const contentPage = page as AnswerPage;
contentPage.isContent = true;
if (contentPage.answerdata?.answers) {
contentPage.answerdata.answers.forEach((answer) => {
// Content pages only have 1 valid field in the answer array.
answer[0] = AddonModLessonHelper.getContentPageAnswerDataFromHtml(answer[0]);
});
}
} else if (AddonModLesson.answerPageIsQuestion(page)) {
const questionPage = page as AnswerPage;
questionPage.isQuestion = true;
if (questionPage.answerdata?.answers) {
questionPage.answerdata.answers.forEach((answer) => {
// Only the first field of the answer array requires to be parsed.
answer[0] = AddonModLessonHelper.getQuestionPageAnswerDataFromHtml(answer[0]);
});
}
}
});
return formattedData;
}
/**
* Log view.
*/
protected performLogView(): void {
if (!this.lesson) {
return;
}
CoreAnalytics.logEvent({
type: CoreAnalyticsEventType.VIEW_ITEM,
ws: 'mod_lesson_get_user_attempt',
name: this.lesson.name + ': ' + Translate.instant('addon.mod_lesson.detailedstats'),
data: { id: this.lesson.id, userid: this.userId, try: this.selectedRetake, category: 'lesson' },
url: `/mod/lesson/report.php?id=${this.cmId}&action=reportdetail&userid=${this.userId}&try=${this.selectedRetake}`,
});
}
}
/**
* Student data with some calculated data.
*/
type StudentData = Omit<AddonModLessonAttemptsOverviewsStudentWSData, 'attempts'> & {
profileimageurl?: string;
attempts: AttemptWithLabel[];
};
/**
* Student attempt with a calculated label.
*/
type AttemptWithLabel = AddonModLessonAttemptsOverviewsAttemptWSData & {
label?: string;
};
/**
* Retake with calculated data.
*/
type RetakeToDisplay = Omit<AddonModLessonGetUserAttemptWSResponse, 'answerpages'> & {
answerpages: AnswerPage[];
};
/**
* Answer page with calculated data.
*/
type AnswerPage = Omit<AddonModLessonUserAttemptAnswerPageWSData, 'answerdata'> & {
isContent?: boolean;
isQuestion?: boolean;
answerdata?: AnswerData;
};
/**
* Answer data with calculated data.
*/
type AnswerData = Omit<AddonModLessonUserAttemptAnswerData, 'answers'> & {
answers?: (string[] | AddonModLessonAnswerData)[]; // User answers.
};