MOBILE-2427 qtype: Support units in numerical questions
parent
21899c74f7
commit
e27e294d5b
|
@ -17,7 +17,6 @@ import { Injectable, Injector } from '@angular/core';
|
|||
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||
import { CoreUtilsProvider } from '@providers/utils/utils';
|
||||
import { CoreQuestionHandler } from '@core/question/providers/delegate';
|
||||
import { AddonQtypeNumericalHandler } from '@addon/qtype/numerical/providers/handler';
|
||||
import { AddonQtypeCalculatedComponent } from '../component/calculated';
|
||||
|
||||
/**
|
||||
|
@ -28,8 +27,7 @@ export class AddonQtypeCalculatedHandler implements CoreQuestionHandler {
|
|||
name = 'AddonQtypeCalculated';
|
||||
type = 'qtype_calculated';
|
||||
|
||||
constructor(private utils: CoreUtilsProvider, private numericalHandler: AddonQtypeNumericalHandler,
|
||||
private domUtils: CoreDomUtilsProvider) { }
|
||||
constructor(private utils: CoreUtilsProvider, private domUtils: CoreDomUtilsProvider) { }
|
||||
|
||||
/**
|
||||
* Return the Component to use to display the question.
|
||||
|
@ -51,8 +49,7 @@ export class AddonQtypeCalculatedHandler implements CoreQuestionHandler {
|
|||
* @return {number} 1 if complete, 0 if not complete, -1 if cannot determine.
|
||||
*/
|
||||
isCompleteResponse(question: any, answers: any): number {
|
||||
// This question type depends on numerical.
|
||||
if (this.isGradableResponse(question, answers) === 0 || !this.numericalHandler.validateUnits(answers['answer'])) {
|
||||
if (this.isGradableResponse(question, answers) === 0 || !this.validateUnits(answers['answer'])) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
@ -81,7 +78,6 @@ export class AddonQtypeCalculatedHandler implements CoreQuestionHandler {
|
|||
* @return {number} 1 if gradable, 0 if not gradable, -1 if cannot determine.
|
||||
*/
|
||||
isGradableResponse(question: any, answers: any): number {
|
||||
// This question type depends on numerical.
|
||||
let isGradable = this.isValidValue(answers['answer']);
|
||||
if (isGradable && this.requiresUnits(question)) {
|
||||
// The question requires a unit.
|
||||
|
@ -100,7 +96,6 @@ export class AddonQtypeCalculatedHandler implements CoreQuestionHandler {
|
|||
* @return {boolean} Whether they're the same.
|
||||
*/
|
||||
isSameResponse(question: any, prevAnswers: any, newAnswers: any): boolean {
|
||||
// This question type depends on numerical.
|
||||
return this.utils.sameAtKeyMissingIsBlank(prevAnswers, newAnswers, 'answer') &&
|
||||
this.utils.sameAtKeyMissingIsBlank(prevAnswers, newAnswers, 'unit');
|
||||
}
|
||||
|
@ -126,4 +121,38 @@ export class AddonQtypeCalculatedHandler implements CoreQuestionHandler {
|
|||
|
||||
return !!(element.querySelector('select[name*=unit]') || element.querySelector('input[type="radio"]'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a number with units. We don't have the list of valid units and conversions, so we can't perform
|
||||
* a full validation. If this function returns true it means we can't be sure it's valid.
|
||||
*
|
||||
* @param {string} answer Answer.
|
||||
* @return {boolean} False if answer isn't valid, true if we aren't sure if it's valid.
|
||||
*/
|
||||
validateUnits(answer: string): boolean {
|
||||
if (!answer) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const regexString = '[+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:e[-+]?\\d+)?';
|
||||
|
||||
// Strip spaces (which may be thousands separators) and change other forms of writing e to e.
|
||||
answer = answer.replace(' ', '');
|
||||
answer = answer.replace(/(?:e|E|(?:x|\*|×)10(?:\^|\*\*))([+-]?\d+)/, 'e$1');
|
||||
|
||||
// If a '.' is present or there are multiple ',' (i.e. 2,456,789) assume ',' is a thousands separator and stip it.
|
||||
// Else assume it is a decimal separator, and change it to '.'.
|
||||
if (answer.indexOf('.') != -1 || answer.split(',').length - 1 > 1) {
|
||||
answer = answer.replace(',', '');
|
||||
} else {
|
||||
answer = answer.replace(',', '.');
|
||||
}
|
||||
|
||||
// We don't know if units should be before or after so we check both.
|
||||
if (answer.match(new RegExp('^' + regexString)) === null || answer.match(new RegExp(regexString + '$')) === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,113 +13,15 @@
|
|||
// 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 { AddonQtypeShortAnswerComponent } from '@addon/qtype/shortanswer/component/shortanswer';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { AddonQtypeCalculatedHandler } from '@addon/qtype/calculated/providers/handler';
|
||||
|
||||
/**
|
||||
* Handler to support numerical question type.
|
||||
* This question type depends on calculated question type.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonQtypeNumericalHandler implements CoreQuestionHandler {
|
||||
export class AddonQtypeNumericalHandler extends AddonQtypeCalculatedHandler {
|
||||
name = 'AddonQtypeNumerical';
|
||||
type = 'qtype_numerical';
|
||||
|
||||
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<any>} The component (or promise resolved with component) to use, undefined if not found.
|
||||
*/
|
||||
getComponent(injector: Injector, question: any): any | Promise<any> {
|
||||
// Numerical behaves like a short answer, use the same component.
|
||||
return AddonQtypeShortAnswerComponent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
if (this.isGradableResponse(question, answers) === 0 || !this.validateUnits(answers['answer'])) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the handler is enabled on a site level.
|
||||
*
|
||||
* @return {boolean|Promise<boolean>} True or promise resolved with true if enabled.
|
||||
*/
|
||||
isEnabled(): boolean | Promise<boolean> {
|
||||
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 (answers['answer'] || answers['answer'] === '0' || answers['answer'] === 0) ? 1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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');
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a number with units. We don't have the list of valid units and conversions, so we can't perform
|
||||
* a full validation. If this function returns true it means we can't be sure it's valid.
|
||||
*
|
||||
* @param {string} answer Answer.
|
||||
* @return {boolean} False if answer isn't valid, true if we aren't sure if it's valid.
|
||||
*/
|
||||
validateUnits(answer: string): boolean {
|
||||
if (!answer) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const regexString = '[+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:e[-+]?\\d+)?';
|
||||
|
||||
// Strip spaces (which may be thousands separators) and change other forms of writing e to e.
|
||||
answer = answer.replace(' ', '');
|
||||
answer = answer.replace(/(?:e|E|(?:x|\*|×)10(?:\^|\*\*))([+-]?\d+)/, 'e$1');
|
||||
|
||||
// If a '.' is present or there are multiple ',' (i.e. 2,456,789) assume ',' is a thousands separator and stip it.
|
||||
// Else assume it is a decimal separator, and change it to '.'.
|
||||
if (answer.indexOf('.') != -1 || answer.split(',').length - 1 > 1) {
|
||||
answer = answer.replace(',', '');
|
||||
} else {
|
||||
answer = answer.replace(',', '.');
|
||||
}
|
||||
|
||||
// We don't know if units should be before or after so we check both.
|
||||
if (answer.match(new RegExp('^' + regexString)) === null || answer.match(new RegExp(regexString + '$')) === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue