From 46efcc441ac854e6eb1a61cb4f107a5af677a089 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 29 Sep 2020 10:46:17 +0200 Subject: [PATCH] MOBILE-2272 quiz: Support add new files in essay in online --- src/addon/mod/quiz/pages/player/player.ts | 5 +- src/addon/mod/quiz/providers/quiz-offline.ts | 4 +- .../deferredcbm/providers/handler.ts | 19 ++-- .../deferredfeedback/providers/handler.ts | 25 +++-- .../informationitem/providers/handler.ts | 3 +- .../manualgraded/providers/handler.ts | 28 ++++-- .../qtype/calculated/providers/handler.ts | 8 +- .../calculatedmulti/providers/handler.ts | 8 +- .../calculatedsimple/providers/handler.ts | 12 ++- .../qtype/ddimageortext/providers/handler.ts | 8 +- src/addon/qtype/ddmarker/providers/handler.ts | 10 +- src/addon/qtype/ddwtos/providers/handler.ts | 8 +- .../essay/component/addon-qtype-essay.html | 2 +- src/addon/qtype/essay/component/essay.ts | 7 +- src/addon/qtype/essay/providers/handler.ts | 96 +++++++++++++++++-- .../qtype/gapselect/providers/handler.ts | 8 +- src/addon/qtype/match/providers/handler.ts | 8 +- .../qtype/multianswer/providers/handler.ts | 8 +- .../qtype/multichoice/providers/handler.ts | 15 ++- .../qtype/randomsamatch/providers/handler.ts | 12 ++- .../qtype/shortanswer/providers/handler.ts | 10 +- .../qtype/truefalse/providers/handler.ts | 15 ++- .../fileuploader/providers/fileuploader.ts | 39 +++++++- .../classes/base-behaviour-handler.ts | 3 +- .../question/classes/base-question-handler.ts | 13 ++- .../question/providers/behaviour-delegate.ts | 10 +- src/core/question/providers/delegate.ts | 51 ++++++++-- src/core/question/providers/helper.ts | 26 ++++- src/core/question/providers/question.ts | 34 +++++++ 29 files changed, 393 insertions(+), 102 deletions(-) diff --git a/src/addon/mod/quiz/pages/player/player.ts b/src/addon/mod/quiz/pages/player/player.ts index 75fb54312..0ccbb17cf 100644 --- a/src/addon/mod/quiz/pages/player/player.ts +++ b/src/addon/mod/quiz/pages/player/player.ts @@ -569,7 +569,8 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy { // Prepare the answers to be sent for the attempt. protected prepareAnswers(): Promise { - return this.questionHelper.prepareAnswers(this.questions, this.getAnswers(), this.offline); + return this.questionHelper.prepareAnswers(this.questions, this.getAnswers(), this.offline, this.component, + this.quiz.coursemodule); } /** @@ -612,6 +613,8 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy { if (this.formElement) { this.domUtils.triggerFormSubmittedEvent(this.formElement, !this.offline, this.sitesProvider.getCurrentSiteId()); } + + return this.questionHelper.clearTmpData(this.questions, this.component, this.quiz.coursemodule); }); } diff --git a/src/addon/mod/quiz/providers/quiz-offline.ts b/src/addon/mod/quiz/providers/quiz-offline.ts index 6c1f5121e..e7bac37d5 100644 --- a/src/addon/mod/quiz/providers/quiz-offline.ts +++ b/src/addon/mod/quiz/providers/quiz-offline.ts @@ -342,8 +342,8 @@ export class AddonModQuizOfflineProvider { for (const slot in questionsWithAnswers) { const question = questionsWithAnswers[slot]; - promises.push(this.behaviourDelegate.determineNewState( - quiz.preferredbehaviour, AddonModQuizProvider.COMPONENT, attempt.id, question, siteId).then((state) => { + promises.push(this.behaviourDelegate.determineNewState(quiz.preferredbehaviour, AddonModQuizProvider.COMPONENT, + attempt.id, question, quiz.coursemodule, siteId).then((state) => { // Check if state has changed. if (state && state.name != question.state) { newStates[question.slot] = state.name; diff --git a/src/addon/qbehaviour/deferredcbm/providers/handler.ts b/src/addon/qbehaviour/deferredcbm/providers/handler.ts index a32634c1f..1ba3bcdcc 100644 --- a/src/addon/qbehaviour/deferredcbm/providers/handler.ts +++ b/src/addon/qbehaviour/deferredcbm/providers/handler.ts @@ -40,13 +40,14 @@ export class AddonQbehaviourDeferredCBMHandler implements CoreQuestionBehaviourH * @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. * @return New state (or promise resolved with state). */ - determineNewState(component: string, attemptId: number, question: any, siteId?: string) + determineNewState(component: string, attemptId: number, question: any, componentId: string | number, siteId?: string) : CoreQuestionState | Promise { // Depends on deferredfeedback. - return this.deferredFeedbackHandler.determineNewStateDeferred(component, attemptId, question, siteId, + return this.deferredFeedbackHandler.determineNewStateDeferred(component, attemptId, question, componentId, siteId, this.isCompleteResponse.bind(this), this.isSameResponse.bind(this)); } @@ -71,11 +72,13 @@ export class AddonQbehaviourDeferredCBMHandler implements CoreQuestionBehaviourH * * @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. */ - protected isCompleteResponse(question: any, answers: any): number { + protected isCompleteResponse(question: any, answers: any, component: string, componentId: string | number): number { // First check if the question answer is complete. - const complete = this.questionDelegate.isCompleteResponse(question, answers); + const complete = this.questionDelegate.isCompleteResponse(question, answers, component, componentId); if (complete > 0) { // Answer is complete, check the user answered CBM too. return answers['-certainty'] ? 1 : 0; @@ -101,12 +104,14 @@ export class AddonQbehaviourDeferredCBMHandler implements CoreQuestionBehaviourH * @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. */ - protected isSameResponse(question: any, prevAnswers: any, prevBasicAnswers: any, newAnswers: any, newBasicAnswers: any) - : boolean { + protected isSameResponse(question: any, prevAnswers: any, prevBasicAnswers: any, newAnswers: any, newBasicAnswers: any, + component: string, componentId: string | number): boolean { // First check if the question answer is the same. - const same = this.questionDelegate.isSameResponse(question, prevBasicAnswers, newBasicAnswers); + const same = this.questionDelegate.isSameResponse(question, prevBasicAnswers, newBasicAnswers, component, componentId); if (same) { // Same response, check the CBM is the same too. return prevAnswers['-certainty'] == newAnswers['-certainty']; diff --git a/src/addon/qbehaviour/deferredfeedback/providers/handler.ts b/src/addon/qbehaviour/deferredfeedback/providers/handler.ts index 0edf35d41..e53242ffc 100644 --- a/src/addon/qbehaviour/deferredfeedback/providers/handler.ts +++ b/src/addon/qbehaviour/deferredfeedback/providers/handler.ts @@ -23,9 +23,11 @@ import { CoreQuestionProvider, CoreQuestionState } from '@core/question/provider * * @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) => number; +export type isCompleteResponseFunction = (question: any, answers: any, component: string, componentId: string | number) => number; /** * Check if two responses are the same. @@ -35,10 +37,12 @@ export type isCompleteResponseFunction = (question: any, answers: any) => number * @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) => boolean; + newBasicAnswers: any, component: string, componentId: string | number) => boolean; /** * Handler to support deferred feedback question behaviour. @@ -58,12 +62,13 @@ export class AddonQbehaviourDeferredFeedbackHandler implements CoreQuestionBehav * @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. * @return New state (or promise resolved with state). */ - determineNewState(component: string, attemptId: number, question: any, siteId?: string) + determineNewState(component: string, attemptId: number, question: any, componentId: string | number, siteId?: string) : CoreQuestionState | Promise { - return this.determineNewStateDeferred(component, attemptId, question, siteId); + return this.determineNewStateDeferred(component, attemptId, question, componentId, siteId); } /** @@ -72,12 +77,13 @@ export class AddonQbehaviourDeferredFeedbackHandler implements CoreQuestionBehav * @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. */ - determineNewStateDeferred(component: string, attemptId: number, question: any, siteId?: string, + determineNewStateDeferred(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. @@ -103,11 +109,12 @@ export class AddonQbehaviourDeferredFeedbackHandler implements CoreQuestionBehav // If answers haven't changed the state is the same. if (isSameFn) { - if (isSameFn(question, prevAnswers, prevBasicAnswers, question.answers, newBasicAnswers)) { + if (isSameFn(question, prevAnswers, prevBasicAnswers, question.answers, newBasicAnswers, + component, componentId)) { return state; } } else { - if (this.questionDelegate.isSameResponse(question, prevBasicAnswers, newBasicAnswers)) { + if (this.questionDelegate.isSameResponse(question, prevBasicAnswers, newBasicAnswers, component, componentId)) { return state; } } @@ -117,10 +124,10 @@ export class AddonQbehaviourDeferredFeedbackHandler implements CoreQuestionBehav newState: string; if (isCompleteFn) { // Pass all the answers since some behaviours might need the extra data. - complete = isCompleteFn(question, question.answers); + 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); + complete = this.questionDelegate.isCompleteResponse(question, newBasicAnswers, component, componentId); } if (complete < 0) { diff --git a/src/addon/qbehaviour/informationitem/providers/handler.ts b/src/addon/qbehaviour/informationitem/providers/handler.ts index c30e2c800..dfe438725 100644 --- a/src/addon/qbehaviour/informationitem/providers/handler.ts +++ b/src/addon/qbehaviour/informationitem/providers/handler.ts @@ -35,10 +35,11 @@ export class AddonQbehaviourInformationItemHandler implements CoreQuestionBehavi * @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. * @return New state (or promise resolved with state). */ - determineNewState(component: string, attemptId: number, question: any, siteId?: string) + determineNewState(component: string, attemptId: number, question: any, componentId: string | number, siteId?: string) : CoreQuestionState | Promise { if (question.answers['-seen']) { return this.questionProvider.getState('complete'); diff --git a/src/addon/qbehaviour/manualgraded/providers/handler.ts b/src/addon/qbehaviour/manualgraded/providers/handler.ts index 98da9c06d..2cfe4c20b 100644 --- a/src/addon/qbehaviour/manualgraded/providers/handler.ts +++ b/src/addon/qbehaviour/manualgraded/providers/handler.ts @@ -23,9 +23,11 @@ import { CoreQuestionProvider, CoreQuestionState } from '@core/question/provider * * @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) => number; +export type isCompleteResponseFunction = (question: any, answers: any, component: string, componentId: string | number) => number; /** * Check if two responses are the same. @@ -35,10 +37,12 @@ export type isCompleteResponseFunction = (question: any, answers: any) => number * @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) => boolean; + newBasicAnswers: any, component: string, componentId: string | number) => boolean; /** * Handler to support manual graded question behaviour. @@ -58,12 +62,13 @@ export class AddonQbehaviourManualGradedHandler implements CoreQuestionBehaviour * @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. * @return New state (or promise resolved with state). */ - determineNewState(component: string, attemptId: number, question: any, siteId?: string) + determineNewState(component: string, attemptId: number, question: any, componentId: string | number, siteId?: string) : CoreQuestionState | Promise { - return this.determineNewStateManualGraded(component, attemptId, question, siteId); + return this.determineNewStateManualGraded(component, attemptId, question, componentId, siteId); } /** @@ -72,13 +77,15 @@ export class AddonQbehaviourManualGradedHandler implements CoreQuestionBehaviour * @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. */ - determineNewStateManualGraded(component: string, attemptId: number, question: any, siteId?: string, - isCompleteFn?: isCompleteResponseFunction, isSameFn?: isSameResponseFunction): Promise { + 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. return this.questionProvider.getQuestion(component, attemptId, question.slot, siteId).catch(() => { @@ -103,11 +110,12 @@ export class AddonQbehaviourManualGradedHandler implements CoreQuestionBehaviour // If answers haven't changed the state is the same. if (isSameFn) { - if (isSameFn(question, prevAnswers, prevBasicAnswers, question.answers, newBasicAnswers)) { + if (isSameFn(question, prevAnswers, prevBasicAnswers, question.answers, newBasicAnswers, + component, componentId)) { return state; } } else { - if (this.questionDelegate.isSameResponse(question, prevBasicAnswers, newBasicAnswers)) { + if (this.questionDelegate.isSameResponse(question, prevBasicAnswers, newBasicAnswers, component, componentId)) { return state; } } @@ -117,10 +125,10 @@ export class AddonQbehaviourManualGradedHandler implements CoreQuestionBehaviour newState: string; if (isCompleteFn) { // Pass all the answers since some behaviours might need the extra data. - complete = isCompleteFn(question, question.answers); + 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); + complete = this.questionDelegate.isCompleteResponse(question, newBasicAnswers, component, componentId); } if (complete < 0) { diff --git a/src/addon/qtype/calculated/providers/handler.ts b/src/addon/qtype/calculated/providers/handler.ts index c7832f91c..bc5d7fdc9 100644 --- a/src/addon/qtype/calculated/providers/handler.ts +++ b/src/addon/qtype/calculated/providers/handler.ts @@ -46,9 +46,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 complete, 0 if not complete, -1 if cannot determine. */ - isCompleteResponse(question: any, answers: any): number { + isCompleteResponse(question: any, answers: any, component: string, componentId: string | number): number { if (this.isGradableResponse(question, answers) === 0 || !this.validateUnits(answers['answer'])) { return 0; } @@ -93,9 +95,11 @@ export class AddonQtypeCalculatedHandler implements CoreQuestionHandler { * @param question Question. * @param prevAnswers Object with the previous question answers. * @param newAnswers Object with the new question answers. + * @param component The component the question is related to. + * @param componentId Component ID. * @return Whether they're the same. */ - isSameResponse(question: any, prevAnswers: any, newAnswers: any): boolean { + isSameResponse(question: any, prevAnswers: any, newAnswers: any, component: string, componentId: string | number): boolean { return this.utils.sameAtKeyMissingIsBlank(prevAnswers, newAnswers, 'answer') && this.utils.sameAtKeyMissingIsBlank(prevAnswers, newAnswers, 'unit'); } diff --git a/src/addon/qtype/calculatedmulti/providers/handler.ts b/src/addon/qtype/calculatedmulti/providers/handler.ts index 63fcd7a3e..5d1a0622d 100644 --- a/src/addon/qtype/calculatedmulti/providers/handler.ts +++ b/src/addon/qtype/calculatedmulti/providers/handler.ts @@ -46,9 +46,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 complete, 0 if not complete, -1 if cannot determine. */ - isCompleteResponse(question: any, answers: any): number { + isCompleteResponse(question: any, answers: any, component: string, componentId: string | number): number { // This question type depends on multichoice. return this.multichoiceHandler.isCompleteResponseSingle(answers); } @@ -81,9 +83,11 @@ export class AddonQtypeCalculatedMultiHandler implements CoreQuestionHandler { * @param question Question. * @param prevAnswers Object with the previous question answers. * @param newAnswers Object with the new question answers. + * @param component The component the question is related to. + * @param componentId Component ID. * @return Whether they're the same. */ - isSameResponse(question: any, prevAnswers: any, newAnswers: any): boolean { + isSameResponse(question: any, prevAnswers: any, newAnswers: any, component: string, componentId: string | number): boolean { // This question type depends on multichoice. return this.multichoiceHandler.isSameResponseSingle(prevAnswers, newAnswers); } diff --git a/src/addon/qtype/calculatedsimple/providers/handler.ts b/src/addon/qtype/calculatedsimple/providers/handler.ts index e5f442b20..e1ec0b099 100644 --- a/src/addon/qtype/calculatedsimple/providers/handler.ts +++ b/src/addon/qtype/calculatedsimple/providers/handler.ts @@ -46,11 +46,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 complete, 0 if not complete, -1 if cannot determine. */ - isCompleteResponse(question: any, answers: any): number { + isCompleteResponse(question: any, answers: any, component: string, componentId: string | number): number { // This question type depends on calculated. - return this.calculatedHandler.isCompleteResponse(question, answers); + return this.calculatedHandler.isCompleteResponse(question, answers, component, componentId); } /** @@ -81,10 +83,12 @@ export class AddonQtypeCalculatedSimpleHandler implements CoreQuestionHandler { * @param question Question. * @param prevAnswers Object with the previous question answers. * @param newAnswers Object with the new question answers. + * @param component The component the question is related to. + * @param componentId Component ID. * @return Whether they're the same. */ - isSameResponse(question: any, prevAnswers: any, newAnswers: any): boolean { + isSameResponse(question: any, prevAnswers: any, newAnswers: any, component: string, componentId: string | number): boolean { // This question type depends on calculated. - return this.calculatedHandler.isSameResponse(question, prevAnswers, newAnswers); + return this.calculatedHandler.isSameResponse(question, prevAnswers, newAnswers, component, componentId); } } diff --git a/src/addon/qtype/ddimageortext/providers/handler.ts b/src/addon/qtype/ddimageortext/providers/handler.ts index 1774415fb..124e13950 100644 --- a/src/addon/qtype/ddimageortext/providers/handler.ts +++ b/src/addon/qtype/ddimageortext/providers/handler.ts @@ -61,9 +61,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 complete, 0 if not complete, -1 if cannot determine. */ - isCompleteResponse(question: any, answers: any): number { + isCompleteResponse(question: any, answers: any, component: string, componentId: string | number): number { // An answer is complete if all drop zones have an answer. // We should always receive all the drop zones with their value ('' if not answered). for (const name in answers) { @@ -110,9 +112,11 @@ export class AddonQtypeDdImageOrTextHandler implements CoreQuestionHandler { * @param question Question. * @param prevAnswers Object with the previous question answers. * @param newAnswers Object with the new question answers. + * @param component The component the question is related to. + * @param componentId Component ID. * @return Whether they're the same. */ - isSameResponse(question: any, prevAnswers: any, newAnswers: any): boolean { + isSameResponse(question: any, prevAnswers: any, newAnswers: any, component: string, componentId: string | number): boolean { return this.questionProvider.compareAllAnswers(prevAnswers, newAnswers); } } diff --git a/src/addon/qtype/ddmarker/providers/handler.ts b/src/addon/qtype/ddmarker/providers/handler.ts index 2e8195ff5..61b4b57ee 100644 --- a/src/addon/qtype/ddmarker/providers/handler.ts +++ b/src/addon/qtype/ddmarker/providers/handler.ts @@ -62,9 +62,11 @@ 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 complete, 0 if not complete, -1 if cannot determine. */ - isCompleteResponse(question: any, answers: any): number { + isCompleteResponse(question: any, answers: any, component: string, componentId: string | number): number { // If 1 dragitem is set we assume the answer is complete (like Moodle does). for (const name in answers) { if (answers[name]) { @@ -93,7 +95,7 @@ export class AddonQtypeDdMarkerHandler implements CoreQuestionHandler { * @return 1 if gradable, 0 if not gradable, -1 if cannot determine. */ isGradableResponse(question: any, answers: any): number { - return this.isCompleteResponse(question, answers); + return this.isCompleteResponse(question, answers, null, null); } /** @@ -102,9 +104,11 @@ export class AddonQtypeDdMarkerHandler implements CoreQuestionHandler { * @param question Question. * @param prevAnswers Object with the previous question answers. * @param newAnswers Object with the new question answers. + * @param component The component the question is related to. + * @param componentId Component ID. * @return Whether they're the same. */ - isSameResponse(question: any, prevAnswers: any, newAnswers: any): boolean { + isSameResponse(question: any, prevAnswers: any, newAnswers: any, component: string, componentId: string | number): boolean { return this.questionProvider.compareAllAnswers(prevAnswers, newAnswers); } diff --git a/src/addon/qtype/ddwtos/providers/handler.ts b/src/addon/qtype/ddwtos/providers/handler.ts index d310e27b2..93088447e 100644 --- a/src/addon/qtype/ddwtos/providers/handler.ts +++ b/src/addon/qtype/ddwtos/providers/handler.ts @@ -61,9 +61,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 complete, 0 if not complete, -1 if cannot determine. */ - isCompleteResponse(question: any, answers: any): number { + isCompleteResponse(question: any, answers: any, component: string, componentId: string | number): number { for (const name in answers) { const value = answers[name]; if (!value || value === '0') { @@ -108,9 +110,11 @@ export class AddonQtypeDdwtosHandler implements CoreQuestionHandler { * @param question Question. * @param prevAnswers Object with the previous question answers. * @param newAnswers Object with the new question answers. + * @param component The component the question is related to. + * @param componentId Component ID. * @return Whether they're the same. */ - isSameResponse(question: any, prevAnswers: any, newAnswers: any): boolean { + isSameResponse(question: any, prevAnswers: any, newAnswers: any, component: string, componentId: string | number): boolean { return this.questionProvider.compareAllAnswers(prevAnswers, newAnswers); } } diff --git a/src/addon/qtype/essay/component/addon-qtype-essay.html b/src/addon/qtype/essay/component/addon-qtype-essay.html index 677a3149c..bb496cb6a 100644 --- a/src/addon/qtype/essay/component/addon-qtype-essay.html +++ b/src/addon/qtype/essay/component/addon-qtype-essay.html @@ -27,7 +27,7 @@ - + diff --git a/src/addon/qtype/essay/component/essay.ts b/src/addon/qtype/essay/component/essay.ts index 3419ca125..c2f245661 100644 --- a/src/addon/qtype/essay/component/essay.ts +++ b/src/addon/qtype/essay/component/essay.ts @@ -14,9 +14,9 @@ import { Component, OnInit, Injector } from '@angular/core'; import { CoreLoggerProvider } from '@providers/logger'; -import { CoreSites } from '@providers/sites'; import { CoreWSExternalFile } from '@providers/ws'; import { CoreQuestionBaseComponent } from '@core/question/classes/base-question-component'; +import { CoreQuestion } from '@core/question/providers/question'; import { FormControl, FormBuilder } from '@angular/forms'; import { CoreFileSession } from '@providers/file-session'; @@ -42,14 +42,15 @@ export class AddonQtypeEssayComponent extends CoreQuestionBaseComponent implemen * Component being initialized. */ ngOnInit(): void { - this.uploadFilesSupported = CoreSites.instance.getCurrentSite().isVersionGreaterEqualThan('3.10'); + this.uploadFilesSupported = typeof this.question.responsefileareas != 'undefined'; this.initEssayComponent(); this.formControl = this.fb.control(this.question.textarea && this.question.textarea.text); if (this.question.allowsAttachments && this.uploadFilesSupported) { this.attachments = Array.from(this.questionHelper.getResponseFileAreaFiles(this.question, 'attachments')); - CoreFileSession.instance.setFiles(this.component, this.componentId + '_' + this.question.id, this.attachments); + CoreFileSession.instance.setFiles(this.component, + CoreQuestion.instance.getQuestionComponentId(this.question, this.componentId), this.attachments); } } } diff --git a/src/addon/qtype/essay/providers/handler.ts b/src/addon/qtype/essay/providers/handler.ts index fc9466973..0f911e828 100644 --- a/src/addon/qtype/essay/providers/handler.ts +++ b/src/addon/qtype/essay/providers/handler.ts @@ -14,12 +14,15 @@ // limitations under the License. import { Injectable, Injector } from '@angular/core'; +import { CoreFileSession } from '@providers/file-session'; import { CoreSites } from '@providers/sites'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreFileUploader } from '@core/fileuploader/providers/fileuploader'; import { CoreQuestionHandler } from '@core/question/providers/delegate'; import { CoreQuestionHelperProvider } from '@core/question/providers/helper'; +import { CoreQuestion } from '@core/question/providers/question'; import { AddonQtypeEssayComponent } from '../component/essay'; /** @@ -33,6 +36,24 @@ export class AddonQtypeEssayHandler implements CoreQuestionHandler { constructor(private utils: CoreUtilsProvider, private questionHelper: CoreQuestionHelperProvider, private textUtils: CoreTextUtilsProvider, private domUtils: CoreDomUtilsProvider) { } + /** + * Clear temporary data after the data has been saved. + * + * @param question Question. + * @param component The component the question is related to. + * @param componentId Component ID. + */ + clearTmpData(question: any, component: string, componentId: string | number): void { + const questionComponentId = CoreQuestion.instance.getQuestionComponentId(question, componentId); + const files = CoreFileSession.instance.getFiles(component, questionComponentId); + + // Clear the files in session for this question. + CoreFileSession.instance.clearFiles(component, questionComponentId); + + // Now delete the local files from the tmp folder. + CoreFileUploader.instance.clearTmpFiles(files); + } + /** * Return the name of the behaviour to use for the question. * If the question should use the default behaviour you shouldn't implement this function. @@ -66,13 +87,14 @@ export class AddonQtypeEssayHandler implements CoreQuestionHandler { */ getPreventSubmitMessage(question: any): string { const element = this.domUtils.convertToElement(question.html); + const uploadFilesSupported = typeof question.responsefileareas != 'undefined'; - if (element.querySelector('div[id*=filemanager]')) { + if (!uploadFilesSupported && element.querySelector('div[id*=filemanager]')) { // The question allows attachments. Since the app cannot attach files yet we will prevent submitting the question. return 'core.question.errorattachmentsnotsupportedinsite'; } - if (this.questionHelper.hasDraftFileUrls(element.innerHTML)) { + if (!uploadFilesSupported && this.questionHelper.hasDraftFileUrls(element.innerHTML)) { return 'core.question.errorinlinefilesnotsupportedinsite'; } } @@ -82,20 +104,36 @@ 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 complete, 0 if not complete, -1 if cannot determine. */ - isCompleteResponse(question: any, answers: any): number { + isCompleteResponse(question: any, answers: any, component: string, componentId: string | number): number { const element = this.domUtils.convertToElement(question.html); - const hasInlineText = answers['answer'] && answers['answer'] !== '', - allowsAttachments = !!element.querySelector('div[id*=filemanager]'); + const hasInlineText = answers['answer'] && answers['answer'] !== ''; + const allowsInlineText = !!element.querySelector('textarea[name*=_answer]'); + const allowsAttachments = !!element.querySelector('div[id*=filemanager]'); + const uploadFilesSupported = typeof question.responsefileareas != 'undefined'; if (!allowsAttachments) { return hasInlineText ? 1 : 0; } - // We can't know if the attachments are required or if the user added any in web. - return -1; + if (!uploadFilesSupported) { + // We can't know if the attachments are required or if the user added any in web. + return -1; + } + + const questionComponentId = CoreQuestion.instance.getQuestionComponentId(question, componentId); + const attachments = CoreFileSession.instance.getFiles(component, questionComponentId); + + if (!allowsInlineText) { + return attachments && attachments.length > 0 ? 1 : 0; + } + + // If any of the fields is missing return -1 because we can't know if they're required or not. + return hasInlineText && attachments && attachments.length > 0 ? 1 : -1; } /** @@ -125,10 +163,30 @@ export class AddonQtypeEssayHandler implements CoreQuestionHandler { * @param question Question. * @param prevAnswers Object with the previous question answers. * @param newAnswers Object with the new question answers. + * @param component The component the question is related to. + * @param componentId Component ID. * @return Whether they're the same. */ - isSameResponse(question: any, prevAnswers: any, newAnswers: any): boolean { - return this.utils.sameAtKeyMissingIsBlank(prevAnswers, newAnswers, 'answer'); + isSameResponse(question: any, prevAnswers: any, newAnswers: any, component: string, componentId: string | number): boolean { + const element = this.domUtils.convertToElement(question.html); + const allowsInlineText = !!element.querySelector('textarea[name*=_answer]'); + const allowsAttachments = !!element.querySelector('div[id*=filemanager]'); + const uploadFilesSupported = typeof question.responsefileareas != 'undefined'; + + // First check the inline text. + const answerIsEqual = allowsInlineText ? this.utils.sameAtKeyMissingIsBlank(prevAnswers, newAnswers, 'answer') : true; + + if (!allowsAttachments || !uploadFilesSupported || !answerIsEqual) { + // No need to check attachments. + return answerIsEqual; + } + + // Check attachments now. + const questionComponentId = CoreQuestion.instance.getQuestionComponentId(question, componentId); + const attachments = CoreFileSession.instance.getFiles(component, questionComponentId); + const originalAttachments = this.questionHelper.getResponseFileAreaFiles(question, 'attachments'); + + return CoreFileUploader.instance.areFileListDifferent(attachments, originalAttachments); } /** @@ -137,11 +195,16 @@ export class AddonQtypeEssayHandler implements CoreQuestionHandler { * @param question Question. * @param answers The answers retrieved from the form. Prepared answers must be stored in this object. * @param offline Whether the data should be saved in offline. + * @param component The component the question is related to. + * @param componentId Component ID. * @param siteId Site ID. If not defined, current site. * @return Return a promise resolved when done if async, void if sync. */ - async prepareAnswers(question: any, answers: any, offline: boolean, siteId?: string): Promise { + async prepareAnswers(question: any, answers: any, offline: boolean, component: string, componentId: string | number, + siteId?: string): Promise { + const element = this.domUtils.convertToElement(question.html); + const attachmentsInput = element.querySelector('.attachments input[name*=_attachments]'); // Search the textarea to get its name. const textarea = element.querySelector('textarea[name*=_answer]'); @@ -158,5 +221,18 @@ export class AddonQtypeEssayHandler implements CoreQuestionHandler { // Add some HTML to the text if needed. answers[textarea.name] = this.textUtils.formatHtmlLines(answers[textarea.name]); } + + if (attachmentsInput) { + // Treat attachments if any. + const questionComponentId = CoreQuestion.instance.getQuestionComponentId(question, componentId); + const attachments = CoreFileSession.instance.getFiles(component, questionComponentId); + const draftId = Number(attachmentsInput.value); + + if (offline) { + // @TODO Support offline. + } else { + await CoreFileUploader.instance.uploadFiles(draftId, attachments, siteId); + } + } } } diff --git a/src/addon/qtype/gapselect/providers/handler.ts b/src/addon/qtype/gapselect/providers/handler.ts index 50b83068e..ef252a85a 100644 --- a/src/addon/qtype/gapselect/providers/handler.ts +++ b/src/addon/qtype/gapselect/providers/handler.ts @@ -61,9 +61,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 complete, 0 if not complete, -1 if cannot determine. */ - isCompleteResponse(question: any, answers: any): number { + isCompleteResponse(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]; @@ -110,9 +112,11 @@ export class AddonQtypeGapSelectHandler implements CoreQuestionHandler { * @param question Question. * @param prevAnswers Object with the previous question answers. * @param newAnswers Object with the new question answers. + * @param component The component the question is related to. + * @param componentId Component ID. * @return Whether they're the same. */ - isSameResponse(question: any, prevAnswers: any, newAnswers: any): boolean { + isSameResponse(question: any, prevAnswers: any, newAnswers: any, component: string, componentId: string | number): boolean { return this.questionProvider.compareAllAnswers(prevAnswers, newAnswers); } } diff --git a/src/addon/qtype/match/providers/handler.ts b/src/addon/qtype/match/providers/handler.ts index 90d87d1d1..3f147aed2 100644 --- a/src/addon/qtype/match/providers/handler.ts +++ b/src/addon/qtype/match/providers/handler.ts @@ -61,9 +61,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 complete, 0 if not complete, -1 if cannot determine. */ - isCompleteResponse(question: any, answers: any): number { + isCompleteResponse(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]; @@ -110,9 +112,11 @@ export class AddonQtypeMatchHandler implements CoreQuestionHandler { * @param question Question. * @param prevAnswers Object with the previous question answers. * @param newAnswers Object with the new question answers. + * @param component The component the question is related to. + * @param componentId Component ID. * @return Whether they're the same. */ - isSameResponse(question: any, prevAnswers: any, newAnswers: any): boolean { + isSameResponse(question: any, prevAnswers: any, newAnswers: any, component: string, componentId: string | number): boolean { return this.questionProvider.compareAllAnswers(prevAnswers, newAnswers); } } diff --git a/src/addon/qtype/multianswer/providers/handler.ts b/src/addon/qtype/multianswer/providers/handler.ts index 798dcf572..64d9c39f6 100644 --- a/src/addon/qtype/multianswer/providers/handler.ts +++ b/src/addon/qtype/multianswer/providers/handler.ts @@ -62,9 +62,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 complete, 0 if not complete, -1 if cannot determine. */ - isCompleteResponse(question: any, answers: any): number { + isCompleteResponse(question: any, answers: any, component: string, componentId: string | number): number { // Get all the inputs in the question to check if they've all been answered. const names = this.questionProvider.getBasicAnswers(this.questionHelper.getAllInputNamesFromHtml(question.html)); for (const name in names) { @@ -112,9 +114,11 @@ export class AddonQtypeMultiAnswerHandler implements CoreQuestionHandler { * @param question Question. * @param prevAnswers Object with the previous question answers. * @param newAnswers Object with the new question answers. + * @param component The component the question is related to. + * @param componentId Component ID. * @return Whether they're the same. */ - isSameResponse(question: any, prevAnswers: any, newAnswers: any): boolean { + isSameResponse(question: any, prevAnswers: any, newAnswers: any, component: string, componentId: string | number): boolean { return this.questionProvider.compareAllAnswers(prevAnswers, newAnswers); } diff --git a/src/addon/qtype/multichoice/providers/handler.ts b/src/addon/qtype/multichoice/providers/handler.ts index 92424fbd2..ce49d8bed 100644 --- a/src/addon/qtype/multichoice/providers/handler.ts +++ b/src/addon/qtype/multichoice/providers/handler.ts @@ -45,9 +45,11 @@ 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 complete, 0 if not complete, -1 if cannot determine. */ - isCompleteResponse(question: any, answers: any): number { + isCompleteResponse(question: any, answers: any, component: string, componentId: string | number): number { let isSingle = true, isMultiComplete = false; @@ -98,7 +100,7 @@ export class AddonQtypeMultichoiceHandler implements CoreQuestionHandler { * @return 1 if gradable, 0 if not gradable, -1 if cannot determine. */ isGradableResponse(question: any, answers: any): number { - return this.isCompleteResponse(question, answers); + return this.isCompleteResponse(question, answers, null, null); } /** @@ -118,9 +120,11 @@ export class AddonQtypeMultichoiceHandler implements CoreQuestionHandler { * @param question Question. * @param prevAnswers Object with the previous question answers. * @param newAnswers Object with the new question answers. + * @param component The component the question is related to. + * @param componentId Component ID. * @return Whether they're the same. */ - isSameResponse(question: any, prevAnswers: any, newAnswers: any): boolean { + isSameResponse(question: any, prevAnswers: any, newAnswers: any, component: string, componentId: string | number): boolean { let isSingle = true, isMultiSame = true; @@ -158,10 +162,13 @@ export class AddonQtypeMultichoiceHandler implements CoreQuestionHandler { * @param question Question. * @param answers The answers retrieved from the form. Prepared answers must be stored in this object. * @param offline Whether the data should be saved in offline. + * @param component The component the question is related to. + * @param componentId Component ID. * @param siteId Site ID. If not defined, current site. * @return Return a promise resolved when done if async, void if sync. */ - prepareAnswers(question: any, answers: any, offline: boolean, siteId?: string): void | Promise { + prepareAnswers(question: any, answers: any, offline: boolean, component: string, componentId: string | number, siteId?: string) + : void | Promise { if (question && !question.multi && typeof answers[question.optionsName] != 'undefined' && !answers[question.optionsName]) { /* It's a single choice and the user hasn't answered. Delete the answer because sending an empty string (default value) will mark the first option as selected. */ diff --git a/src/addon/qtype/randomsamatch/providers/handler.ts b/src/addon/qtype/randomsamatch/providers/handler.ts index a0eede940..34db08724 100644 --- a/src/addon/qtype/randomsamatch/providers/handler.ts +++ b/src/addon/qtype/randomsamatch/providers/handler.ts @@ -46,11 +46,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 complete, 0 if not complete, -1 if cannot determine. */ - isCompleteResponse(question: any, answers: any): number { + isCompleteResponse(question: any, answers: any, component: string, componentId: string | number): number { // This question behaves like a match question. - return this.matchHandler.isCompleteResponse(question, answers); + return this.matchHandler.isCompleteResponse(question, answers, component, componentId); } /** @@ -81,10 +83,12 @@ export class AddonQtypeRandomSaMatchHandler implements CoreQuestionHandler { * @param question Question. * @param prevAnswers Object with the previous question answers. * @param newAnswers Object with the new question answers. + * @param component The component the question is related to. + * @param componentId Component ID. * @return Whether they're the same. */ - isSameResponse(question: any, prevAnswers: any, newAnswers: any): boolean { + isSameResponse(question: any, prevAnswers: any, newAnswers: any, component: string, componentId: string | number): boolean { // This question behaves like a match question. - return this.matchHandler.isSameResponse(question, prevAnswers, newAnswers); + return this.matchHandler.isSameResponse(question, prevAnswers, newAnswers, component, componentId); } } diff --git a/src/addon/qtype/shortanswer/providers/handler.ts b/src/addon/qtype/shortanswer/providers/handler.ts index f9629fa75..b8cef9466 100644 --- a/src/addon/qtype/shortanswer/providers/handler.ts +++ b/src/addon/qtype/shortanswer/providers/handler.ts @@ -45,9 +45,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 complete, 0 if not complete, -1 if cannot determine. */ - isCompleteResponse(question: any, answers: any): number { + isCompleteResponse(question: any, answers: any, component: string, componentId: string | number): number { return (answers['answer'] || answers['answer'] === 0) ? 1 : 0; } @@ -69,7 +71,7 @@ export class AddonQtypeShortAnswerHandler implements CoreQuestionHandler { * @return 1 if gradable, 0 if not gradable, -1 if cannot determine. */ isGradableResponse(question: any, answers: any): number { - return this.isCompleteResponse(question, answers); + return this.isCompleteResponse(question, answers, null, null); } /** @@ -78,9 +80,11 @@ export class AddonQtypeShortAnswerHandler implements CoreQuestionHandler { * @param question Question. * @param prevAnswers Object with the previous question answers. * @param newAnswers Object with the new question answers. + * @param component The component the question is related to. + * @param componentId Component ID. * @return Whether they're the same. */ - isSameResponse(question: any, prevAnswers: any, newAnswers: any): boolean { + isSameResponse(question: any, prevAnswers: any, newAnswers: any, component: string, componentId: string | number): boolean { return this.utils.sameAtKeyMissingIsBlank(prevAnswers, newAnswers, 'answer'); } } diff --git a/src/addon/qtype/truefalse/providers/handler.ts b/src/addon/qtype/truefalse/providers/handler.ts index f811dfa65..48ce82549 100644 --- a/src/addon/qtype/truefalse/providers/handler.ts +++ b/src/addon/qtype/truefalse/providers/handler.ts @@ -46,9 +46,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 complete, 0 if not complete, -1 if cannot determine. */ - isCompleteResponse(question: any, answers: any): number { + isCompleteResponse(question: any, answers: any, component: string, componentId: string | number): number { return answers['answer'] ? 1 : 0; } @@ -70,7 +72,7 @@ export class AddonQtypeTrueFalseHandler implements CoreQuestionHandler { * @return 1 if gradable, 0 if not gradable, -1 if cannot determine. */ isGradableResponse(question: any, answers: any): number { - return this.isCompleteResponse(question, answers); + return this.isCompleteResponse(question, answers, null, null); } /** @@ -79,9 +81,11 @@ export class AddonQtypeTrueFalseHandler implements CoreQuestionHandler { * @param question Question. * @param prevAnswers Object with the previous question answers. * @param newAnswers Object with the new question answers. + * @param component The component the question is related to. + * @param componentId Component ID. * @return Whether they're the same. */ - isSameResponse(question: any, prevAnswers: any, newAnswers: any): boolean { + isSameResponse(question: any, prevAnswers: any, newAnswers: any, component: string, componentId: string | number): boolean { return this.utils.sameAtKeyMissingIsBlank(prevAnswers, newAnswers, 'answer'); } @@ -91,10 +95,13 @@ export class AddonQtypeTrueFalseHandler implements CoreQuestionHandler { * @param question Question. * @param answers The answers retrieved from the form. Prepared answers must be stored in this object. * @param offline Whether the data should be saved in offline. + * @param component The component the question is related to. + * @param componentId Component ID. * @param siteId Site ID. If not defined, current site. * @return Return a promise resolved when done if async, void if sync. */ - prepareAnswers(question: any, answers: any, offline: boolean, siteId?: string): void | Promise { + prepareAnswers(question: any, answers: any, offline: boolean, component: string, componentId: string | number, siteId?: string) + : void | Promise { if (question && typeof answers[question.optionsName] != 'undefined' && !answers[question.optionsName]) { // The user hasn't answered. Delete the answer to prevent marking one of the answers automatically. delete answers[question.optionsName]; diff --git a/src/core/fileuploader/providers/fileuploader.ts b/src/core/fileuploader/providers/fileuploader.ts index 2caac3c0c..a12d44987 100644 --- a/src/core/fileuploader/providers/fileuploader.ts +++ b/src/core/fileuploader/providers/fileuploader.ts @@ -15,6 +15,7 @@ import { Injectable } from '@angular/core'; import { ModalController } from 'ionic-angular'; import { Camera, CameraOptions } from '@ionic-native/camera'; +import { FileEntry } from '@ionic-native/file'; import { MediaCapture, MediaFile, CaptureError, CaptureAudioOptions, CaptureVideoOptions } from '@ionic-native/media-capture'; import { TranslateService } from '@ngx-translate/core'; import { CoreFileProvider } from '@providers/file'; @@ -25,9 +26,10 @@ import { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreUtilsProvider } from '@providers/utils/utils'; -import { CoreWSFileUploadOptions } from '@providers/ws'; +import { CoreWSFileUploadOptions, CoreWSExternalFile } from '@providers/ws'; import { Subject } from 'rxjs'; import { CoreApp } from '@providers/app'; +import { makeSingleton } from '@singletons/core.singletons'; /** * File upload options. @@ -96,7 +98,7 @@ export class CoreFileUploaderProvider { // Currently we are going to compare the order of the files as well. // This function can be improved comparing more fields or not comparing the order. for (let i = 0; i < a.length; i++) { - if ((a[i].name || a[i].filename) != (b[i].name || b[i].filename)) { + if (a[i].name != b[i].name || a[i].filename != b[i].filename) { return true; } } @@ -534,6 +536,37 @@ export class CoreFileUploaderProvider { }); } + /** + * Given a list of files (either online files or local files), upload the local files to the draft area. + * Local files are not deleted from the device after upload. + * + * @param itemId Draft ID. + * @param files List of files. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with the itemId. + */ + async uploadFiles(itemId: number, files: (CoreWSExternalFile | FileEntry)[], siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + if (!files || !files.length) { + return; + } + + await Promise.all(files.map(async (file) => { + if (( file).filename && !( file).name) { + // File already uploaded, ignore it. + return; + } + + file = file; + + // Now upload the file. + const options = this.getFileUploadOptions(file.toURL(), file.name, undefined, false, 'draft', itemId); + + await this.uploadFile(file.toURL(), options, undefined, siteId); + })); + } + /** * Upload a file to a draft area and return the draft ID. * @@ -615,3 +648,5 @@ export class CoreFileUploaderProvider { }); } } + +export class CoreFileUploader extends makeSingleton(CoreFileUploaderProvider) {} diff --git a/src/core/question/classes/base-behaviour-handler.ts b/src/core/question/classes/base-behaviour-handler.ts index 852d7dfc5..50e3c7229 100644 --- a/src/core/question/classes/base-behaviour-handler.ts +++ b/src/core/question/classes/base-behaviour-handler.ts @@ -34,10 +34,11 @@ export class CoreQuestionBehaviourBaseHandler implements CoreQuestionBehaviourHa * @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. * @return New state (or promise resolved with state). */ - determineNewState(component: string, attemptId: number, question: any, siteId?: string) + determineNewState(component: string, attemptId: number, question: any, componentId: string | number, siteId?: string) : CoreQuestionState | Promise { // Return the current state. return this.questionProvider.getState(question.state); diff --git a/src/core/question/classes/base-question-handler.ts b/src/core/question/classes/base-question-handler.ts index 745536c5a..233704d78 100644 --- a/src/core/question/classes/base-question-handler.ts +++ b/src/core/question/classes/base-question-handler.ts @@ -79,9 +79,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 complete, 0 if not complete, -1 if cannot determine. */ - isCompleteResponse(question: any, answers: any): number { + isCompleteResponse(question: any, answers: any, component: string, componentId: string | number): number { return -1; } @@ -103,9 +105,11 @@ export class CoreQuestionBaseHandler implements CoreQuestionHandler { * @param question Question. * @param prevAnswers Object with the previous question answers. * @param newAnswers Object with the new question answers. + * @param component The component the question is related to. + * @param componentId Component ID. * @return Whether they're the same. */ - isSameResponse(question: any, prevAnswers: any, newAnswers: any): boolean { + isSameResponse(question: any, prevAnswers: any, newAnswers: any, component: string, componentId: string | number): boolean { return false; } @@ -115,10 +119,13 @@ export class CoreQuestionBaseHandler implements CoreQuestionHandler { * @param question Question. * @param answers The answers retrieved from the form. Prepared answers must be stored in this object. * @param offline Whether the data should be saved in offline. + * @param component The component the question is related to. + * @param componentId Component ID. * @param siteId Site ID. If not defined, current site. * @return Return a promise resolved when done if async, void if sync. */ - prepareAnswers(question: any, answers: any, offline: boolean, siteId?: string): void | Promise { + prepareAnswers(question: any, answers: any, offline: boolean, component: string, componentId: string | number, siteId?: string) + : void | Promise { // Nothing to do. } diff --git a/src/core/question/providers/behaviour-delegate.ts b/src/core/question/providers/behaviour-delegate.ts index db0d052de..c0c9dd717 100644 --- a/src/core/question/providers/behaviour-delegate.ts +++ b/src/core/question/providers/behaviour-delegate.ts @@ -36,10 +36,11 @@ export interface CoreQuestionBehaviourHandler extends CoreDelegateHandler { * @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. * @return State (or promise resolved with state). */ - determineNewState?(component: string, attemptId: number, question: any, siteId?: string) + determineNewState?(component: string, attemptId: number, question: any, componentId: string | number, siteId?: string) : CoreQuestionState | Promise; /** @@ -74,15 +75,16 @@ export class CoreQuestionBehaviourDelegate extends CoreDelegate { * @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. * @return Promise resolved with state. */ - determineNewState(behaviour: string, component: string, attemptId: number, question: any, siteId?: string) - : Promise { + determineNewState(behaviour: string, component: string, attemptId: number, question: any, componentId: string | number, + siteId?: string): Promise { behaviour = this.questionDelegate.getBehaviourForQuestion(question, behaviour); return Promise.resolve(this.executeFunctionOnEnabled(behaviour, 'determineNewState', - [component, attemptId, question, siteId])); + [component, attemptId, question, componentId, siteId])); } /** diff --git a/src/core/question/providers/delegate.ts b/src/core/question/providers/delegate.ts index 3d53a059c..da8529180 100644 --- a/src/core/question/providers/delegate.ts +++ b/src/core/question/providers/delegate.ts @@ -62,9 +62,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 complete, 0 if not complete, -1 if cannot determine. */ - isCompleteResponse?(question: any, answers: any): number; + isCompleteResponse?(question: any, answers: any, component: string, componentId: string | number): number; /** * Check if a student has provided enough of an answer for the question to be graded automatically, @@ -84,7 +86,7 @@ export interface CoreQuestionHandler extends CoreDelegateHandler { * @param newAnswers Object with the new question answers. * @return Whether they're the same. */ - isSameResponse?(question: any, prevAnswers: any, newAnswers: any): boolean; + isSameResponse?(question: any, prevAnswers: any, newAnswers: any, component: string, componentId: string | number): boolean; /** * Prepare and add to answers the data to send to server based in the input. Return promise if async. @@ -92,10 +94,13 @@ export interface CoreQuestionHandler extends CoreDelegateHandler { * @param question Question. * @param answers The answers retrieved from the form. Prepared answers must be stored in this object. * @param offline Whether the data should be saved in offline. + * @param component The component the question is related to. + * @param componentId Component ID. * @param siteId Site ID. If not defined, current site. * @return Return a promise resolved when done if async, void if sync. */ - prepareAnswers?(question: any, answers: any, offline: boolean, siteId?: string): void | Promise; + prepareAnswers?(question: any, answers: any, offline: boolean, component: string, componentId: string | number, + siteId?: string): void | Promise; /** * Validate if an offline sequencecheck is valid compared with the online one. @@ -115,6 +120,15 @@ export interface CoreQuestionHandler extends CoreDelegateHandler { * @return List of URLs. */ getAdditionalDownloadableFiles?(question: any, usageId: number): string[]; + + /** + * Clear temporary data after the data has been saved. + * + * @param question Question. + * @param component The component the question is related to. + * @param componentId Component ID. + */ + clearTmpData?(question: any, component: string, componentId: string | number): void | Promise; } /** @@ -196,12 +210,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 complete, 0 if not complete, -1 if cannot determine. */ - isCompleteResponse(question: any, answers: any): number { + isCompleteResponse(question: any, answers: any, component: string, componentId: string | number): number { const type = this.getTypeName(question); - return this.executeFunctionOnEnabled(type, 'isCompleteResponse', [question, answers]); + return this.executeFunctionOnEnabled(type, 'isCompleteResponse', [question, answers, component, componentId]); } /** @@ -226,10 +242,10 @@ export class CoreQuestionDelegate extends CoreDelegate { * @param newAnswers Object with the new question answers. * @return Whether they're the same. */ - isSameResponse(question: any, prevAnswers: any, newAnswers: any): boolean { + isSameResponse(question: any, prevAnswers: any, newAnswers: any, component: string, componentId: string | number): boolean { const type = this.getTypeName(question); - return this.executeFunctionOnEnabled(type, 'isSameResponse', [question, prevAnswers, newAnswers]); + return this.executeFunctionOnEnabled(type, 'isSameResponse', [question, prevAnswers, newAnswers, component, componentId]); } /** @@ -248,13 +264,17 @@ export class CoreQuestionDelegate extends CoreDelegate { * @param question Question. * @param answers The answers retrieved from the form. Prepared answers must be stored in this object. * @param offline Whether the data should be saved in offline. + * @param component The component the question is related to. + * @param componentId Component ID. * @param siteId Site ID. If not defined, current site. * @return Promise resolved when data has been prepared. */ - prepareAnswersForQuestion(question: any, answers: any, offline: boolean, siteId?: string): Promise { + prepareAnswersForQuestion(question: any, answers: any, offline: boolean, component: string, componentId: string | number, + siteId?: string): Promise { const type = this.getTypeName(question); - return Promise.resolve(this.executeFunctionOnEnabled(type, 'prepareAnswers', [question, answers, offline, siteId])); + return Promise.resolve(this.executeFunctionOnEnabled(type, 'prepareAnswers', + [question, answers, offline, component, componentId, siteId])); } /** @@ -282,4 +302,17 @@ export class CoreQuestionDelegate extends CoreDelegate { return this.executeFunctionOnEnabled(type, 'getAdditionalDownloadableFiles', [question, usageId]) || []; } + + /** + * Clear temporary data after the data has been saved. + * + * @param question Question. + * @param component The component the question is related to. + * @param componentId Component ID. + */ + clearTmpData(question: any, component: string, componentId: string | number): void | Promise { + const type = this.getTypeName(question); + + return this.executeFunctionOnEnabled(type, 'clearTmpData', [question, component, componentId]); + } } diff --git a/src/core/question/providers/helper.ts b/src/core/question/providers/helper.ts index de5c9111e..21e4c5a0d 100644 --- a/src/core/question/providers/helper.ts +++ b/src/core/question/providers/helper.ts @@ -60,6 +60,22 @@ export class CoreQuestionHelperProvider { }); } + /** + * Clear questions temporary data after the data has been saved. + * + * @param questions The list of questions. + * @param component The component the question is related to. + * @param componentId Component ID. + * @return Promise resolved when done. + */ + async clearTmpData(questions: any[], component: string, componentId: string | number): Promise { + questions = questions || []; + + await Promise.all(questions.map(async (question) => { + await this.questionDelegate.clearTmpData(question, component, componentId); + })); + } + /** * Extract question behaviour submit buttons from the question's HTML and add them to "behaviourButtons" property. * The buttons aren't deleted from the content because all the im-controls block will be removed afterwards. @@ -541,7 +557,7 @@ export class CoreQuestionHelperProvider { if (!component) { component = CoreQuestionProvider.COMPONENT; - componentId = question.id; + componentId = question.number; } urls.push(...this.questionDelegate.getAdditionalDownloadableFiles(question, usageId)); @@ -572,15 +588,19 @@ export class CoreQuestionHelperProvider { * @param questions The list of questions. * @param answers The input data. * @param offline True if data should be saved in offline. + * @param component The component the question is related to. + * @param componentId Component ID. * @param siteId Site ID. If not defined, current site. * @return Promise resolved with answers to send to server. */ - prepareAnswers(questions: any[], answers: any, offline?: boolean, siteId?: string): Promise { + prepareAnswers(questions: any[], answers: any, offline: boolean, component: string, componentId: string | number, + siteId?: string): Promise { const promises = []; questions = questions || []; questions.forEach((question) => { - promises.push(this.questionDelegate.prepareAnswersForQuestion(question, answers, offline, siteId)); + promises.push(this.questionDelegate.prepareAnswersForQuestion(question, answers, offline, component, componentId, + siteId)); }); return this.utils.allPromises(promises).then(() => { diff --git a/src/core/question/providers/question.ts b/src/core/question/providers/question.ts index 30e2ef73b..0a9945fda 100644 --- a/src/core/question/providers/question.ts +++ b/src/core/question/providers/question.ts @@ -13,10 +13,13 @@ // limitations under the License. import { Injectable } from '@angular/core'; +import { CoreFile } from '@providers/file'; import { CoreLoggerProvider } from '@providers/logger'; import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites'; +import { CoreTextUtils } from '@providers/utils/text'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreUtilsProvider } from '@providers/utils/utils'; +import { makeSingleton } from '@singletons/core.singletons'; /** * An object to represent a question state. @@ -413,6 +416,35 @@ export class CoreQuestionProvider { }); } + /** + * Given a question and a componentId, return a componentId that is unique for the question. + * + * @param question Question. + * @param componentId Component ID. + * @return Question component ID. + */ + getQuestionComponentId(question: any, componentId: string | number): string { + return componentId + '_' + question.number; + } + + /** + * Get the path to the folder where to store files for an offline question. + * + * @param type Question type. + * @param component Component the question is related to. + * @param componentId Question component ID, returned by getQuestionComponentId. + * @param siteId Site ID. If not defined, current site. + * @return Folder path. + */ + getQuestionFolder(type: string, component: string, componentId: string, siteId?: string): string { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + const siteFolderPath = CoreFile.instance.getSiteFolder(siteId); + const questionFolderPath = 'offlinequestion/' + type + '/' + component + '/' + componentId; + + return CoreTextUtils.instance.concatenatePaths(siteFolderPath, questionFolderPath); + } + /** * Extract the question slot from a question name. * @@ -612,3 +644,5 @@ export class CoreQuestionProvider { }); } } + +export class CoreQuestion extends makeSingleton(CoreQuestionProvider) {}