// (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 { 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 { 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 { const promises: Promise[] = []; 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 { 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 & { profileimageurl?: string; attempts: AttemptWithLabel[]; }; /** * Student attempt with a calculated label. */ type AttemptWithLabel = AddonModLessonAttemptsOverviewsAttemptWSData & { label?: string; }; /** * Retake with calculated data. */ type RetakeToDisplay = Omit & { answerpages: AnswerPage[]; }; /** * Answer page with calculated data. */ type AnswerPage = Omit & { isContent?: boolean; isQuestion?: boolean; answerdata?: AnswerData; }; /** * Answer data with calculated data. */ type AnswerData = Omit & { answers?: (string[] | AddonModLessonAnswerData)[]; // User answers. };