MOBILE-2272 quiz: Improve calculate essay state in offline

main
Dani Palou 2020-10-01 16:01:14 +02:00
parent 65909073fa
commit 07af88d5d9
20 changed files with 77 additions and 134 deletions

View File

@ -142,7 +142,7 @@ export class AddonQbehaviourDeferredFeedbackHandler implements CoreQuestionBehav
} else if (complete > 0) { } else if (complete > 0) {
newState = 'complete'; newState = 'complete';
} else { } else {
const gradable = this.questionDelegate.isGradableResponse(question, newBasicAnswers); const gradable = this.questionDelegate.isGradableResponse(question, newBasicAnswers, component, componentId);
if (gradable < 0) { if (gradable < 0) {
newState = 'cannotdeterminestatus'; newState = 'cannotdeterminestatus';
} else if (gradable > 0) { } else if (gradable > 0) {

View File

@ -17,32 +17,7 @@ import { Injectable } from '@angular/core';
import { CoreQuestionBehaviourHandler } from '@core/question/providers/behaviour-delegate'; import { CoreQuestionBehaviourHandler } from '@core/question/providers/behaviour-delegate';
import { CoreQuestionDelegate } from '@core/question/providers/delegate'; import { CoreQuestionDelegate } from '@core/question/providers/delegate';
import { CoreQuestionProvider, CoreQuestionState } from '@core/question/providers/question'; import { CoreQuestionProvider, CoreQuestionState } from '@core/question/providers/question';
import { AddonQbehaviourDeferredFeedbackHandler } from '@addon/qbehaviour/deferredfeedback/providers/handler';
/**
* 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;
/** /**
* Handler to support manual graded question behaviour. * Handler to support manual graded question behaviour.
@ -52,7 +27,9 @@ export class AddonQbehaviourManualGradedHandler implements CoreQuestionBehaviour
name = 'AddonQbehaviourManualGraded'; name = 'AddonQbehaviourManualGraded';
type = 'manualgraded'; type = 'manualgraded';
constructor(private questionDelegate: CoreQuestionDelegate, private questionProvider: CoreQuestionProvider) { constructor(protected questionDelegate: CoreQuestionDelegate,
protected questionProvider: CoreQuestionProvider,
protected deferredFeedbackHandler: AddonQbehaviourDeferredFeedbackHandler) {
// Nothing to do. // Nothing to do.
} }
@ -68,84 +45,8 @@ export class AddonQbehaviourManualGradedHandler implements CoreQuestionBehaviour
*/ */
determineNewState(component: string, attemptId: number, question: any, componentId: string | number, siteId?: string) determineNewState(component: string, attemptId: number, question: any, componentId: string | number, siteId?: string)
: CoreQuestionState | Promise<CoreQuestionState> { : CoreQuestionState | Promise<CoreQuestionState> {
return this.determineNewStateManualGraded(component, attemptId, question, componentId, siteId); // Same implementation as the deferred feedback. Use that function instead of replicating it.
} return this.deferredFeedbackHandler.determineNewStateDeferred(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<CoreQuestionState> {
// 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);
} }
/** /**

View File

@ -76,7 +76,7 @@ export class AddonQtypeCalculatedHandler implements CoreQuestionHandler {
* @return 1 if complete, 0 if not complete, -1 if cannot determine. * @return 1 if complete, 0 if not complete, -1 if cannot determine.
*/ */
isCompleteResponse(question: any, answers: any, component: string, componentId: string | number): number { 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; return 0;
} }
@ -129,9 +129,11 @@ export class AddonQtypeCalculatedHandler implements CoreQuestionHandler {
* *
* @param question The question. * @param question The question.
* @param answers Object with the question answers (without prefix). * @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. * @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; return this.isValidValue(answers['answer']) ? 1 : 0;
} }

View File

@ -70,9 +70,11 @@ export class AddonQtypeCalculatedMultiHandler implements CoreQuestionHandler {
* *
* @param question The question. * @param question The question.
* @param answers Object with the question answers (without prefix). * @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. * @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. // This question type depends on multichoice.
return this.multichoiceHandler.isGradableResponseSingle(answers); return this.multichoiceHandler.isGradableResponseSingle(answers);
} }

View File

@ -70,11 +70,13 @@ export class AddonQtypeCalculatedSimpleHandler implements CoreQuestionHandler {
* *
* @param question The question. * @param question The question.
* @param answers Object with the question answers (without prefix). * @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. * @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. // This question type depends on calculated.
return this.calculatedHandler.isGradableResponse(question, answers); return this.calculatedHandler.isGradableResponse(question, answers, component, componentId);
} }
/** /**

View File

@ -93,9 +93,11 @@ export class AddonQtypeDdImageOrTextHandler implements CoreQuestionHandler {
* *
* @param question The question. * @param question The question.
* @param answers Object with the question answers (without prefix). * @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. * @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) { for (const name in answers) {
const value = answers[name]; const value = answers[name];
if (value && value !== '0') { if (value && value !== '0') {

View File

@ -92,10 +92,12 @@ export class AddonQtypeDdMarkerHandler implements CoreQuestionHandler {
* *
* @param question The question. * @param question The question.
* @param answers Object with the question answers (without prefix). * @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. * @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); return this.isCompleteResponse(question, answers, component, componentId);
} }
/** /**

View File

@ -91,9 +91,11 @@ export class AddonQtypeDdwtosHandler implements CoreQuestionHandler {
* *
* @param question The question. * @param question The question.
* @param answers Object with the question answers (without prefix). * @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. * @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) { for (const name in answers) {
const value = answers[name]; const value = answers[name];
if (value && value !== '0') { if (value && value !== '0') {

View File

@ -162,11 +162,11 @@ export class AddonQtypeEssayHandler implements CoreQuestionHandler {
const attachments = CoreFileSession.instance.getFiles(component, questionComponentId); const attachments = CoreFileSession.instance.getFiles(component, questionComponentId);
if (!allowedOptions.text) { 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') && 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 question The question.
* @param answers Object with the question answers (without prefix). * @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. * @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 0; 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;
} }
/** /**

View File

@ -92,9 +92,11 @@ export class AddonQtypeGapSelectHandler implements CoreQuestionHandler {
* *
* @param question The question. * @param question The question.
* @param answers Object with the question answers (without prefix). * @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. * @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. // We should always get a value for each select so we can assume we receive all the possible answers.
for (const name in answers) { for (const name in answers) {
const value = answers[name]; const value = answers[name];

View File

@ -92,9 +92,11 @@ export class AddonQtypeMatchHandler implements CoreQuestionHandler {
* *
* @param question The question. * @param question The question.
* @param answers Object with the question answers (without prefix). * @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. * @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. // We should always get a value for each select so we can assume we receive all the possible answers.
for (const name in answers) { for (const name in answers) {
const value = answers[name]; const value = answers[name];

View File

@ -94,9 +94,11 @@ export class AddonQtypeMultiAnswerHandler implements CoreQuestionHandler {
* *
* @param question The question. * @param question The question.
* @param answers Object with the question answers (without prefix). * @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. * @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. // We should always get a value for each select so we can assume we receive all the possible answers.
for (const name in answers) { for (const name in answers) {
const value = answers[name]; const value = answers[name];

View File

@ -97,10 +97,12 @@ export class AddonQtypeMultichoiceHandler implements CoreQuestionHandler {
* *
* @param question The question. * @param question The question.
* @param answers Object with the question answers (without prefix). * @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. * @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); return this.isCompleteResponse(question, answers, component, componentId);
} }
/** /**

View File

@ -70,11 +70,13 @@ export class AddonQtypeRandomSaMatchHandler implements CoreQuestionHandler {
* *
* @param question The question. * @param question The question.
* @param answers Object with the question answers (without prefix). * @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. * @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. // This question behaves like a match question.
return this.matchHandler.isGradableResponse(question, answers); return this.matchHandler.isGradableResponse(question, answers, component, componentId);
} }
/** /**

View File

@ -68,9 +68,11 @@ export class AddonQtypeShortAnswerHandler implements CoreQuestionHandler {
* *
* @param question The question. * @param question The question.
* @param answers Object with the question answers (without prefix). * @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. * @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); return this.isCompleteResponse(question, answers, null, null);
} }

View File

@ -69,9 +69,11 @@ export class AddonQtypeTrueFalseHandler implements CoreQuestionHandler {
* *
* @param question The question. * @param question The question.
* @param answers Object with the question answers (without prefix). * @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. * @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); return this.isCompleteResponse(question, answers, null, null);
} }

View File

@ -1946,7 +1946,7 @@
"core.question.complete": "Complete", "core.question.complete": "Complete",
"core.question.correct": "Correct", "core.question.correct": "Correct",
"core.question.errorattachmentsnotsupportedinsite": "Your site doesn't support attaching files to answers yet.", "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.errorquestionnotsupported": "This question type is not supported by the app: {{$a}}.",
"core.question.feedback": "Feedback", "core.question.feedback": "Feedback",
"core.question.howtodraganddrop": "Tap to select then tap to drop.", "core.question.howtodraganddrop": "Tap to select then tap to drop.",

View File

@ -93,9 +93,11 @@ export class CoreQuestionBaseHandler implements CoreQuestionHandler {
* *
* @param question The question. * @param question The question.
* @param answers Object with the question answers (without prefix). * @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. * @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; return -1;
} }

View File

@ -6,7 +6,7 @@
"complete": "Complete", "complete": "Complete",
"correct": "Correct", "correct": "Correct",
"errorattachmentsnotsupportedinsite": "Your site doesn't support attaching files to answers yet.", "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}}.", "errorquestionnotsupported": "This question type is not supported by the app: {{$a}}.",
"feedback": "Feedback", "feedback": "Feedback",
"howtodraganddrop": "Tap to select then tap to drop.", "howtodraganddrop": "Tap to select then tap to drop.",

View File

@ -74,9 +74,11 @@ export interface CoreQuestionHandler extends CoreDelegateHandler {
* *
* @param question The question. * @param question The question.
* @param answers Object with the question answers (without prefix). * @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. * @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. * Check if two responses are the same.
@ -251,12 +253,14 @@ export class CoreQuestionDelegate extends CoreDelegate {
* *
* @param question The question. * @param question The question.
* @param answers Object with the question answers (without prefix). * @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. * @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); const type = this.getTypeName(question);
return this.executeFunctionOnEnabled(type, 'isGradableResponse', [question, answers]); return this.executeFunctionOnEnabled(type, 'isGradableResponse', [question, answers, component, componentId]);
} }
/** /**