diff --git a/src/addons/mod/quiz/components/index/index.ts b/src/addons/mod/quiz/components/index/index.ts index 8a5b7a0a3..c4adff103 100644 --- a/src/addons/mod/quiz/components/index/index.ts +++ b/src/addons/mod/quiz/components/index/index.ts @@ -333,15 +333,16 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp return; } + const bestGrade = this.bestGrade.grade; const formattedGradebookGrade = AddonModQuiz.formatGrade(this.gradebookData.grade, quiz.decimalpoints); - const formattedBestGrade = AddonModQuiz.formatGrade(this.bestGrade.grade, quiz.decimalpoints); + const formattedBestGrade = AddonModQuiz.formatGrade(bestGrade, quiz.decimalpoints); let gradeToShow = formattedGradebookGrade; // By default we show the grade in the gradebook. this.showResults = true; this.gradeOverridden = formattedGradebookGrade != formattedBestGrade; this.gradebookFeedback = this.gradebookData.feedback; - if (this.bestGrade.grade! > this.gradebookData.grade && this.gradebookData.grade == quiz.grade) { + if (bestGrade && bestGrade > this.gradebookData.grade && this.gradebookData.grade == quiz.grade) { // The best grade is higher than the max grade for the quiz. // We'll do like Moodle web and show the best grade instead of the gradebook grade. this.gradeOverridden = false; @@ -556,8 +557,16 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp * * @returns Promise resolved when done. */ - protected sync(): Promise { - return AddonModQuizSync.syncQuiz(this.candidateQuiz!, true); + protected async sync(): Promise { + if (!this.candidateQuiz) { + return { + warnings: [], + attemptFinished: false, + updated: false, + }; + } + + return AddonModQuizSync.syncQuiz(this.candidateQuiz, true); } /** @@ -579,36 +588,31 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp } const lastFinished = AddonModQuiz.getLastFinishedAttemptFromList(attempts); - const promises: Promise[] = []; + let openReview = false; if (this.autoReview && lastFinished && lastFinished.id >= this.autoReview.attemptId) { // User just finished an attempt in offline and it seems it's been synced, since it's finished in online. // Go to the review of this attempt if the user hasn't left this view. if (!this.isDestroyed && this.isCurrentView) { - promises.push(this.goToAutoReview()); + openReview = true; } this.autoReview = undefined; } - // Get combined review options. - promises.push(AddonModQuiz.getCombinedReviewOptions(quiz.id, { cmId: this.module.id }).then((options) => { - this.options = options; - - return; - })); - - // Get best grade. - promises.push(this.getQuizGrade()); - - await Promise.all(promises); + const [options] = await Promise.all([ + AddonModQuiz.getCombinedReviewOptions(quiz.id, { cmId: this.module.id }), + this.getQuizGrade(), + openReview ? this.goToAutoReview() : undefined, + ]); + this.options = options; const grade = this.gradebookData?.grade !== undefined ? this.gradebookData.grade : this.bestGrade?.grade; const quizGrade = AddonModQuiz.formatGrade(grade, quiz.decimalpoints); // Calculate data to construct the header of the attempts table. - AddonModQuizHelper.setQuizCalculatedData(quiz, this.options!); + AddonModQuizHelper.setQuizCalculatedData(quiz, this.options); - this.overallStats = !!lastFinished && this.options!.alloptions.marks >= AddonModQuizProvider.QUESTION_OPTIONS_MARK_AND_MAX; + this.overallStats = !!lastFinished && this.options.alloptions.marks >= AddonModQuizProvider.QUESTION_OPTIONS_MARK_AND_MAX; // Calculate data to show for each attempt. const formattedAttempts = await Promise.all(attempts.map((attempt, index) => { diff --git a/src/addons/mod/quiz/components/navigation-modal/navigation-modal.ts b/src/addons/mod/quiz/components/navigation-modal/navigation-modal.ts index 93fb6848c..044d4d0e8 100644 --- a/src/addons/mod/quiz/components/navigation-modal/navigation-modal.ts +++ b/src/addons/mod/quiz/components/navigation-modal/navigation-modal.ts @@ -61,6 +61,6 @@ export type AddonModQuizNavigationQuestion = CoreQuestionQuestionParsed & { }; export type AddonModQuizNavigationModalReturn = { - page?: number; + page: number; slot?: number; }; diff --git a/src/addons/mod/quiz/components/preflight-modal/preflight-modal.ts b/src/addons/mod/quiz/components/preflight-modal/preflight-modal.ts index 486771f1b..afb82240f 100644 --- a/src/addons/mod/quiz/components/preflight-modal/preflight-modal.ts +++ b/src/addons/mod/quiz/components/preflight-modal/preflight-modal.ts @@ -66,11 +66,13 @@ export class AddonModQuizPreflightModalComponent implements OnInit { } try { + const quiz = this.quiz; + await Promise.all(this.rules.map(async (rule) => { // Check if preflight is required for rule and, if so, get the component to render it. const required = await AddonModQuizAccessRuleDelegate.isPreflightCheckRequiredForRule( rule, - this.quiz!, + quiz, this.attempt, this.prefetch, this.siteId, diff --git a/src/addons/mod/quiz/pages/attempt/attempt.page.ts b/src/addons/mod/quiz/pages/attempt/attempt.page.ts index ebc7ca96e..21b26bab2 100644 --- a/src/addons/mod/quiz/pages/attempt/attempt.page.ts +++ b/src/addons/mod/quiz/pages/attempt/attempt.page.ts @@ -94,20 +94,20 @@ export class AddonModQuizAttemptPage implements OnInit { // Load attempt data. const [options, accessInfo, attempt] = await Promise.all([ AddonModQuiz.getCombinedReviewOptions(this.quiz.id, { cmId: this.quiz.coursemodule }), - this.fetchAccessInfo(), - this.fetchAttempt(), + this.fetchAccessInfo(this.quiz), + this.fetchAttempt(this.quiz.id), ]); // Set calculated data. this.showReviewColumn = accessInfo.canreviewmyattempts; AddonModQuizHelper.setQuizCalculatedData(this.quiz, options); - this.attempt = await AddonModQuizHelper.setAttemptCalculatedData(this.quiz!, attempt, false, undefined, true); + this.attempt = await AddonModQuizHelper.setAttemptCalculatedData(this.quiz, attempt, false, undefined, true); // Check if the feedback should be displayed. - const grade = Number(this.attempt!.rescaledGrade); + const grade = Number(this.attempt.rescaledGrade); - if (this.quiz.showFeedbackColumn && AddonModQuiz.isAttemptFinished(this.attempt!.state) && + if (this.quiz.showFeedbackColumn && AddonModQuiz.isAttemptFinished(this.attempt.state) && options.someoptions.overallfeedback && !isNaN(grade)) { // Feedback should be displayed, get the feedback for the grade. @@ -127,11 +127,12 @@ export class AddonModQuizAttemptPage implements OnInit { /** * Get the attempt. * + * @param quizId Quiz ID. * @returns Promise resolved when done. */ - protected async fetchAttempt(): Promise { + protected async fetchAttempt(quizId: number): Promise { // Get all the attempts and search the one we want. - const attempts = await AddonModQuiz.getUserAttempts(this.quiz!.id, { cmId: this.cmId }); + const attempts = await AddonModQuiz.getUserAttempts(quizId, { cmId: this.cmId }); const attempt = attempts.find(attempt => attempt.id == this.attemptId); @@ -148,10 +149,11 @@ export class AddonModQuizAttemptPage implements OnInit { /** * Get the access info. * + * @param quiz Quiz instance. * @returns Promise resolved when done. */ - protected async fetchAccessInfo(): Promise { - const accessInfo = await AddonModQuiz.getQuizAccessInformation(this.quiz!.id, { cmId: this.cmId }); + protected async fetchAccessInfo(quiz: AddonModQuizQuizData): Promise { + const accessInfo = await AddonModQuiz.getQuizAccessInformation(quiz.id, { cmId: this.cmId }); if (!accessInfo.canreviewmyattempts) { return accessInfo; @@ -161,7 +163,7 @@ export class AddonModQuizAttemptPage implements OnInit { await CoreUtils.ignoreErrors(AddonModQuiz.invalidateAttemptReviewForPage(this.attemptId, -1)); try { - await AddonModQuiz.getAttemptReview(this.attemptId, { page: -1, cmId: this.quiz!.coursemodule }); + await AddonModQuiz.getAttemptReview(this.attemptId, { page: -1, cmId: quiz.coursemodule }); } catch { // Error getting the review, assume the user cannot review the attempt. accessInfo.canreviewmyattempts = false; @@ -202,7 +204,11 @@ export class AddonModQuizAttemptPage implements OnInit { * @returns Promise resolved when done. */ async reviewAttempt(): Promise { - CoreNavigator.navigate(`../../review/${this.attempt!.id}`); + if (!this.attempt) { + return; + } + + CoreNavigator.navigate(`../../review/${this.attempt.id}`); } } diff --git a/src/addons/mod/quiz/pages/player/player.page.ts b/src/addons/mod/quiz/pages/player/player.page.ts index 0d6a9d5b8..c44d5119c 100644 --- a/src/addons/mod/quiz/pages/player/player.page.ts +++ b/src/addons/mod/quiz/pages/player/player.page.ts @@ -203,6 +203,10 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave { * @param button Clicked button. */ async behaviourButtonClicked(button: CoreQuestionBehaviourButton): Promise { + if (!this.quiz || !this.attempt) { + return; + } + let modal: CoreIonLoadingElement | undefined; try { @@ -212,13 +216,13 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave { modal = await CoreDomUtils.showModalLoading('core.sending', true); // Get the answers. - const answers = await this.prepareAnswers(); + const answers = await this.prepareAnswers(this.quiz.coursemodule); // Add the clicked button data. answers[button.name] = button.value; // Behaviour checks are always in online. - await AddonModQuiz.processAttempt(this.quiz!, this.attempt!, answers, this.preflightData); + await AddonModQuiz.processAttempt(this.quiz, this.attempt, answers, this.preflightData); this.reloadNavigation = true; // Data sent to server, navigation should be reloaded. @@ -230,7 +234,7 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave { this.content?.scrollToTop(); // Scroll top so the spinner is seen. try { - await this.loadPage(this.attempt!.currentpage!); + await this.loadPage(this.attempt.currentpage ?? 0); } finally { this.loaded = true; if (scrollTop != -1) { @@ -311,8 +315,8 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave { } } catch (error) { // If the user isn't seeing the summary, start the check again. - if (!this.showSummary) { - this.autoSave.startCheckChangesProcess(this.quiz!, this.attempt, this.preflightData, this.offline); + if (!this.showSummary && this.quiz) { + this.autoSave.startCheckChangesProcess(this.quiz, this.attempt, this.preflightData, this.offline); } CoreDomUtils.showErrorModalDefault(error, 'addon.mod_quiz.errorgetquestions', true); @@ -351,7 +355,7 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave { this.offline = await AddonModQuiz.isLastAttemptOfflineUnfinished(this.quiz); } - if (this.quiz!.timelimit && this.quiz!.timelimit > 0) { + if (this.quiz.timelimit && this.quiz.timelimit > 0) { this.readableTimeLimit = CoreTime.formatTime(this.quiz.timelimit); } @@ -394,11 +398,15 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave { * @returns Promise resolved when done. */ async finishAttempt(userFinish?: boolean, timeUp?: boolean): Promise { + if (!this.quiz || !this.attempt) { + return; + } + let modal: CoreIonLoadingElement | undefined; try { // Show confirm if the user clicked the finish button and the quiz is in progress. - if (!timeUp && this.attempt!.state == AddonModQuizProvider.ATTEMPT_IN_PROGRESS) { + if (!timeUp && this.attempt.state == AddonModQuizProvider.ATTEMPT_IN_PROGRESS) { await CoreDomUtils.showConfirm(Translate.instant('addon.mod_quiz.confirmclose')); } @@ -408,8 +416,8 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave { // Trigger an event to notify the attempt was finished. CoreEvents.trigger(AddonModQuizProvider.ATTEMPT_FINISHED_EVENT, { - quizId: this.quiz!.id, - attemptId: this.attempt!.id, + quizId: this.quiz.id, + attemptId: this.attempt.id, synced: !this.offline, }, CoreSites.getCurrentSiteId()); @@ -431,9 +439,13 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave { * @returns Promise resolved when done. */ protected async fixSequenceChecks(): Promise { + if (!this.attempt) { + return; + } + // Get current page data again to get the latest sequencechecks. - const data = await AddonModQuiz.getAttemptData(this.attempt!.id, this.attempt!.currentpage!, this.preflightData, { - cmId: this.quiz!.coursemodule, + const data = await AddonModQuiz.getAttemptData(this.attempt.id, this.attempt.currentpage ?? 0, this.preflightData, { + cmId: this.quiz?.coursemodule, readingStrategy: this.offline ? CoreSitesReadingStrategy.PREFER_CACHE : CoreSitesReadingStrategy.ONLY_NETWORK, }); @@ -465,14 +477,14 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave { * Initializes the timer if enabled. */ protected initTimer(): void { - if (!this.attemptAccessInfo?.endtime || this.attemptAccessInfo.endtime < 0) { + if (!this.quizAccessInfo || !this.attempt || !this.attemptAccessInfo?.endtime || this.attemptAccessInfo.endtime < 0) { return; } // Quiz has an end time. Check if time left should be shown. const shouldShowTime = AddonModQuiz.shouldShowTimeLeft( - this.quizAccessInfo!.activerulenames, - this.attempt!, + this.quizAccessInfo.activerulenames, + this.attempt, this.attemptAccessInfo.endtime, ); @@ -490,8 +502,12 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave { * @returns Promise resolved when done. */ protected async loadPage(page: number): Promise { - const data = await AddonModQuiz.getAttemptData(this.attempt!.id, page, this.preflightData, { - cmId: this.quiz!.coursemodule, + if (!this.quiz || !this.attempt) { + return; + } + + const data = await AddonModQuiz.getAttemptData(this.attempt.id, page, this.preflightData, { + cmId: this.quiz.coursemodule, readingStrategy: this.offline ? CoreSitesReadingStrategy.PREFER_CACHE : CoreSitesReadingStrategy.ONLY_NETWORK, }); @@ -523,7 +539,7 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave { ); // Start looking for changes. - this.autoSave.startCheckChangesProcess(this.quiz!, this.attempt, this.preflightData, this.offline); + this.autoSave.startCheckChangesProcess(this.quiz, this.attempt, this.preflightData, this.offline); } /** @@ -532,23 +548,27 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave { * @returns Promise resolved when done. */ protected async loadSummary(): Promise { + if (!this.quiz || !this.attempt) { + return; + } + this.summaryQuestions = []; - this.summaryQuestions = await AddonModQuiz.getAttemptSummary(this.attempt!.id, this.preflightData, { - cmId: this.quiz!.coursemodule, + this.summaryQuestions = await AddonModQuiz.getAttemptSummary(this.attempt.id, this.preflightData, { + cmId: this.quiz.coursemodule, loadLocal: this.offline, readingStrategy: this.offline ? CoreSitesReadingStrategy.PREFER_CACHE : CoreSitesReadingStrategy.ONLY_NETWORK, }); this.showSummary = true; - this.canReturn = this.attempt!.state == AddonModQuizProvider.ATTEMPT_IN_PROGRESS && !this.attempt!.finishedOffline; + this.canReturn = this.attempt.state == AddonModQuizProvider.ATTEMPT_IN_PROGRESS && !this.attempt.finishedOffline; this.preventSubmitMessages = AddonModQuiz.getPreventSubmitMessages(this.summaryQuestions); - this.dueDateWarning = AddonModQuiz.getAttemptDueDateWarning(this.quiz!, this.attempt!); + this.dueDateWarning = AddonModQuiz.getAttemptDueDateWarning(this.quiz, this.attempt); // Log summary as viewed. CoreUtils.ignoreErrors( - AddonModQuiz.logViewAttemptSummary(this.attempt!.id, this.preflightData, this.quiz!.id, this.quiz!.name), + AddonModQuiz.logViewAttemptSummary(this.attempt.id, this.preflightData, this.quiz.id, this.quiz.name), ); } @@ -558,9 +578,13 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave { * @returns Promise resolved when done. */ protected async loadNavigation(): Promise { + if (!this.attempt) { + return; + } + // We use the attempt summary to build the navigation because it contains all the questions. - this.navigation = await AddonModQuiz.getAttemptSummary(this.attempt!.id, this.preflightData, { - cmId: this.quiz!.coursemodule, + this.navigation = await AddonModQuiz.getAttemptSummary(this.attempt.id, this.preflightData, { + cmId: this.quiz?.coursemodule, loadLocal: this.offline, readingStrategy: this.offline ? CoreSitesReadingStrategy.PREFER_CACHE : CoreSitesReadingStrategy.ONLY_NETWORK, }); @@ -602,21 +626,22 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave { return; } - this.changePage(modalData.page!, true, modalData.slot); + this.changePage(modalData.page, true, modalData.slot); } /** * Prepare the answers to be sent for the attempt. * + * @param componentId Component ID. * @returns Promise resolved with the answers. */ - protected prepareAnswers(): Promise { + protected prepareAnswers(componentId: number): Promise { return CoreQuestionHelper.prepareAnswers( this.questions, this.getAnswers(), this.offline, this.component, - this.quiz!.coursemodule, + componentId, ); } @@ -629,18 +654,22 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave { * @returns Promise resolved when done. */ protected async processAttempt(userFinish?: boolean, timeUp?: boolean, retrying?: boolean): Promise { + if (!this.quiz || !this.attempt) { + return; + } + // Get the answers to send. let answers: CoreQuestionsAnswers = {}; if (!this.showSummary) { - answers = await this.prepareAnswers(); + answers = await this.prepareAnswers(this.quiz.coursemodule); } try { // Send the answers. await AddonModQuiz.processAttempt( - this.quiz!, - this.attempt!, + this.quiz, + this.attempt, answers, this.preflightData, userFinish, @@ -676,7 +705,7 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave { CoreForms.triggerFormSubmittedEvent(this.formElement, !this.offline, CoreSites.getCurrentSiteId()); } - return CoreQuestionHelper.clearTmpData(this.questions, this.component, this.quiz!.coursemodule); + return CoreQuestionHelper.clearTmpData(this.questions, this.component, this.quiz.coursemodule); } /** @@ -731,12 +760,16 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave { * @returns Promise resolved when done. */ protected async startOrContinueAttempt(): Promise { + if (!this.quiz || !this.quizAccessInfo) { + return; + } + let attempt = this.newAttempt ? undefined : this.lastAttempt; // Get the preflight data and start attempt if needed. attempt = await AddonModQuizHelper.getAndCheckPreflightData( - this.quiz!, - this.quizAccessInfo!, + this.quiz, + this.quizAccessInfo, this.preflightData, attempt, this.offline, @@ -745,8 +778,8 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave { ); // Re-fetch attempt access information with the right attempt (might have changed because a new attempt was created). - this.attemptAccessInfo = await AddonModQuiz.getAttemptAccessInformation(this.quiz!.id, attempt.id, { - cmId: this.quiz!.coursemodule, + this.attemptAccessInfo = await AddonModQuiz.getAttemptAccessInformation(this.quiz.id, attempt.id, { + cmId: this.quiz.coursemodule, readingStrategy: this.offline ? CoreSitesReadingStrategy.PREFER_CACHE : CoreSitesReadingStrategy.ONLY_NETWORK, }); @@ -756,7 +789,7 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave { if (this.attempt.state != AddonModQuizProvider.ATTEMPT_OVERDUE && !this.attempt.finishedOffline) { // Attempt not overdue and not finished in offline, load page. - await this.loadPage(this.attempt.currentpage!); + await this.loadPage(this.attempt.currentpage ?? 0); this.initTimer(); } else { diff --git a/src/addons/mod/quiz/pages/review/review.page.ts b/src/addons/mod/quiz/pages/review/review.page.ts index adb1b02d9..a5c789a0c 100644 --- a/src/addons/mod/quiz/pages/review/review.page.ts +++ b/src/addons/mod/quiz/pages/review/review.page.ts @@ -112,7 +112,7 @@ export class AddonModQuizReviewPage implements OnInit { * @param slot Slot of the question to scroll to. */ async changePage(page: number, slot?: number): Promise { - if (slot !== undefined && (this.attempt!.currentpage == -1 || page == this.currentPage)) { + if (slot !== undefined && (this.attempt?.currentpage == -1 || page == this.currentPage)) { // Scrol to a certain question in the current page. this.scrollToQuestion(slot); @@ -174,7 +174,7 @@ export class AddonModQuizReviewPage implements OnInit { * @returns Promise resolved when done. */ protected async loadPage(page: number): Promise { - const data = await AddonModQuiz.getAttemptReview(this.attemptId, { page, cmId: this.quiz!.coursemodule }); + const data = await AddonModQuiz.getAttemptReview(this.attemptId, { page, cmId: this.quiz?.coursemodule }); this.attempt = data.attempt; this.attempt.currentpage = page; @@ -203,7 +203,7 @@ export class AddonModQuizReviewPage implements OnInit { */ protected async loadNavigation(): Promise { // Get all questions in single page to retrieve all the questions. - const data = await AddonModQuiz.getAttemptReview(this.attemptId, { page: -1, cmId: this.quiz!.coursemodule }); + const data = await AddonModQuiz.getAttemptReview(this.attemptId, { page: -1, cmId: this.quiz?.coursemodule }); this.navigation = data.questions; @@ -260,7 +260,7 @@ export class AddonModQuizReviewPage implements OnInit { return; } - this.readableState = AddonModQuiz.getAttemptReadableStateName(this.attempt!.state || ''); + this.readableState = AddonModQuiz.getAttemptReadableStateName(this.attempt.state ?? ''); if (this.attempt.state != AddonModQuizProvider.ATTEMPT_FINISHED) { return; @@ -283,7 +283,7 @@ export class AddonModQuizReviewPage implements OnInit { } // Treat grade. - if (this.options!.someoptions.marks >= AddonModQuizProvider.QUESTION_OPTIONS_MARK_AND_MAX && + if (this.options && this.options.someoptions.marks >= AddonModQuizProvider.QUESTION_OPTIONS_MARK_AND_MAX && AddonModQuiz.quizHasGrades(this.quiz)) { if (data.grade === null || data.grade === undefined) { @@ -348,7 +348,7 @@ export class AddonModQuizReviewPage implements OnInit { return; } - this.changePage(modalData.page!, modalData.slot); + this.changePage(modalData.page, modalData.slot); } } diff --git a/src/addons/mod/quiz/services/handlers/prefetch.ts b/src/addons/mod/quiz/services/handlers/prefetch.ts index cb2da0a9c..3457eb277 100644 --- a/src/addons/mod/quiz/services/handlers/prefetch.ts +++ b/src/addons/mod/quiz/services/handlers/prefetch.ts @@ -241,7 +241,7 @@ export class AddonModQuizPrefetchHandlerService extends CoreCourseActivityPrefet const isLastFinished = !attempts.length || AddonModQuiz.isAttemptFinished(attempts[attempts.length - 1].state); - return quiz.attempts === 0 || quiz.attempts! > attempts.length || !isLastFinished; + return quiz.attempts === 0 || (quiz.attempts ?? 0) > attempts.length || !isLastFinished; } /** @@ -444,7 +444,7 @@ export class AddonModQuizPrefetchHandlerService extends CoreCourseActivityPrefet if (attempt.state == AddonModQuizProvider.ATTEMPT_IN_PROGRESS) { // Get data for each page. promises = promises.concat(pages.map(async (page) => { - if (isSequential && page < attempt.currentpage!) { + if (isSequential && attempt.currentpage && page < attempt.currentpage) { // Sequential quiz, cannot get pages before the current one. return; } diff --git a/src/addons/mod/quiz/services/handlers/push-click.ts b/src/addons/mod/quiz/services/handlers/push-click.ts index 70f59024a..d8c916ed7 100644 --- a/src/addons/mod/quiz/services/handlers/push-click.ts +++ b/src/addons/mod/quiz/services/handlers/push-click.ts @@ -43,7 +43,7 @@ export class AddonModQuizPushClickHandlerService implements CorePushNotification */ async handles(notification: AddonModQuizPushNotificationData): Promise { return CoreUtils.isTrueOrOne(notification.notif) && notification.moodlecomponent == 'mod_quiz' && - this.SUPPORTED_NAMES.indexOf(notification.name!) != -1; + this.SUPPORTED_NAMES.indexOf(notification.name ?? '') != -1; } /** diff --git a/src/addons/mod/quiz/services/quiz-helper.ts b/src/addons/mod/quiz/services/quiz-helper.ts index 66496ced3..cba2dcf1f 100644 --- a/src/addons/mod/quiz/services/quiz-helper.ts +++ b/src/addons/mod/quiz/services/quiz-helper.ts @@ -358,7 +358,7 @@ export class AddonModQuizHelperProvider { if (attempt) { if (attempt.state != AddonModQuizProvider.ATTEMPT_OVERDUE && !attempt.finishedOffline) { // We're continuing an attempt. Call getAttemptData to validate the preflight data. - await AddonModQuiz.getAttemptData(attempt.id, attempt.currentpage!, preflightData, modOptions); + await AddonModQuiz.getAttemptData(attempt.id, attempt.currentpage ?? 0, preflightData, modOptions); if (offline) { // Get current page stored in local. diff --git a/src/addons/mod/quiz/services/quiz-offline.ts b/src/addons/mod/quiz/services/quiz-offline.ts index c14ed939d..ed477be09 100644 --- a/src/addons/mod/quiz/services/quiz-offline.ts +++ b/src/addons/mod/quiz/services/quiz-offline.ts @@ -198,11 +198,11 @@ export class AddonModQuizOfflineProvider { } else { entry = { quizid: quiz.id, - userid: attempt.userid!, + userid: attempt.userid ?? CoreSites.getCurrentSiteUserId(), id: attempt.id, courseid: quiz.course, timecreated: now, - attempt: attempt.attempt!, + attempt: attempt.attempt ?? 0, currentpage: attempt.currentpage, timemodified: now, finished: finish ? 1 : 0, @@ -284,10 +284,12 @@ export class AddonModQuizOfflineProvider { if (questions[slot]) { if (!questionsWithAnswers[slot]) { - questionsWithAnswers[slot] = questions[slot]; - questionsWithAnswers[slot].answers = {}; + questionsWithAnswers[slot] = { + ...questions[slot], + answers: {}, + }; } - questionsWithAnswers[slot].answers![nameWithoutPrefix] = answers[name]; + questionsWithAnswers[slot].answers[nameWithoutPrefix] = answers[name]; } } @@ -295,7 +297,7 @@ export class AddonModQuizOfflineProvider { await Promise.all(Object.values(questionsWithAnswers).map(async (question) => { const state = await CoreQuestionBehaviourDelegate.determineNewState( - quiz.preferredbehaviour!, + quiz.preferredbehaviour ?? '', AddonModQuizProvider.COMPONENT, attempt.id, question, @@ -317,7 +319,7 @@ export class AddonModQuizOfflineProvider { AddonModQuizProvider.COMPONENT, quiz.id, attempt.id, - attempt.userid!, + attempt.userid ?? CoreSites.getCurrentSiteUserId(), answers, timeMod, siteId, @@ -332,7 +334,7 @@ export class AddonModQuizOfflineProvider { AddonModQuizProvider.COMPONENT, quiz.id, attempt.id, - attempt.userid!, + attempt.userid ?? CoreSites.getCurrentSiteUserId(), question, newStates[slot], siteId, diff --git a/src/addons/mod/quiz/services/quiz-sync.ts b/src/addons/mod/quiz/services/quiz-sync.ts index e6f799dc8..4b6a573ae 100644 --- a/src/addons/mod/quiz/services/quiz-sync.ts +++ b/src/addons/mod/quiz/services/quiz-sync.ts @@ -305,8 +305,9 @@ export class AddonModQuizSyncProvider extends CoreCourseActivitySyncBaseProvider // Get all the offline attempts for the quiz. It should always be 0 or 1 attempt const offlineAttempts = await AddonModQuizOffline.getQuizAttempts(quiz.id, siteId); + const offlineAttempt = offlineAttempts.pop(); - if (!offlineAttempts.length) { + if (!offlineAttempt) { // Nothing to sync, finish. return this.finishSync(siteId, quiz, courseId, warnings); } @@ -316,8 +317,6 @@ export class AddonModQuizSyncProvider extends CoreCourseActivitySyncBaseProvider throw new CoreError(Translate.instant('core.cannotconnect')); } - const offlineAttempt = offlineAttempts.pop()!; - // Now get the list of online attempts to make sure this attempt exists and isn't finished. const onlineAttempts = await AddonModQuiz.getUserAttempts(quiz.id, modOptions); diff --git a/src/addons/mod/quiz/services/quiz.ts b/src/addons/mod/quiz/services/quiz.ts index 88ba291fd..4ee1a7711 100644 --- a/src/addons/mod/quiz/services/quiz.ts +++ b/src/addons/mod/quiz/services/quiz.ts @@ -290,7 +290,7 @@ export class AddonModQuizProvider { return dueDate * 1000; case AddonModQuizProvider.ATTEMPT_OVERDUE: - return (dueDate + quiz.graceperiod!) * 1000; + return (dueDate + (quiz.graceperiod ?? 0)) * 1000; default: this.logger.warn('Unexpected state when getting due date: ' + attempt.state); @@ -356,7 +356,7 @@ export class AddonModQuizProvider { Translate.instant('addon.mod_quiz.statefinished'), Translate.instant( 'addon.mod_quiz.statefinisheddetails', - { $a: CoreTimeUtils.userDate(attempt.timefinish! * 1000) }, + { $a: CoreTimeUtils.userDate((attempt.timefinish ?? 0) * 1000) }, ), ]; @@ -625,7 +625,7 @@ export class AddonModQuizProvider { } if (quiz.questiondecimalpoints == -1) { - return quiz.decimalpoints!; + return quiz.decimalpoints ?? 1; } return quiz.questiondecimalpoints; @@ -1780,7 +1780,7 @@ export class AddonModQuizProvider { * @returns Whether quiz is graded. */ quizHasGrades(quiz: AddonModQuizQuizWSData): boolean { - return quiz.grade! >= 0.000005 && quiz.sumgrades! >= 0.000005; + return (quiz.grade ?? 0) >= 0.000005 && (quiz.sumgrades ?? 0) >= 0.000005; } /** @@ -1802,8 +1802,8 @@ export class AddonModQuizProvider { const rawGradeNum = typeof rawGrade == 'string' ? parseFloat(rawGrade) : rawGrade; if (rawGradeNum !== undefined && rawGradeNum !== null && !isNaN(rawGradeNum)) { - if (quiz.sumgrades! >= 0.000005) { - grade = rawGradeNum * quiz.grade! / quiz.sumgrades!; + if (quiz.sumgrades && quiz.sumgrades >= 0.000005) { + grade = rawGradeNum * (quiz.grade ?? 0) / quiz.sumgrades; } else { grade = 0; } @@ -1816,7 +1816,7 @@ export class AddonModQuizProvider { if (format === 'question') { return this.formatGrade(grade, this.getGradeDecimals(quiz)); } else if (format) { - return this.formatGrade(grade, quiz.decimalpoints!); + return this.formatGrade(grade, quiz.decimalpoints ?? 1); } return String(grade); diff --git a/src/core/features/question/services/behaviour-delegate.ts b/src/core/features/question/services/behaviour-delegate.ts index 51d2bab74..cbe9856fa 100644 --- a/src/core/features/question/services/behaviour-delegate.ts +++ b/src/core/features/question/services/behaviour-delegate.ts @@ -132,5 +132,5 @@ export const CoreQuestionBehaviourDelegate = makeSingleton(CoreQuestionBehaviour * Answers classified by question slot. */ export type CoreQuestionQuestionWithAnswers = CoreQuestionQuestionParsed & { - answers?: CoreQuestionsAnswers; + answers: CoreQuestionsAnswers; };