From 07af88d5d929e780d22194d6e3f5c59650c0a169 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 1 Oct 2020 16:01:14 +0200 Subject: [PATCH] MOBILE-2272 quiz: Improve calculate essay state in offline --- .../deferredfeedback/providers/handler.ts | 2 +- .../manualgraded/providers/handler.ts | 111 +----------------- .../qtype/calculated/providers/handler.ts | 6 +- .../calculatedmulti/providers/handler.ts | 4 +- .../calculatedsimple/providers/handler.ts | 6 +- .../qtype/ddimageortext/providers/handler.ts | 4 +- src/addon/qtype/ddmarker/providers/handler.ts | 6 +- src/addon/qtype/ddwtos/providers/handler.ts | 4 +- src/addon/qtype/essay/providers/handler.ts | 18 ++- .../qtype/gapselect/providers/handler.ts | 4 +- src/addon/qtype/match/providers/handler.ts | 4 +- .../qtype/multianswer/providers/handler.ts | 4 +- .../qtype/multichoice/providers/handler.ts | 6 +- .../qtype/randomsamatch/providers/handler.ts | 6 +- .../qtype/shortanswer/providers/handler.ts | 4 +- .../qtype/truefalse/providers/handler.ts | 4 +- src/assets/lang/en.json | 2 +- .../question/classes/base-question-handler.ts | 4 +- src/core/question/lang/en.json | 2 +- src/core/question/providers/delegate.ts | 10 +- 20 files changed, 77 insertions(+), 134 deletions(-) diff --git a/src/addon/qbehaviour/deferredfeedback/providers/handler.ts b/src/addon/qbehaviour/deferredfeedback/providers/handler.ts index 9ae43b3ec..31f21252a 100644 --- a/src/addon/qbehaviour/deferredfeedback/providers/handler.ts +++ b/src/addon/qbehaviour/deferredfeedback/providers/handler.ts @@ -142,7 +142,7 @@ export class AddonQbehaviourDeferredFeedbackHandler implements CoreQuestionBehav } else if (complete > 0) { newState = 'complete'; } else { - const gradable = this.questionDelegate.isGradableResponse(question, newBasicAnswers); + const gradable = this.questionDelegate.isGradableResponse(question, newBasicAnswers, component, componentId); if (gradable < 0) { newState = 'cannotdeterminestatus'; } else if (gradable > 0) { diff --git a/src/addon/qbehaviour/manualgraded/providers/handler.ts b/src/addon/qbehaviour/manualgraded/providers/handler.ts index b12cb3e35..9a650d9d2 100644 --- a/src/addon/qbehaviour/manualgraded/providers/handler.ts +++ b/src/addon/qbehaviour/manualgraded/providers/handler.ts @@ -17,32 +17,7 @@ import { Injectable } from '@angular/core'; import { CoreQuestionBehaviourHandler } from '@core/question/providers/behaviour-delegate'; import { CoreQuestionDelegate } from '@core/question/providers/delegate'; import { CoreQuestionProvider, CoreQuestionState } from '@core/question/providers/question'; - -/** - * Check if a response is complete. - * - * @param question The question. - * @param answers Object with the question answers (without prefix). - * @param component The component the question is related to. - * @param componentId Component ID. - * @return 1 if complete, 0 if not complete, -1 if cannot determine. - */ -export type isCompleteResponseFunction = (question: any, answers: any, component: string, componentId: string | number) => number; - -/** - * Check if two responses are the same. - * - * @param question Question. - * @param prevAnswers Object with the previous question answers. - * @param prevBasicAnswers Object with the previous basic" answers (without sequencecheck, certainty, ...). - * @param newAnswers Object with the new question answers. - * @param newBasicAnswers Object with the previous basic" answers (without sequencecheck, certainty, ...). - * @param component The component the question is related to. - * @param componentId Component ID. - * @return Whether they're the same. - */ -export type isSameResponseFunction = (question: any, prevAnswers: any, prevBasicAnswers: any, newAnswers: any, - newBasicAnswers: any, component: string, componentId: string | number) => boolean; +import { AddonQbehaviourDeferredFeedbackHandler } from '@addon/qbehaviour/deferredfeedback/providers/handler'; /** * Handler to support manual graded question behaviour. @@ -52,7 +27,9 @@ export class AddonQbehaviourManualGradedHandler implements CoreQuestionBehaviour name = 'AddonQbehaviourManualGraded'; type = 'manualgraded'; - constructor(private questionDelegate: CoreQuestionDelegate, private questionProvider: CoreQuestionProvider) { + constructor(protected questionDelegate: CoreQuestionDelegate, + protected questionProvider: CoreQuestionProvider, + protected deferredFeedbackHandler: AddonQbehaviourDeferredFeedbackHandler) { // Nothing to do. } @@ -68,84 +45,8 @@ export class AddonQbehaviourManualGradedHandler implements CoreQuestionBehaviour */ determineNewState(component: string, attemptId: number, question: any, componentId: string | number, siteId?: string) : CoreQuestionState | Promise { - return this.determineNewStateManualGraded(component, attemptId, question, componentId, siteId); - } - - /** - * Determine a question new state based on its answer(s) for manual graded question behaviour. - * - * @param component Component the question belongs to. - * @param attemptId Attempt ID the question belongs to. - * @param question The question. - * @param componentId Component ID. - * @param siteId Site ID. If not defined, current site. - * @param isCompleteFn Function to override the default isCompleteResponse check. - * @param isSameFn Function to override the default isSameResponse check. - * @return Promise resolved with state. - */ - async determineNewStateManualGraded(component: string, attemptId: number, question: any, componentId: string | number, - siteId?: string, isCompleteFn?: isCompleteResponseFunction, isSameFn?: isSameResponseFunction) - : Promise { - - // Check if we have local data for the question. - let dbQuestion; - try { - dbQuestion = await this.questionProvider.getQuestion(component, attemptId, question.slot, siteId); - } catch (error) { - // No entry found, use the original data. - dbQuestion = question; - } - - const state = this.questionProvider.getState(dbQuestion.state); - - if (state.finished || !state.active) { - // Question is finished, it cannot change. - return state; - } - - const newBasicAnswers = this.questionProvider.getBasicAnswers(question.answers); - - if (dbQuestion.state) { - // Question already has a state stored. Check if answer has changed. - let prevAnswers = await this.questionProvider.getQuestionAnswers(component, attemptId, question.slot, false, siteId); - - prevAnswers = this.questionProvider.convertAnswersArrayToObject(prevAnswers, true); - const prevBasicAnswers = this.questionProvider.getBasicAnswers(prevAnswers); - - // If answers haven't changed the state is the same. - if (isSameFn) { - if (isSameFn(question, prevAnswers, prevBasicAnswers, question.answers, newBasicAnswers, - component, componentId)) { - return state; - } - } else { - if (this.questionDelegate.isSameResponse(question, prevBasicAnswers, newBasicAnswers, component, componentId)) { - return state; - } - } - } - - // Check if the response is complete and calculate the new state. - let complete: number; - let newState: string; - - if (isCompleteFn) { - // Pass all the answers since some behaviours might need the extra data. - complete = isCompleteFn(question, question.answers, component, componentId); - } else { - // Only pass the basic answers since questions should be independent of extra data. - complete = this.questionDelegate.isCompleteResponse(question, newBasicAnswers, component, componentId); - } - - if (complete < 0) { - newState = 'cannotdeterminestatus'; - } else if (complete > 0) { - newState = 'complete'; - } else { - newState = 'todo'; - } - - return this.questionProvider.getState(newState); + // Same implementation as the deferred feedback. Use that function instead of replicating it. + return this.deferredFeedbackHandler.determineNewStateDeferred(component, attemptId, question, componentId, siteId); } /** diff --git a/src/addon/qtype/calculated/providers/handler.ts b/src/addon/qtype/calculated/providers/handler.ts index 605842935..20b174edf 100644 --- a/src/addon/qtype/calculated/providers/handler.ts +++ b/src/addon/qtype/calculated/providers/handler.ts @@ -76,7 +76,7 @@ export class AddonQtypeCalculatedHandler implements CoreQuestionHandler { * @return 1 if complete, 0 if not complete, -1 if cannot determine. */ isCompleteResponse(question: any, answers: any, component: string, componentId: string | number): number { - if (!this.isGradableResponse(question, answers)) { + if (!this.isGradableResponse(question, answers, component, componentId)) { return 0; } @@ -129,9 +129,11 @@ export class AddonQtypeCalculatedHandler implements CoreQuestionHandler { * * @param question The question. * @param answers Object with the question answers (without prefix). + * @param component The component the question is related to. + * @param componentId Component ID. * @return 1 if gradable, 0 if not gradable, -1 if cannot determine. */ - isGradableResponse(question: any, answers: any): number { + isGradableResponse(question: any, answers: any, component: string, componentId: string | number): number { return this.isValidValue(answers['answer']) ? 1 : 0; } diff --git a/src/addon/qtype/calculatedmulti/providers/handler.ts b/src/addon/qtype/calculatedmulti/providers/handler.ts index 5d1a0622d..e2d8111ca 100644 --- a/src/addon/qtype/calculatedmulti/providers/handler.ts +++ b/src/addon/qtype/calculatedmulti/providers/handler.ts @@ -70,9 +70,11 @@ export class AddonQtypeCalculatedMultiHandler implements CoreQuestionHandler { * * @param question The question. * @param answers Object with the question answers (without prefix). + * @param component The component the question is related to. + * @param componentId Component ID. * @return 1 if gradable, 0 if not gradable, -1 if cannot determine. */ - isGradableResponse(question: any, answers: any): number { + isGradableResponse(question: any, answers: any, component: string, componentId: string | number): number { // This question type depends on multichoice. return this.multichoiceHandler.isGradableResponseSingle(answers); } diff --git a/src/addon/qtype/calculatedsimple/providers/handler.ts b/src/addon/qtype/calculatedsimple/providers/handler.ts index e1ec0b099..1301f3da5 100644 --- a/src/addon/qtype/calculatedsimple/providers/handler.ts +++ b/src/addon/qtype/calculatedsimple/providers/handler.ts @@ -70,11 +70,13 @@ export class AddonQtypeCalculatedSimpleHandler implements CoreQuestionHandler { * * @param question The question. * @param answers Object with the question answers (without prefix). + * @param component The component the question is related to. + * @param componentId Component ID. * @return 1 if gradable, 0 if not gradable, -1 if cannot determine. */ - isGradableResponse(question: any, answers: any): number { + isGradableResponse(question: any, answers: any, component: string, componentId: string | number): number { // This question type depends on calculated. - return this.calculatedHandler.isGradableResponse(question, answers); + return this.calculatedHandler.isGradableResponse(question, answers, component, componentId); } /** diff --git a/src/addon/qtype/ddimageortext/providers/handler.ts b/src/addon/qtype/ddimageortext/providers/handler.ts index 124e13950..94dcebcf6 100644 --- a/src/addon/qtype/ddimageortext/providers/handler.ts +++ b/src/addon/qtype/ddimageortext/providers/handler.ts @@ -93,9 +93,11 @@ export class AddonQtypeDdImageOrTextHandler implements CoreQuestionHandler { * * @param question The question. * @param answers Object with the question answers (without prefix). + * @param component The component the question is related to. + * @param componentId Component ID. * @return 1 if gradable, 0 if not gradable, -1 if cannot determine. */ - isGradableResponse(question: any, answers: any): number { + isGradableResponse(question: any, answers: any, component: string, componentId: string | number): number { for (const name in answers) { const value = answers[name]; if (value && value !== '0') { diff --git a/src/addon/qtype/ddmarker/providers/handler.ts b/src/addon/qtype/ddmarker/providers/handler.ts index 61b4b57ee..e4fa425b8 100644 --- a/src/addon/qtype/ddmarker/providers/handler.ts +++ b/src/addon/qtype/ddmarker/providers/handler.ts @@ -92,10 +92,12 @@ export class AddonQtypeDdMarkerHandler implements CoreQuestionHandler { * * @param question The question. * @param answers Object with the question answers (without prefix). + * @param component The component the question is related to. + * @param componentId Component ID. * @return 1 if gradable, 0 if not gradable, -1 if cannot determine. */ - isGradableResponse(question: any, answers: any): number { - return this.isCompleteResponse(question, answers, null, null); + isGradableResponse(question: any, answers: any, component: string, componentId: string | number): number { + return this.isCompleteResponse(question, answers, component, componentId); } /** diff --git a/src/addon/qtype/ddwtos/providers/handler.ts b/src/addon/qtype/ddwtos/providers/handler.ts index 93088447e..8f740bea7 100644 --- a/src/addon/qtype/ddwtos/providers/handler.ts +++ b/src/addon/qtype/ddwtos/providers/handler.ts @@ -91,9 +91,11 @@ export class AddonQtypeDdwtosHandler implements CoreQuestionHandler { * * @param question The question. * @param answers Object with the question answers (without prefix). + * @param component The component the question is related to. + * @param componentId Component ID. * @return 1 if gradable, 0 if not gradable, -1 if cannot determine. */ - isGradableResponse(question: any, answers: any): number { + isGradableResponse(question: any, answers: any, component: string, componentId: string | number): number { for (const name in answers) { const value = answers[name]; if (value && value !== '0') { diff --git a/src/addon/qtype/essay/providers/handler.ts b/src/addon/qtype/essay/providers/handler.ts index d78f299ee..41ae1592b 100644 --- a/src/addon/qtype/essay/providers/handler.ts +++ b/src/addon/qtype/essay/providers/handler.ts @@ -162,11 +162,11 @@ export class AddonQtypeEssayHandler implements CoreQuestionHandler { const attachments = CoreFileSession.instance.getFiles(component, questionComponentId); if (!allowedOptions.text) { - return attachments && attachments.length > 0 ? 1 : 0; + return attachments && attachments.length >= Number(question.displayoptions.attachmentsrequired) ? 1 : 0; } return (hasTextAnswer || question.displayoptions.responserequired == '0') && - ((attachments && attachments.length > 0) || question.displayoptions.attachmentsrequired == '0') ? 1 : 0; + (attachments && attachments.length > Number(question.displayoptions.attachmentsrequired)) ? 1 : 0; } /** @@ -184,10 +184,20 @@ export class AddonQtypeEssayHandler implements CoreQuestionHandler { * * @param question The question. * @param answers Object with the question answers (without prefix). + * @param component The component the question is related to. + * @param componentId Component ID. * @return 1 if gradable, 0 if not gradable, -1 if cannot determine. */ - isGradableResponse(question: any, answers: any): number { - return 0; + isGradableResponse(question: any, answers: any, component: string, componentId: string | number): number { + if (typeof question.responsefileareas == 'undefined') { + return -1; + } + + const questionComponentId = CoreQuestion.instance.getQuestionComponentId(question, componentId); + const attachments = CoreFileSession.instance.getFiles(component, questionComponentId); + + // Determine if the given response has online text or attachments. + return (answers['answer'] && answers['answer'] !== '') || (attachments && attachments.length > 0) ? 1 : 0; } /** diff --git a/src/addon/qtype/gapselect/providers/handler.ts b/src/addon/qtype/gapselect/providers/handler.ts index ef252a85a..26ee5c10e 100644 --- a/src/addon/qtype/gapselect/providers/handler.ts +++ b/src/addon/qtype/gapselect/providers/handler.ts @@ -92,9 +92,11 @@ export class AddonQtypeGapSelectHandler implements CoreQuestionHandler { * * @param question The question. * @param answers Object with the question answers (without prefix). + * @param component The component the question is related to. + * @param componentId Component ID. * @return 1 if gradable, 0 if not gradable, -1 if cannot determine. */ - isGradableResponse(question: any, answers: any): number { + isGradableResponse(question: any, answers: any, component: string, componentId: string | number): number { // We should always get a value for each select so we can assume we receive all the possible answers. for (const name in answers) { const value = answers[name]; diff --git a/src/addon/qtype/match/providers/handler.ts b/src/addon/qtype/match/providers/handler.ts index 3f147aed2..dcc287984 100644 --- a/src/addon/qtype/match/providers/handler.ts +++ b/src/addon/qtype/match/providers/handler.ts @@ -92,9 +92,11 @@ export class AddonQtypeMatchHandler implements CoreQuestionHandler { * * @param question The question. * @param answers Object with the question answers (without prefix). + * @param component The component the question is related to. + * @param componentId Component ID. * @return 1 if gradable, 0 if not gradable, -1 if cannot determine. */ - isGradableResponse(question: any, answers: any): number { + isGradableResponse(question: any, answers: any, component: string, componentId: string | number): number { // We should always get a value for each select so we can assume we receive all the possible answers. for (const name in answers) { const value = answers[name]; diff --git a/src/addon/qtype/multianswer/providers/handler.ts b/src/addon/qtype/multianswer/providers/handler.ts index 64d9c39f6..5cdf85a99 100644 --- a/src/addon/qtype/multianswer/providers/handler.ts +++ b/src/addon/qtype/multianswer/providers/handler.ts @@ -94,9 +94,11 @@ export class AddonQtypeMultiAnswerHandler implements CoreQuestionHandler { * * @param question The question. * @param answers Object with the question answers (without prefix). + * @param component The component the question is related to. + * @param componentId Component ID. * @return 1 if gradable, 0 if not gradable, -1 if cannot determine. */ - isGradableResponse(question: any, answers: any): number { + isGradableResponse(question: any, answers: any, component: string, componentId: string | number): number { // We should always get a value for each select so we can assume we receive all the possible answers. for (const name in answers) { const value = answers[name]; diff --git a/src/addon/qtype/multichoice/providers/handler.ts b/src/addon/qtype/multichoice/providers/handler.ts index ce49d8bed..1bd43f76b 100644 --- a/src/addon/qtype/multichoice/providers/handler.ts +++ b/src/addon/qtype/multichoice/providers/handler.ts @@ -97,10 +97,12 @@ export class AddonQtypeMultichoiceHandler implements CoreQuestionHandler { * * @param question The question. * @param answers Object with the question answers (without prefix). + * @param component The component the question is related to. + * @param componentId Component ID. * @return 1 if gradable, 0 if not gradable, -1 if cannot determine. */ - isGradableResponse(question: any, answers: any): number { - return this.isCompleteResponse(question, answers, null, null); + isGradableResponse(question: any, answers: any, component: string, componentId: string | number): number { + return this.isCompleteResponse(question, answers, component, componentId); } /** diff --git a/src/addon/qtype/randomsamatch/providers/handler.ts b/src/addon/qtype/randomsamatch/providers/handler.ts index 34db08724..8c48923f2 100644 --- a/src/addon/qtype/randomsamatch/providers/handler.ts +++ b/src/addon/qtype/randomsamatch/providers/handler.ts @@ -70,11 +70,13 @@ export class AddonQtypeRandomSaMatchHandler implements CoreQuestionHandler { * * @param question The question. * @param answers Object with the question answers (without prefix). + * @param component The component the question is related to. + * @param componentId Component ID. * @return 1 if gradable, 0 if not gradable, -1 if cannot determine. */ - isGradableResponse(question: any, answers: any): number { + isGradableResponse(question: any, answers: any, component: string, componentId: string | number): number { // This question behaves like a match question. - return this.matchHandler.isGradableResponse(question, answers); + return this.matchHandler.isGradableResponse(question, answers, component, componentId); } /** diff --git a/src/addon/qtype/shortanswer/providers/handler.ts b/src/addon/qtype/shortanswer/providers/handler.ts index b8cef9466..14abddb04 100644 --- a/src/addon/qtype/shortanswer/providers/handler.ts +++ b/src/addon/qtype/shortanswer/providers/handler.ts @@ -68,9 +68,11 @@ export class AddonQtypeShortAnswerHandler implements CoreQuestionHandler { * * @param question The question. * @param answers Object with the question answers (without prefix). + * @param component The component the question is related to. + * @param componentId Component ID. * @return 1 if gradable, 0 if not gradable, -1 if cannot determine. */ - isGradableResponse(question: any, answers: any): number { + isGradableResponse(question: any, answers: any, component: string, componentId: string | number): number { return this.isCompleteResponse(question, answers, null, null); } diff --git a/src/addon/qtype/truefalse/providers/handler.ts b/src/addon/qtype/truefalse/providers/handler.ts index 48ce82549..a8ef44aac 100644 --- a/src/addon/qtype/truefalse/providers/handler.ts +++ b/src/addon/qtype/truefalse/providers/handler.ts @@ -69,9 +69,11 @@ export class AddonQtypeTrueFalseHandler implements CoreQuestionHandler { * * @param question The question. * @param answers Object with the question answers (without prefix). + * @param component The component the question is related to. + * @param componentId Component ID. * @return 1 if gradable, 0 if not gradable, -1 if cannot determine. */ - isGradableResponse(question: any, answers: any): number { + isGradableResponse(question: any, answers: any, component: string, componentId: string | number): number { return this.isCompleteResponse(question, answers, null, null); } diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 7f1c3160f..8657003fb 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -1946,7 +1946,7 @@ "core.question.complete": "Complete", "core.question.correct": "Correct", "core.question.errorattachmentsnotsupportedinsite": "Your site doesn't support attaching files to answers yet.", - "core.question.errorinlinefilesnotsupportedinsite": "Your site doesn't support editing inline files yet.", + "core.question.errorembeddedfilesnotsupportedinsite": "Your site doesn't support editing embedded files yet.", "core.question.errorquestionnotsupported": "This question type is not supported by the app: {{$a}}.", "core.question.feedback": "Feedback", "core.question.howtodraganddrop": "Tap to select then tap to drop.", diff --git a/src/core/question/classes/base-question-handler.ts b/src/core/question/classes/base-question-handler.ts index 233704d78..afaa35fa4 100644 --- a/src/core/question/classes/base-question-handler.ts +++ b/src/core/question/classes/base-question-handler.ts @@ -93,9 +93,11 @@ export class CoreQuestionBaseHandler implements CoreQuestionHandler { * * @param question The question. * @param answers Object with the question answers (without prefix). + * @param component The component the question is related to. + * @param componentId Component ID. * @return 1 if gradable, 0 if not gradable, -1 if cannot determine. */ - isGradableResponse(question: any, answers: any): number { + isGradableResponse(question: any, answers: any, component: string, componentId: string | number): number { return -1; } diff --git a/src/core/question/lang/en.json b/src/core/question/lang/en.json index 1f06d28f9..ee7018835 100644 --- a/src/core/question/lang/en.json +++ b/src/core/question/lang/en.json @@ -6,7 +6,7 @@ "complete": "Complete", "correct": "Correct", "errorattachmentsnotsupportedinsite": "Your site doesn't support attaching files to answers yet.", - "errorinlinefilesnotsupportedinsite": "Your site doesn't support editing inline files yet.", + "errorembeddedfilesnotsupportedinsite": "Your site doesn't support editing embedded files yet.", "errorquestionnotsupported": "This question type is not supported by the app: {{$a}}.", "feedback": "Feedback", "howtodraganddrop": "Tap to select then tap to drop.", diff --git a/src/core/question/providers/delegate.ts b/src/core/question/providers/delegate.ts index b3c2c6818..6caa7eb2b 100644 --- a/src/core/question/providers/delegate.ts +++ b/src/core/question/providers/delegate.ts @@ -74,9 +74,11 @@ export interface CoreQuestionHandler extends CoreDelegateHandler { * * @param question The question. * @param answers Object with the question answers (without prefix). + * @param component The component the question is related to. + * @param componentId Component ID. * @return 1 if gradable, 0 if not gradable, -1 if cannot determine. */ - isGradableResponse?(question: any, answers: any): number; + isGradableResponse?(question: any, answers: any, component: string, componentId: string | number): number; /** * Check if two responses are the same. @@ -251,12 +253,14 @@ export class CoreQuestionDelegate extends CoreDelegate { * * @param question The question. * @param answers Object with the question answers (without prefix). + * @param component The component the question is related to. + * @param componentId Component ID. * @return 1 if gradable, 0 if not gradable, -1 if cannot determine. */ - isGradableResponse(question: any, answers: any): number { + isGradableResponse(question: any, answers: any, component: string, componentId: string | number): number { const type = this.getTypeName(question); - return this.executeFunctionOnEnabled(type, 'isGradableResponse', [question, answers]); + return this.executeFunctionOnEnabled(type, 'isGradableResponse', [question, answers, component, componentId]); } /**