From be573d240b080b64ec27e6efad9a60282976f846 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Mon, 19 Mar 2018 12:13:45 +0100 Subject: [PATCH] MOBILE-2389 qtype: Implement description and multichoices types --- .../calculatedmulti/calculatedmulti.module.ts | 30 ++++ .../calculatedmulti/providers/handler.ts | 90 ++++++++++ .../description/component/description.html | 7 + .../description/component/description.ts | 50 ++++++ .../qtype/description/description.module.ts | 46 ++++++ .../qtype/description/providers/handler.ts | 77 +++++++++ .../multichoice/component/multichoice.html | 25 +++ .../multichoice/component/multichoice.ts | 40 +++++ .../qtype/multichoice/multichoice.module.ts | 46 ++++++ .../qtype/multichoice/providers/handler.ts | 154 ++++++++++++++++++ src/addon/qtype/qtype.module.ts | 10 +- .../qtype/truefalse/providers/handler.ts | 87 ++++++++++ src/addon/qtype/truefalse/truefalse.module.ts | 30 ++++ .../classes/base-question-component.ts | 91 +++++++++++ 14 files changed, 782 insertions(+), 1 deletion(-) create mode 100644 src/addon/qtype/calculatedmulti/calculatedmulti.module.ts create mode 100644 src/addon/qtype/calculatedmulti/providers/handler.ts create mode 100644 src/addon/qtype/description/component/description.html create mode 100644 src/addon/qtype/description/component/description.ts create mode 100644 src/addon/qtype/description/description.module.ts create mode 100644 src/addon/qtype/description/providers/handler.ts create mode 100644 src/addon/qtype/multichoice/component/multichoice.html create mode 100644 src/addon/qtype/multichoice/component/multichoice.ts create mode 100644 src/addon/qtype/multichoice/multichoice.module.ts create mode 100644 src/addon/qtype/multichoice/providers/handler.ts create mode 100644 src/addon/qtype/truefalse/providers/handler.ts create mode 100644 src/addon/qtype/truefalse/truefalse.module.ts diff --git a/src/addon/qtype/calculatedmulti/calculatedmulti.module.ts b/src/addon/qtype/calculatedmulti/calculatedmulti.module.ts new file mode 100644 index 000000000..a5e7ee3eb --- /dev/null +++ b/src/addon/qtype/calculatedmulti/calculatedmulti.module.ts @@ -0,0 +1,30 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { NgModule } from '@angular/core'; +import { CoreQuestionDelegate } from '@core/question/providers/delegate'; +import { AddonQtypeCalculatedMultiHandler } from './providers/handler'; + +@NgModule({ + declarations: [ + ], + providers: [ + AddonQtypeCalculatedMultiHandler + ] +}) +export class AddonQtypeCalculatedMultiModule { + constructor(questionDelegate: CoreQuestionDelegate, handler: AddonQtypeCalculatedMultiHandler) { + questionDelegate.registerHandler(handler); + } +} diff --git a/src/addon/qtype/calculatedmulti/providers/handler.ts b/src/addon/qtype/calculatedmulti/providers/handler.ts new file mode 100644 index 000000000..3cbe18057 --- /dev/null +++ b/src/addon/qtype/calculatedmulti/providers/handler.ts @@ -0,0 +1,90 @@ + +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable, Injector } from '@angular/core'; +import { CoreQuestionHandler } from '@core/question/providers/delegate'; +import { AddonQtypeMultichoiceHandler } from '@addon/qtype/multichoice/providers/handler'; +import { AddonQtypeMultichoiceComponent } from '@addon/qtype/multichoice/component/multichoice'; + +/** + * Handler to support calculated multi question type. + */ +@Injectable() +export class AddonQtypeCalculatedMultiHandler implements CoreQuestionHandler { + name = 'AddonQtypeCalculatedMulti'; + type = 'qtype_calculatedmulti'; + + constructor(private multichoiceHandler: AddonQtypeMultichoiceHandler) { } + + /** + * Return the Component to use to display the question. + * It's recommended to return the class of the component, but you can also return an instance of the component. + * + * @param {Injector} injector Injector. + * @param {any} question The question to render. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. + */ + getComponent(injector: Injector, question: any): any | Promise { + // Calculated multi behaves like a multichoice, use the same component. + return AddonQtypeMultichoiceComponent; + } + + /** + * Check if a response is complete. + * + * @param {any} question The question. + * @param {any} answers Object with the question answers (without prefix). + * @return {number} 1 if complete, 0 if not complete, -1 if cannot determine. + */ + isCompleteResponse(question: any, answers: any): number { + // This question type depends on multichoice. + return this.multichoiceHandler.isCompleteResponseSingle(answers); + } + + /** + * Whether or not the handler is enabled on a site level. + * + * @return {boolean|Promise} True or promise resolved with true if enabled. + */ + isEnabled(): boolean | Promise { + return true; + } + + /** + * Check if a student has provided enough of an answer for the question to be graded automatically, + * or whether it must be considered aborted. + * + * @param {any} question The question. + * @param {any} answers Object with the question answers (without prefix). + * @return {number} 1 if gradable, 0 if not gradable, -1 if cannot determine. + */ + isGradableResponse(question: any, answers: any): number { + // This question type depends on multichoice. + return this.multichoiceHandler.isGradableResponseSingle(answers); + } + + /** + * Check if two responses are the same. + * + * @param {any} question Question. + * @param {any} prevAnswers Object with the previous question answers. + * @param {any} newAnswers Object with the new question answers. + * @return {boolean} Whether they're the same. + */ + isSameResponse(question: any, prevAnswers: any, newAnswers: any): boolean { + // This question type depends on multichoice. + return this.multichoiceHandler.isSameResponseSingle(prevAnswers, newAnswers); + } +} diff --git a/src/addon/qtype/description/component/description.html b/src/addon/qtype/description/component/description.html new file mode 100644 index 000000000..74d4f190d --- /dev/null +++ b/src/addon/qtype/description/component/description.html @@ -0,0 +1,7 @@ +
+ + + +

+
+
diff --git a/src/addon/qtype/description/component/description.ts b/src/addon/qtype/description/component/description.ts new file mode 100644 index 000000000..f08b7dc82 --- /dev/null +++ b/src/addon/qtype/description/component/description.ts @@ -0,0 +1,50 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit } from '@angular/core'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreQuestionHelperProvider } from '@core/question/providers/helper'; +import { CoreQuestionBaseComponent } from '@core/question/classes/base-question-component'; + +/** + * Component to render a description question. + */ +@Component({ + selector: 'addon-qtype-description', + templateUrl: 'description.html' +}) +export class AddonQtypeDescriptionComponent extends CoreQuestionBaseComponent implements OnInit { + + constructor(logger: CoreLoggerProvider, questionHelper: CoreQuestionHelperProvider, domUtils: CoreDomUtilsProvider) { + super(logger, 'AddonQtypeDescriptionComponent', questionHelper, domUtils); + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + const questionDiv = this.initComponent(); + if (questionDiv) { + // Get the "seen" hidden input. + const input = questionDiv.querySelector('input[type="hidden"][name*=seen]'); + if (input) { + this.question.seenInput = { + name: input.name, + value: input.value + }; + } + } + } +} diff --git a/src/addon/qtype/description/description.module.ts b/src/addon/qtype/description/description.module.ts new file mode 100644 index 000000000..fa64bb94d --- /dev/null +++ b/src/addon/qtype/description/description.module.ts @@ -0,0 +1,46 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { NgModule } from '@angular/core'; +import { IonicModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreQuestionDelegate } from '@core/question/providers/delegate'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { AddonQtypeDescriptionHandler } from './providers/handler'; +import { AddonQtypeDescriptionComponent } from './component/description'; + +@NgModule({ + declarations: [ + AddonQtypeDescriptionComponent + ], + imports: [ + IonicModule, + TranslateModule.forChild(), + CoreDirectivesModule + ], + providers: [ + AddonQtypeDescriptionHandler + ], + exports: [ + AddonQtypeDescriptionComponent + ], + entryComponents: [ + AddonQtypeDescriptionComponent + ] +}) +export class AddonQtypeDescriptionModule { + constructor(questionDelegate: CoreQuestionDelegate, handler: AddonQtypeDescriptionHandler) { + questionDelegate.registerHandler(handler); + } +} diff --git a/src/addon/qtype/description/providers/handler.ts b/src/addon/qtype/description/providers/handler.ts new file mode 100644 index 000000000..49bba134c --- /dev/null +++ b/src/addon/qtype/description/providers/handler.ts @@ -0,0 +1,77 @@ + +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable, Injector } from '@angular/core'; +import { CoreQuestionHandler } from '@core/question/providers/delegate'; +import { AddonQtypeDescriptionComponent } from '../component/description'; + +/** + * Handler to support description question type. + */ +@Injectable() +export class AddonQtypeDescriptionHandler implements CoreQuestionHandler { + name = 'AddonQtypeDescription'; + type = 'qtype_description'; + + constructor() { + // Nothing to do. + } + + /** + * 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. + * + * @param {any} question The question. + * @param {string} behaviour The default behaviour. + * @return {string} The behaviour to use. + */ + getBehaviour(question: any, behaviour: string): string { + return 'informationitem'; + } + + /** + * Return the Component to use to display the question. + * It's recommended to return the class of the component, but you can also return an instance of the component. + * + * @param {Injector} injector Injector. + * @param {any} question The question to render. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. + */ + getComponent(injector: Injector, question: any): any | Promise { + return AddonQtypeDescriptionComponent; + } + + /** + * Whether or not the handler is enabled on a site level. + * + * @return {boolean|Promise} True or promise resolved with true if enabled. + */ + isEnabled(): boolean | Promise { + return true; + } + + /** + * Validate if an offline sequencecheck is valid compared with the online one. + * This function only needs to be implemented if a specific compare is required. + * + * @param {any} question The question. + * @param {string} offlineSequenceCheck Sequence check stored in offline. + * @return {boolean} Whether sequencecheck is valid. + */ + validateSequenceCheck(question: any, offlineSequenceCheck: string): boolean { + // Descriptions don't have any answer so we'll always treat them as valid. + return true; + } +} diff --git a/src/addon/qtype/multichoice/component/multichoice.html b/src/addon/qtype/multichoice/component/multichoice.html new file mode 100644 index 000000000..ee734dd99 --- /dev/null +++ b/src/addon/qtype/multichoice/component/multichoice.html @@ -0,0 +1,25 @@ +
+ + +

+

+
+ + + + +

+

+
+
+ + +
+ + + +

+
+
+
+
diff --git a/src/addon/qtype/multichoice/component/multichoice.ts b/src/addon/qtype/multichoice/component/multichoice.ts new file mode 100644 index 000000000..add6a7af7 --- /dev/null +++ b/src/addon/qtype/multichoice/component/multichoice.ts @@ -0,0 +1,40 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit } from '@angular/core'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreQuestionHelperProvider } from '@core/question/providers/helper'; +import { CoreQuestionBaseComponent } from '@core/question/classes/base-question-component'; + +/** + * Component to render a multichoice question. + */ +@Component({ + selector: 'addon-qtype-multichoice', + templateUrl: 'multichoice.html' +}) +export class AddonQtypeMultichoiceComponent extends CoreQuestionBaseComponent implements OnInit { + + constructor(logger: CoreLoggerProvider, questionHelper: CoreQuestionHelperProvider, domUtils: CoreDomUtilsProvider) { + super(logger, 'AddonQtypeMultichoiceComponent', questionHelper, domUtils); + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + this.initMultichoiceComponent(); + } +} diff --git a/src/addon/qtype/multichoice/multichoice.module.ts b/src/addon/qtype/multichoice/multichoice.module.ts new file mode 100644 index 000000000..c3d591ba7 --- /dev/null +++ b/src/addon/qtype/multichoice/multichoice.module.ts @@ -0,0 +1,46 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { NgModule } from '@angular/core'; +import { IonicModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreQuestionDelegate } from '@core/question/providers/delegate'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { AddonQtypeMultichoiceHandler } from './providers/handler'; +import { AddonQtypeMultichoiceComponent } from './component/multichoice'; + +@NgModule({ + declarations: [ + AddonQtypeMultichoiceComponent + ], + imports: [ + IonicModule, + TranslateModule.forChild(), + CoreDirectivesModule + ], + providers: [ + AddonQtypeMultichoiceHandler + ], + exports: [ + AddonQtypeMultichoiceComponent + ], + entryComponents: [ + AddonQtypeMultichoiceComponent + ] +}) +export class AddonQtypeMultichoiceModule { + constructor(questionDelegate: CoreQuestionDelegate, handler: AddonQtypeMultichoiceHandler) { + questionDelegate.registerHandler(handler); + } +} diff --git a/src/addon/qtype/multichoice/providers/handler.ts b/src/addon/qtype/multichoice/providers/handler.ts new file mode 100644 index 000000000..8eded4917 --- /dev/null +++ b/src/addon/qtype/multichoice/providers/handler.ts @@ -0,0 +1,154 @@ + +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable, Injector } from '@angular/core'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreQuestionHandler } from '@core/question/providers/delegate'; +import { AddonQtypeMultichoiceComponent } from '../component/multichoice'; + +/** + * Handler to support multichoice question type. + */ +@Injectable() +export class AddonQtypeMultichoiceHandler implements CoreQuestionHandler { + name = 'AddonQtypeMultichoice'; + type = 'qtype_multichoice'; + + constructor(private utils: CoreUtilsProvider) { } + + /** + * Return the Component to use to display the question. + * It's recommended to return the class of the component, but you can also return an instance of the component. + * + * @param {Injector} injector Injector. + * @param {any} question The question to render. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. + */ + getComponent(injector: Injector, question: any): any | Promise { + return AddonQtypeMultichoiceComponent; + } + + /** + * Check if a response is complete. + * + * @param {any} question The question. + * @param {any} answers Object with the question answers (without prefix). + * @return {number} 1 if complete, 0 if not complete, -1 if cannot determine. + */ + isCompleteResponse(question: any, answers: any): number { + let isSingle = true, + isMultiComplete = false; + + // To know if it's single or multi answer we need to search for answers with "choice" in the name. + for (const name in answers) { + if (name.indexOf('choice') != -1) { + isSingle = false; + if (answers[name]) { + isMultiComplete = true; + } + } + } + + if (isSingle) { + // Single. + return this.isCompleteResponseSingle(answers); + } else { + // Multi. + return isMultiComplete ? 1 : 0; + } + } + + /** + * Check if a response is complete. Only for single answer. + * + * @param {any} question The question.uestion answers (without prefix). + * @return {number} 1 if complete, 0 if not complete, -1 if cannot determine. + */ + isCompleteResponseSingle(answers: any): number { + return (answers['answer'] && answers['answer'] !== '') ? 1 : 0; + } + + /** + * Whether or not the handler is enabled on a site level. + * + * @return {boolean|Promise} True or promise resolved with true if enabled. + */ + isEnabled(): boolean | Promise { + return true; + } + + /** + * Check if a student has provided enough of an answer for the question to be graded automatically, + * or whether it must be considered aborted. + * + * @param {any} question The question. + * @param {any} answers Object with the question answers (without prefix). + * @return {number} 1 if gradable, 0 if not gradable, -1 if cannot determine. + */ + isGradableResponse(question: any, answers: any): number { + return this.isCompleteResponse(question, answers); + } + + /** + * Check if a student has provided enough of an answer for the question to be graded automatically, + * or whether it must be considered aborted. Only for single answer. + * + * @param {any} answers Object with the question answers (without prefix). + * @return {number} 1 if gradable, 0 if not gradable, -1 if cannot determine. + */ + isGradableResponseSingle(answers: any): number { + return this.isCompleteResponseSingle(answers); + } + + /** + * Check if two responses are the same. + * + * @param {any} question Question. + * @param {any} prevAnswers Object with the previous question answers. + * @param {any} newAnswers Object with the new question answers. + * @return {boolean} Whether they're the same. + */ + isSameResponse(question: any, prevAnswers: any, newAnswers: any): boolean { + let isSingle = true, + isMultiSame = true; + + // To know if it's single or multi answer we need to search for answers with "choice" in the name. + for (const name in newAnswers) { + if (name.indexOf('choice') != -1) { + isSingle = false; + if (!this.utils.sameAtKeyMissingIsBlank(prevAnswers, newAnswers, name)) { + isMultiSame = false; + } + } + } + + if (isSingle) { + return this.isSameResponseSingle(prevAnswers, newAnswers); + } else { + return isMultiSame ; + } + } + + /** + * Check if two responses are the same. Only for single answer. + * + * @param {any} prevAnswers Object with the previous question answers. + * @param {any} newAnswers Object with the new question answers. + * @return {boolean} Whether they're the same. + */ + isSameResponseSingle(prevAnswers: any, newAnswers: any): boolean { + return this.utils.sameAtKeyMissingIsBlank(prevAnswers, newAnswers, 'answer'); + } +} diff --git a/src/addon/qtype/qtype.module.ts b/src/addon/qtype/qtype.module.ts index ee6fd2406..c7ab3ab46 100644 --- a/src/addon/qtype/qtype.module.ts +++ b/src/addon/qtype/qtype.module.ts @@ -13,14 +13,22 @@ // limitations under the License. import { NgModule } from '@angular/core'; +import { AddonQtypeCalculatedMultiModule } from './calculatedmulti/calculatedmulti.module'; +import { AddonQtypeDescriptionModule } from './description/description.module'; +import { AddonQtypeMultichoiceModule } from './multichoice/multichoice.module'; import { AddonQtypeNumericalModule } from './numerical/numerical.module'; import { AddonQtypeShortAnswerModule } from './shortanswer/shortanswer.module'; +import { AddonQtypeTrueFalseModule } from './truefalse/truefalse.module'; @NgModule({ declarations: [], imports: [ + AddonQtypeCalculatedMultiModule, + AddonQtypeDescriptionModule, + AddonQtypeMultichoiceModule, AddonQtypeNumericalModule, - AddonQtypeShortAnswerModule + AddonQtypeShortAnswerModule, + AddonQtypeTrueFalseModule ], providers: [ ], diff --git a/src/addon/qtype/truefalse/providers/handler.ts b/src/addon/qtype/truefalse/providers/handler.ts new file mode 100644 index 000000000..59c47fdff --- /dev/null +++ b/src/addon/qtype/truefalse/providers/handler.ts @@ -0,0 +1,87 @@ + +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable, Injector } from '@angular/core'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreQuestionHandler } from '@core/question/providers/delegate'; +import { AddonQtypeMultichoiceComponent } from '@addon/qtype/multichoice/component/multichoice'; + +/** + * Handler to support true/false question type. + */ +@Injectable() +export class AddonQtypeTrueFalseHandler implements CoreQuestionHandler { + name = 'AddonQtypeTrueFalse'; + type = 'qtype_truefalse'; + + constructor(private utils: CoreUtilsProvider) { } + + /** + * Return the Component to use to display the question. + * It's recommended to return the class of the component, but you can also return an instance of the component. + * + * @param {Injector} injector Injector. + * @param {any} question The question to render. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. + */ + getComponent(injector: Injector, question: any): any | Promise { + // True/false behaves like a multichoice, use the same component. + return AddonQtypeMultichoiceComponent; + } + + /** + * Check if a response is complete. + * + * @param {any} question The question. + * @param {any} answers Object with the question answers (without prefix). + * @return {number} 1 if complete, 0 if not complete, -1 if cannot determine. + */ + isCompleteResponse(question: any, answers: any): number { + return answers['answer'] ? 1 : 0; + } + + /** + * Whether or not the handler is enabled on a site level. + * + * @return {boolean|Promise} True or promise resolved with true if enabled. + */ + isEnabled(): boolean | Promise { + return true; + } + + /** + * Check if a student has provided enough of an answer for the question to be graded automatically, + * or whether it must be considered aborted. + * + * @param {any} question The question. + * @param {any} answers Object with the question answers (without prefix). + * @return {number} 1 if gradable, 0 if not gradable, -1 if cannot determine. + */ + isGradableResponse(question: any, answers: any): number { + return this.isCompleteResponse(question, answers); + } + + /** + * Check if two responses are the same. + * + * @param {any} question Question. + * @param {any} prevAnswers Object with the previous question answers. + * @param {any} newAnswers Object with the new question answers. + * @return {boolean} Whether they're the same. + */ + isSameResponse(question: any, prevAnswers: any, newAnswers: any): boolean { + return this.utils.sameAtKeyMissingIsBlank(prevAnswers, newAnswers, 'answer'); + } +} diff --git a/src/addon/qtype/truefalse/truefalse.module.ts b/src/addon/qtype/truefalse/truefalse.module.ts new file mode 100644 index 000000000..641a26d15 --- /dev/null +++ b/src/addon/qtype/truefalse/truefalse.module.ts @@ -0,0 +1,30 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { NgModule } from '@angular/core'; +import { CoreQuestionDelegate } from '@core/question/providers/delegate'; +import { AddonQtypeTrueFalseHandler } from './providers/handler'; + +@NgModule({ + declarations: [ + ], + providers: [ + AddonQtypeTrueFalseHandler + ] +}) +export class AddonQtypeTrueFalseModule { + constructor(questionDelegate: CoreQuestionDelegate, handler: AddonQtypeTrueFalseHandler) { + questionDelegate.registerHandler(handler); + } +} diff --git a/src/core/question/classes/base-question-component.ts b/src/core/question/classes/base-question-component.ts index b13404330..859c14a67 100644 --- a/src/core/question/classes/base-question-component.ts +++ b/src/core/question/classes/base-question-component.ts @@ -95,4 +95,95 @@ export class CoreQuestionBaseComponent { return questionDiv; } + + /** + * Initialize a question component with a multiple choice (checkbox) or single choice (radio). + * + * @return {void|HTMLElement} Element containing the question HTML, void if the data is not valid. + */ + initMultichoiceComponent(): void | HTMLElement { + const questionDiv = this.initComponent(); + + if (questionDiv) { + // Create the model for radio buttons. + this.question.singleChoiceModel = {}; + + // Get the prompt. + this.question.prompt = this.domUtils.getContentsOfElement(questionDiv, '.prompt'); + + // Search radio buttons first (single choice). + let options = Array.from(questionDiv.querySelectorAll('input[type="radio"]')); + if (!options || !options.length) { + // Radio buttons not found, it should be a multi answer. Search for checkbox. + this.question.multi = true; + options = Array.from(questionDiv.querySelectorAll('input[type="checkbox"]')); + + if (!options || !options.length) { + // No checkbox found either. Abort. + this.logger.warn('Aborting because of no radio and checkbox found.', this.question.name); + + return this.questionHelper.showComponentError(this.onAbort); + } + } + + this.question.options = []; + + for (const i in options) { + const element = options[i], + option: any = { + id: element.id, + name: element.name, + value: element.value, + checked: element.checked, + disabled: element.disabled + }, + parent = element.parentElement; + + this.question.optionsName = option.name; + + // Get the label with the question text. + const label = questionDiv.querySelector('label[for="' + option.id + '"]'); + if (label) { + option.text = label.innerHTML; + + // Check that we were able to successfully extract options required data. + if (typeof option.name != 'undefined' && typeof option.value != 'undefined' && + typeof option.text != 'undefined') { + + if (element.checked) { + // If the option is checked and it's a single choice we use the model to select the one. + if (!this.question.multi) { + this.question.singleChoiceModel = option.value; + } + + if (parent) { + // Check if answer is correct. + if (parent && parent.className.indexOf('incorrect') >= 0) { + option.isCorrect = 0; + } else if (parent && parent.className.indexOf('correct') >= 0) { + option.isCorrect = 1; + } + + // Search the feedback. + const feedback = parent.querySelector('.specificfeedback'); + if (feedback) { + option.feedback = feedback.innerHTML; + } + } + } + + this.question.options.push(option); + continue; + } + } + + // Something went wrong when extracting the questions data. Abort. + this.logger.warn('Aborting because of an error parsing options.', this.question.name, option.name); + + return this.questionHelper.showComponentError(this.onAbort); + } + } + + return questionDiv; + } }