MOBILE-4263 quiz: Remove non-null assertions

main
Dani Palou 2023-03-02 12:01:52 +01:00
parent 4792e47737
commit 9744eb5137
13 changed files with 143 additions and 97 deletions

View File

@ -333,15 +333,16 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
return; return;
} }
const bestGrade = this.bestGrade.grade;
const formattedGradebookGrade = AddonModQuiz.formatGrade(this.gradebookData.grade, quiz.decimalpoints); 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. let gradeToShow = formattedGradebookGrade; // By default we show the grade in the gradebook.
this.showResults = true; this.showResults = true;
this.gradeOverridden = formattedGradebookGrade != formattedBestGrade; this.gradeOverridden = formattedGradebookGrade != formattedBestGrade;
this.gradebookFeedback = this.gradebookData.feedback; 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. // 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. // We'll do like Moodle web and show the best grade instead of the gradebook grade.
this.gradeOverridden = false; this.gradeOverridden = false;
@ -556,8 +557,16 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
* *
* @returns Promise resolved when done. * @returns Promise resolved when done.
*/ */
protected sync(): Promise<AddonModQuizSyncResult> { protected async sync(): Promise<AddonModQuizSyncResult> {
return AddonModQuizSync.syncQuiz(this.candidateQuiz!, true); 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 lastFinished = AddonModQuiz.getLastFinishedAttemptFromList(attempts);
const promises: Promise<unknown>[] = []; let openReview = false;
if (this.autoReview && lastFinished && lastFinished.id >= this.autoReview.attemptId) { 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. // 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. // Go to the review of this attempt if the user hasn't left this view.
if (!this.isDestroyed && this.isCurrentView) { if (!this.isDestroyed && this.isCurrentView) {
promises.push(this.goToAutoReview()); openReview = true;
} }
this.autoReview = undefined; this.autoReview = undefined;
} }
// Get combined review options. const [options] = await Promise.all([
promises.push(AddonModQuiz.getCombinedReviewOptions(quiz.id, { cmId: this.module.id }).then((options) => { AddonModQuiz.getCombinedReviewOptions(quiz.id, { cmId: this.module.id }),
this.options = options; this.getQuizGrade(),
openReview ? this.goToAutoReview() : undefined,
return; ]);
}));
// Get best grade.
promises.push(this.getQuizGrade());
await Promise.all(promises);
this.options = options;
const grade = this.gradebookData?.grade !== undefined ? this.gradebookData.grade : this.bestGrade?.grade; const grade = this.gradebookData?.grade !== undefined ? this.gradebookData.grade : this.bestGrade?.grade;
const quizGrade = AddonModQuiz.formatGrade(grade, quiz.decimalpoints); const quizGrade = AddonModQuiz.formatGrade(grade, quiz.decimalpoints);
// Calculate data to construct the header of the attempts table. // 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. // Calculate data to show for each attempt.
const formattedAttempts = await Promise.all(attempts.map((attempt, index) => { const formattedAttempts = await Promise.all(attempts.map((attempt, index) => {

View File

@ -61,6 +61,6 @@ export type AddonModQuizNavigationQuestion = CoreQuestionQuestionParsed & {
}; };
export type AddonModQuizNavigationModalReturn = { export type AddonModQuizNavigationModalReturn = {
page?: number; page: number;
slot?: number; slot?: number;
}; };

View File

@ -66,11 +66,13 @@ export class AddonModQuizPreflightModalComponent implements OnInit {
} }
try { try {
const quiz = this.quiz;
await Promise.all(this.rules.map(async (rule) => { await Promise.all(this.rules.map(async (rule) => {
// Check if preflight is required for rule and, if so, get the component to render it. // Check if preflight is required for rule and, if so, get the component to render it.
const required = await AddonModQuizAccessRuleDelegate.isPreflightCheckRequiredForRule( const required = await AddonModQuizAccessRuleDelegate.isPreflightCheckRequiredForRule(
rule, rule,
this.quiz!, quiz,
this.attempt, this.attempt,
this.prefetch, this.prefetch,
this.siteId, this.siteId,

View File

@ -94,20 +94,20 @@ export class AddonModQuizAttemptPage implements OnInit {
// Load attempt data. // Load attempt data.
const [options, accessInfo, attempt] = await Promise.all([ const [options, accessInfo, attempt] = await Promise.all([
AddonModQuiz.getCombinedReviewOptions(this.quiz.id, { cmId: this.quiz.coursemodule }), AddonModQuiz.getCombinedReviewOptions(this.quiz.id, { cmId: this.quiz.coursemodule }),
this.fetchAccessInfo(), this.fetchAccessInfo(this.quiz),
this.fetchAttempt(), this.fetchAttempt(this.quiz.id),
]); ]);
// Set calculated data. // Set calculated data.
this.showReviewColumn = accessInfo.canreviewmyattempts; this.showReviewColumn = accessInfo.canreviewmyattempts;
AddonModQuizHelper.setQuizCalculatedData(this.quiz, options); 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. // 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)) { options.someoptions.overallfeedback && !isNaN(grade)) {
// Feedback should be displayed, get the feedback for the grade. // Feedback should be displayed, get the feedback for the grade.
@ -127,11 +127,12 @@ export class AddonModQuizAttemptPage implements OnInit {
/** /**
* Get the attempt. * Get the attempt.
* *
* @param quizId Quiz ID.
* @returns Promise resolved when done. * @returns Promise resolved when done.
*/ */
protected async fetchAttempt(): Promise<AddonModQuizAttemptWSData> { protected async fetchAttempt(quizId: number): Promise<AddonModQuizAttemptWSData> {
// Get all the attempts and search the one we want. // 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); const attempt = attempts.find(attempt => attempt.id == this.attemptId);
@ -148,10 +149,11 @@ export class AddonModQuizAttemptPage implements OnInit {
/** /**
* Get the access info. * Get the access info.
* *
* @param quiz Quiz instance.
* @returns Promise resolved when done. * @returns Promise resolved when done.
*/ */
protected async fetchAccessInfo(): Promise<AddonModQuizGetQuizAccessInformationWSResponse> { protected async fetchAccessInfo(quiz: AddonModQuizQuizData): Promise<AddonModQuizGetQuizAccessInformationWSResponse> {
const accessInfo = await AddonModQuiz.getQuizAccessInformation(this.quiz!.id, { cmId: this.cmId }); const accessInfo = await AddonModQuiz.getQuizAccessInformation(quiz.id, { cmId: this.cmId });
if (!accessInfo.canreviewmyattempts) { if (!accessInfo.canreviewmyattempts) {
return accessInfo; return accessInfo;
@ -161,7 +163,7 @@ export class AddonModQuizAttemptPage implements OnInit {
await CoreUtils.ignoreErrors(AddonModQuiz.invalidateAttemptReviewForPage(this.attemptId, -1)); await CoreUtils.ignoreErrors(AddonModQuiz.invalidateAttemptReviewForPage(this.attemptId, -1));
try { try {
await AddonModQuiz.getAttemptReview(this.attemptId, { page: -1, cmId: this.quiz!.coursemodule }); await AddonModQuiz.getAttemptReview(this.attemptId, { page: -1, cmId: quiz.coursemodule });
} catch { } catch {
// Error getting the review, assume the user cannot review the attempt. // Error getting the review, assume the user cannot review the attempt.
accessInfo.canreviewmyattempts = false; accessInfo.canreviewmyattempts = false;
@ -202,7 +204,11 @@ export class AddonModQuizAttemptPage implements OnInit {
* @returns Promise resolved when done. * @returns Promise resolved when done.
*/ */
async reviewAttempt(): Promise<void> { async reviewAttempt(): Promise<void> {
CoreNavigator.navigate(`../../review/${this.attempt!.id}`); if (!this.attempt) {
return;
}
CoreNavigator.navigate(`../../review/${this.attempt.id}`);
} }
} }

View File

@ -203,6 +203,10 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave {
* @param button Clicked button. * @param button Clicked button.
*/ */
async behaviourButtonClicked(button: CoreQuestionBehaviourButton): Promise<void> { async behaviourButtonClicked(button: CoreQuestionBehaviourButton): Promise<void> {
if (!this.quiz || !this.attempt) {
return;
}
let modal: CoreIonLoadingElement | undefined; let modal: CoreIonLoadingElement | undefined;
try { try {
@ -212,13 +216,13 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave {
modal = await CoreDomUtils.showModalLoading('core.sending', true); modal = await CoreDomUtils.showModalLoading('core.sending', true);
// Get the answers. // Get the answers.
const answers = await this.prepareAnswers(); const answers = await this.prepareAnswers(this.quiz.coursemodule);
// Add the clicked button data. // Add the clicked button data.
answers[button.name] = button.value; answers[button.name] = button.value;
// Behaviour checks are always in online. // 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. 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. this.content?.scrollToTop(); // Scroll top so the spinner is seen.
try { try {
await this.loadPage(this.attempt!.currentpage!); await this.loadPage(this.attempt.currentpage ?? 0);
} finally { } finally {
this.loaded = true; this.loaded = true;
if (scrollTop != -1) { if (scrollTop != -1) {
@ -311,8 +315,8 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave {
} }
} catch (error) { } catch (error) {
// If the user isn't seeing the summary, start the check again. // If the user isn't seeing the summary, start the check again.
if (!this.showSummary) { if (!this.showSummary && this.quiz) {
this.autoSave.startCheckChangesProcess(this.quiz!, this.attempt, this.preflightData, this.offline); this.autoSave.startCheckChangesProcess(this.quiz, this.attempt, this.preflightData, this.offline);
} }
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_quiz.errorgetquestions', true); 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); 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); this.readableTimeLimit = CoreTime.formatTime(this.quiz.timelimit);
} }
@ -394,11 +398,15 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave {
* @returns Promise resolved when done. * @returns Promise resolved when done.
*/ */
async finishAttempt(userFinish?: boolean, timeUp?: boolean): Promise<void> { async finishAttempt(userFinish?: boolean, timeUp?: boolean): Promise<void> {
if (!this.quiz || !this.attempt) {
return;
}
let modal: CoreIonLoadingElement | undefined; let modal: CoreIonLoadingElement | undefined;
try { try {
// Show confirm if the user clicked the finish button and the quiz is in progress. // 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')); 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. // Trigger an event to notify the attempt was finished.
CoreEvents.trigger(AddonModQuizProvider.ATTEMPT_FINISHED_EVENT, { CoreEvents.trigger(AddonModQuizProvider.ATTEMPT_FINISHED_EVENT, {
quizId: this.quiz!.id, quizId: this.quiz.id,
attemptId: this.attempt!.id, attemptId: this.attempt.id,
synced: !this.offline, synced: !this.offline,
}, CoreSites.getCurrentSiteId()); }, CoreSites.getCurrentSiteId());
@ -431,9 +439,13 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave {
* @returns Promise resolved when done. * @returns Promise resolved when done.
*/ */
protected async fixSequenceChecks(): Promise<void> { protected async fixSequenceChecks(): Promise<void> {
if (!this.attempt) {
return;
}
// Get current page data again to get the latest sequencechecks. // Get current page data again to get the latest sequencechecks.
const data = await AddonModQuiz.getAttemptData(this.attempt!.id, this.attempt!.currentpage!, this.preflightData, { const data = await AddonModQuiz.getAttemptData(this.attempt.id, this.attempt.currentpage ?? 0, this.preflightData, {
cmId: this.quiz!.coursemodule, cmId: this.quiz?.coursemodule,
readingStrategy: this.offline ? CoreSitesReadingStrategy.PREFER_CACHE : CoreSitesReadingStrategy.ONLY_NETWORK, 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. * Initializes the timer if enabled.
*/ */
protected initTimer(): void { protected initTimer(): void {
if (!this.attemptAccessInfo?.endtime || this.attemptAccessInfo.endtime < 0) { if (!this.quizAccessInfo || !this.attempt || !this.attemptAccessInfo?.endtime || this.attemptAccessInfo.endtime < 0) {
return; return;
} }
// Quiz has an end time. Check if time left should be shown. // Quiz has an end time. Check if time left should be shown.
const shouldShowTime = AddonModQuiz.shouldShowTimeLeft( const shouldShowTime = AddonModQuiz.shouldShowTimeLeft(
this.quizAccessInfo!.activerulenames, this.quizAccessInfo.activerulenames,
this.attempt!, this.attempt,
this.attemptAccessInfo.endtime, this.attemptAccessInfo.endtime,
); );
@ -490,8 +502,12 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave {
* @returns Promise resolved when done. * @returns Promise resolved when done.
*/ */
protected async loadPage(page: number): Promise<void> { protected async loadPage(page: number): Promise<void> {
const data = await AddonModQuiz.getAttemptData(this.attempt!.id, page, this.preflightData, { if (!this.quiz || !this.attempt) {
cmId: this.quiz!.coursemodule, 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, readingStrategy: this.offline ? CoreSitesReadingStrategy.PREFER_CACHE : CoreSitesReadingStrategy.ONLY_NETWORK,
}); });
@ -523,7 +539,7 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave {
); );
// Start looking for changes. // 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. * @returns Promise resolved when done.
*/ */
protected async loadSummary(): Promise<void> { protected async loadSummary(): Promise<void> {
if (!this.quiz || !this.attempt) {
return;
}
this.summaryQuestions = []; this.summaryQuestions = [];
this.summaryQuestions = await AddonModQuiz.getAttemptSummary(this.attempt!.id, this.preflightData, { this.summaryQuestions = await AddonModQuiz.getAttemptSummary(this.attempt.id, this.preflightData, {
cmId: this.quiz!.coursemodule, cmId: this.quiz.coursemodule,
loadLocal: this.offline, loadLocal: this.offline,
readingStrategy: this.offline ? CoreSitesReadingStrategy.PREFER_CACHE : CoreSitesReadingStrategy.ONLY_NETWORK, readingStrategy: this.offline ? CoreSitesReadingStrategy.PREFER_CACHE : CoreSitesReadingStrategy.ONLY_NETWORK,
}); });
this.showSummary = true; 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.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. // Log summary as viewed.
CoreUtils.ignoreErrors( 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. * @returns Promise resolved when done.
*/ */
protected async loadNavigation(): Promise<void> { protected async loadNavigation(): Promise<void> {
if (!this.attempt) {
return;
}
// We use the attempt summary to build the navigation because it contains all the questions. // 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, { this.navigation = await AddonModQuiz.getAttemptSummary(this.attempt.id, this.preflightData, {
cmId: this.quiz!.coursemodule, cmId: this.quiz?.coursemodule,
loadLocal: this.offline, loadLocal: this.offline,
readingStrategy: this.offline ? CoreSitesReadingStrategy.PREFER_CACHE : CoreSitesReadingStrategy.ONLY_NETWORK, readingStrategy: this.offline ? CoreSitesReadingStrategy.PREFER_CACHE : CoreSitesReadingStrategy.ONLY_NETWORK,
}); });
@ -602,21 +626,22 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave {
return; return;
} }
this.changePage(modalData.page!, true, modalData.slot); this.changePage(modalData.page, true, modalData.slot);
} }
/** /**
* Prepare the answers to be sent for the attempt. * Prepare the answers to be sent for the attempt.
* *
* @param componentId Component ID.
* @returns Promise resolved with the answers. * @returns Promise resolved with the answers.
*/ */
protected prepareAnswers(): Promise<CoreQuestionsAnswers> { protected prepareAnswers(componentId: number): Promise<CoreQuestionsAnswers> {
return CoreQuestionHelper.prepareAnswers( return CoreQuestionHelper.prepareAnswers(
this.questions, this.questions,
this.getAnswers(), this.getAnswers(),
this.offline, this.offline,
this.component, this.component,
this.quiz!.coursemodule, componentId,
); );
} }
@ -629,18 +654,22 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave {
* @returns Promise resolved when done. * @returns Promise resolved when done.
*/ */
protected async processAttempt(userFinish?: boolean, timeUp?: boolean, retrying?: boolean): Promise<void> { protected async processAttempt(userFinish?: boolean, timeUp?: boolean, retrying?: boolean): Promise<void> {
if (!this.quiz || !this.attempt) {
return;
}
// Get the answers to send. // Get the answers to send.
let answers: CoreQuestionsAnswers = {}; let answers: CoreQuestionsAnswers = {};
if (!this.showSummary) { if (!this.showSummary) {
answers = await this.prepareAnswers(); answers = await this.prepareAnswers(this.quiz.coursemodule);
} }
try { try {
// Send the answers. // Send the answers.
await AddonModQuiz.processAttempt( await AddonModQuiz.processAttempt(
this.quiz!, this.quiz,
this.attempt!, this.attempt,
answers, answers,
this.preflightData, this.preflightData,
userFinish, userFinish,
@ -676,7 +705,7 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave {
CoreForms.triggerFormSubmittedEvent(this.formElement, !this.offline, CoreSites.getCurrentSiteId()); 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. * @returns Promise resolved when done.
*/ */
protected async startOrContinueAttempt(): Promise<void> { protected async startOrContinueAttempt(): Promise<void> {
if (!this.quiz || !this.quizAccessInfo) {
return;
}
let attempt = this.newAttempt ? undefined : this.lastAttempt; let attempt = this.newAttempt ? undefined : this.lastAttempt;
// Get the preflight data and start attempt if needed. // Get the preflight data and start attempt if needed.
attempt = await AddonModQuizHelper.getAndCheckPreflightData( attempt = await AddonModQuizHelper.getAndCheckPreflightData(
this.quiz!, this.quiz,
this.quizAccessInfo!, this.quizAccessInfo,
this.preflightData, this.preflightData,
attempt, attempt,
this.offline, 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). // 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, { this.attemptAccessInfo = await AddonModQuiz.getAttemptAccessInformation(this.quiz.id, attempt.id, {
cmId: this.quiz!.coursemodule, cmId: this.quiz.coursemodule,
readingStrategy: this.offline ? CoreSitesReadingStrategy.PREFER_CACHE : CoreSitesReadingStrategy.ONLY_NETWORK, 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) { if (this.attempt.state != AddonModQuizProvider.ATTEMPT_OVERDUE && !this.attempt.finishedOffline) {
// Attempt not overdue and not finished in offline, load page. // 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(); this.initTimer();
} else { } else {

View File

@ -112,7 +112,7 @@ export class AddonModQuizReviewPage implements OnInit {
* @param slot Slot of the question to scroll to. * @param slot Slot of the question to scroll to.
*/ */
async changePage(page: number, slot?: number): Promise<void> { async changePage(page: number, slot?: number): Promise<void> {
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. // Scrol to a certain question in the current page.
this.scrollToQuestion(slot); this.scrollToQuestion(slot);
@ -174,7 +174,7 @@ export class AddonModQuizReviewPage implements OnInit {
* @returns Promise resolved when done. * @returns Promise resolved when done.
*/ */
protected async loadPage(page: number): Promise<void> { protected async loadPage(page: number): Promise<void> {
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 = data.attempt;
this.attempt.currentpage = page; this.attempt.currentpage = page;
@ -203,7 +203,7 @@ export class AddonModQuizReviewPage implements OnInit {
*/ */
protected async loadNavigation(): Promise<void> { protected async loadNavigation(): Promise<void> {
// Get all questions in single page to retrieve all the questions. // 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; this.navigation = data.questions;
@ -260,7 +260,7 @@ export class AddonModQuizReviewPage implements OnInit {
return; return;
} }
this.readableState = AddonModQuiz.getAttemptReadableStateName(this.attempt!.state || ''); this.readableState = AddonModQuiz.getAttemptReadableStateName(this.attempt.state ?? '');
if (this.attempt.state != AddonModQuizProvider.ATTEMPT_FINISHED) { if (this.attempt.state != AddonModQuizProvider.ATTEMPT_FINISHED) {
return; return;
@ -283,7 +283,7 @@ export class AddonModQuizReviewPage implements OnInit {
} }
// Treat grade. // 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)) { AddonModQuiz.quizHasGrades(this.quiz)) {
if (data.grade === null || data.grade === undefined) { if (data.grade === null || data.grade === undefined) {
@ -348,7 +348,7 @@ export class AddonModQuizReviewPage implements OnInit {
return; return;
} }
this.changePage(modalData.page!, modalData.slot); this.changePage(modalData.page, modalData.slot);
} }
} }

View File

@ -241,7 +241,7 @@ export class AddonModQuizPrefetchHandlerService extends CoreCourseActivityPrefet
const isLastFinished = !attempts.length || AddonModQuiz.isAttemptFinished(attempts[attempts.length - 1].state); 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) { if (attempt.state == AddonModQuizProvider.ATTEMPT_IN_PROGRESS) {
// Get data for each page. // Get data for each page.
promises = promises.concat(pages.map(async (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. // Sequential quiz, cannot get pages before the current one.
return; return;
} }

View File

@ -43,7 +43,7 @@ export class AddonModQuizPushClickHandlerService implements CorePushNotification
*/ */
async handles(notification: AddonModQuizPushNotificationData): Promise<boolean> { async handles(notification: AddonModQuizPushNotificationData): Promise<boolean> {
return CoreUtils.isTrueOrOne(notification.notif) && notification.moodlecomponent == 'mod_quiz' && return CoreUtils.isTrueOrOne(notification.notif) && notification.moodlecomponent == 'mod_quiz' &&
this.SUPPORTED_NAMES.indexOf(notification.name!) != -1; this.SUPPORTED_NAMES.indexOf(notification.name ?? '') != -1;
} }
/** /**

View File

@ -358,7 +358,7 @@ export class AddonModQuizHelperProvider {
if (attempt) { if (attempt) {
if (attempt.state != AddonModQuizProvider.ATTEMPT_OVERDUE && !attempt.finishedOffline) { if (attempt.state != AddonModQuizProvider.ATTEMPT_OVERDUE && !attempt.finishedOffline) {
// We're continuing an attempt. Call getAttemptData to validate the preflight data. // 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) { if (offline) {
// Get current page stored in local. // Get current page stored in local.

View File

@ -198,11 +198,11 @@ export class AddonModQuizOfflineProvider {
} else { } else {
entry = { entry = {
quizid: quiz.id, quizid: quiz.id,
userid: attempt.userid!, userid: attempt.userid ?? CoreSites.getCurrentSiteUserId(),
id: attempt.id, id: attempt.id,
courseid: quiz.course, courseid: quiz.course,
timecreated: now, timecreated: now,
attempt: attempt.attempt!, attempt: attempt.attempt ?? 0,
currentpage: attempt.currentpage, currentpage: attempt.currentpage,
timemodified: now, timemodified: now,
finished: finish ? 1 : 0, finished: finish ? 1 : 0,
@ -284,10 +284,12 @@ export class AddonModQuizOfflineProvider {
if (questions[slot]) { if (questions[slot]) {
if (!questionsWithAnswers[slot]) { if (!questionsWithAnswers[slot]) {
questionsWithAnswers[slot] = questions[slot]; questionsWithAnswers[slot] = {
questionsWithAnswers[slot].answers = {}; ...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) => { await Promise.all(Object.values(questionsWithAnswers).map(async (question) => {
const state = await CoreQuestionBehaviourDelegate.determineNewState( const state = await CoreQuestionBehaviourDelegate.determineNewState(
quiz.preferredbehaviour!, quiz.preferredbehaviour ?? '',
AddonModQuizProvider.COMPONENT, AddonModQuizProvider.COMPONENT,
attempt.id, attempt.id,
question, question,
@ -317,7 +319,7 @@ export class AddonModQuizOfflineProvider {
AddonModQuizProvider.COMPONENT, AddonModQuizProvider.COMPONENT,
quiz.id, quiz.id,
attempt.id, attempt.id,
attempt.userid!, attempt.userid ?? CoreSites.getCurrentSiteUserId(),
answers, answers,
timeMod, timeMod,
siteId, siteId,
@ -332,7 +334,7 @@ export class AddonModQuizOfflineProvider {
AddonModQuizProvider.COMPONENT, AddonModQuizProvider.COMPONENT,
quiz.id, quiz.id,
attempt.id, attempt.id,
attempt.userid!, attempt.userid ?? CoreSites.getCurrentSiteUserId(),
question, question,
newStates[slot], newStates[slot],
siteId, siteId,

View File

@ -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 // 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 offlineAttempts = await AddonModQuizOffline.getQuizAttempts(quiz.id, siteId);
const offlineAttempt = offlineAttempts.pop();
if (!offlineAttempts.length) { if (!offlineAttempt) {
// Nothing to sync, finish. // Nothing to sync, finish.
return this.finishSync(siteId, quiz, courseId, warnings); return this.finishSync(siteId, quiz, courseId, warnings);
} }
@ -316,8 +317,6 @@ export class AddonModQuizSyncProvider extends CoreCourseActivitySyncBaseProvider
throw new CoreError(Translate.instant('core.cannotconnect')); 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. // 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); const onlineAttempts = await AddonModQuiz.getUserAttempts(quiz.id, modOptions);

View File

@ -290,7 +290,7 @@ export class AddonModQuizProvider {
return dueDate * 1000; return dueDate * 1000;
case AddonModQuizProvider.ATTEMPT_OVERDUE: case AddonModQuizProvider.ATTEMPT_OVERDUE:
return (dueDate + quiz.graceperiod!) * 1000; return (dueDate + (quiz.graceperiod ?? 0)) * 1000;
default: default:
this.logger.warn('Unexpected state when getting due date: ' + attempt.state); 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.statefinished'),
Translate.instant( Translate.instant(
'addon.mod_quiz.statefinisheddetails', '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) { if (quiz.questiondecimalpoints == -1) {
return quiz.decimalpoints!; return quiz.decimalpoints ?? 1;
} }
return quiz.questiondecimalpoints; return quiz.questiondecimalpoints;
@ -1780,7 +1780,7 @@ export class AddonModQuizProvider {
* @returns Whether quiz is graded. * @returns Whether quiz is graded.
*/ */
quizHasGrades(quiz: AddonModQuizQuizWSData): boolean { 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; const rawGradeNum = typeof rawGrade == 'string' ? parseFloat(rawGrade) : rawGrade;
if (rawGradeNum !== undefined && rawGradeNum !== null && !isNaN(rawGradeNum)) { if (rawGradeNum !== undefined && rawGradeNum !== null && !isNaN(rawGradeNum)) {
if (quiz.sumgrades! >= 0.000005) { if (quiz.sumgrades && quiz.sumgrades >= 0.000005) {
grade = rawGradeNum * quiz.grade! / quiz.sumgrades!; grade = rawGradeNum * (quiz.grade ?? 0) / quiz.sumgrades;
} else { } else {
grade = 0; grade = 0;
} }
@ -1816,7 +1816,7 @@ export class AddonModQuizProvider {
if (format === 'question') { if (format === 'question') {
return this.formatGrade(grade, this.getGradeDecimals(quiz)); return this.formatGrade(grade, this.getGradeDecimals(quiz));
} else if (format) { } else if (format) {
return this.formatGrade(grade, quiz.decimalpoints!); return this.formatGrade(grade, quiz.decimalpoints ?? 1);
} }
return String(grade); return String(grade);

View File

@ -132,5 +132,5 @@ export const CoreQuestionBehaviourDelegate = makeSingleton(CoreQuestionBehaviour
* Answers classified by question slot. * Answers classified by question slot.
*/ */
export type CoreQuestionQuestionWithAnswers = CoreQuestionQuestionParsed & { export type CoreQuestionQuestionWithAnswers = CoreQuestionQuestionParsed & {
answers?: CoreQuestionsAnswers; answers: CoreQuestionsAnswers;
}; };