diff --git a/src/addons/mod/lesson/services/lesson-helper.ts b/src/addons/mod/lesson/services/lesson-helper.ts index 9bdb48969..6e5c00106 100644 --- a/src/addons/mod/lesson/services/lesson-helper.ts +++ b/src/addons/mod/lesson/services/lesson-helper.ts @@ -27,6 +27,7 @@ import { AddonModLessonProvider, } from './lesson'; import { CoreTime } from '@singletons/time'; +import { CoreUtils } from '@services/utils/utils'; /** * Helper service that provides some features for quiz. @@ -200,17 +201,14 @@ export class AddonModLessonHelperProvider { return question; } - let type = 'text'; - switch (pageData.page?.qtype) { case AddonModLessonProvider.LESSON_PAGE_TRUEFALSE: case AddonModLessonProvider.LESSON_PAGE_MULTICHOICE: return this.getMultiChoiceQuestionData(questionForm, question, fieldContainer); case AddonModLessonProvider.LESSON_PAGE_NUMERICAL: - type = 'number'; case AddonModLessonProvider.LESSON_PAGE_SHORTANSWER: - return this.getInputQuestionData(questionForm, question, fieldContainer, type); + return this.getInputQuestionData(questionForm, question, fieldContainer, pageData.page.qtype); case AddonModLessonProvider.LESSON_PAGE_ESSAY: { return this.getEssayQuestionData(questionForm, question, fieldContainer); @@ -301,21 +299,21 @@ export class AddonModLessonHelperProvider { * @param questionForm The form group where to add the controls. * @param question Basic question data. * @param fieldContainer HTMLElement containing the data. - * @param type Type of the input. + * @param questionType Type of the question. * @returns Question data. */ protected getInputQuestionData( questionForm: FormGroup, question: AddonModLessonQuestion, fieldContainer: HTMLElement, - type: string, + questionType: number, ): AddonModLessonInputQuestion { const inputQuestion = question; inputQuestion.template = 'shortanswer'; // Get the input. - const input = fieldContainer.querySelector('input[type="text"], input[type="number"]'); + const input = fieldContainer.querySelector('input[type="text"], input[type="number"]'); if (!input) { return inputQuestion; } @@ -324,11 +322,14 @@ export class AddonModLessonHelperProvider { id: input.id, name: input.name, maxlength: input.maxLength, - type, + type: 'text', // Use text for numerical questions too to allow different decimal separators. }; // Init the control. - questionForm.addControl(input.name, this.formBuilder.control({ value: input.value, disabled: input.readOnly })); + questionForm.addControl(input.name, this.formBuilder.control({ + value: questionType === AddonModLessonProvider.LESSON_PAGE_NUMERICAL ? CoreUtils.formatFloat(input.value) : input.value, + disabled: input.readOnly, + })); return inputQuestion; } diff --git a/src/addons/mod/lesson/services/lesson.ts b/src/addons/mod/lesson/services/lesson.ts index c32b66b77..27e780f13 100644 --- a/src/addons/mod/lesson/services/lesson.ts +++ b/src/addons/mod/lesson/services/lesson.ts @@ -682,19 +682,20 @@ export class AddonModLessonProvider { pageIndex: Record, result: AddonModLessonCheckAnswerResult, ): void { - - const parsedAnswer = parseFloat( data.answer); + // In LMS, this unformat float is done by the 'float' form field. + const parsedAnswer = CoreUtils.unformatFloat( data.answer, true); // Set defaults. result.response = ''; result.newpageid = 0; - if (!data.answer || isNaN(parsedAnswer)) { + if (!data.answer || parsedAnswer === false || parsedAnswer === '') { result.noanswer = true; return; } + data.answer = String(parsedAnswer); // Store the parsed answer in the supplied data so it uses the standard separator. result.useranswer = parsedAnswer; result.studentanswer = result.userresponse = String(result.useranswer); @@ -3321,7 +3322,7 @@ export class AddonModLessonProvider { // Only 1 answer, add it to the table. result.feedback = this.addAnswerAndResponseToFeedback( result.feedback, - result.studentanswer, + CoreUtils.formatFloat(result.studentanswer), result.studentanswerformat || 1, result.response, className, diff --git a/src/addons/mod/lesson/tests/behat/numerical_decimal_separator.feature b/src/addons/mod/lesson/tests/behat/numerical_decimal_separator.feature new file mode 100755 index 000000000..1484b9960 --- /dev/null +++ b/src/addons/mod/lesson/tests/behat/numerical_decimal_separator.feature @@ -0,0 +1,191 @@ +@mod @mod_lesson @app @javascript +Feature: Test decimal separators in lesson + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | teacher | teacher1@example.com | + | student1 | Student | student | student1@example.com | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + And the following "activities" exist: + | activity | name | intro | course | idnumber | modattempts | review | maxattempts | retake | allowofflineattempts | + | lesson | Basic lesson | Basic lesson descr | C1 | lesson | 1 | 1 | 0 | 1 | 0 | + | lesson | Offline lesson | Offline lesson descr | C1 | lesson | 1 | 1 | 0 | 1 | 1 | + # Currently there are no generators for pages. See MDL-77581. + And I log in as "teacher1" + And I am on "Course 1" course homepage + And I follow "Basic lesson" + And I follow "Add a question page" + And I set the field "Select a question type" to "Numerical" + And I press "Add a question page" + And I set the following fields to these values: + | Page title | Hardest question ever | + | Page contents | 1 + 1.87? | + | id_answer_editor_0 | 2.87 | + | id_response_editor_0 | Correct answer | + | id_jumpto_0 | End of lesson | + | id_score_0 | 1 | + | id_answer_editor_1 | 2.1:2.8 | + | id_response_editor_1 | Incorrect answer | + | id_jumpto_1 | This page | + | id_score_1 | 0 | + And I press "Save page" + And I am on "Course 1" course homepage + And I follow "Offline lesson" + And I follow "Add a question page" + And I set the field "Select a question type" to "Numerical" + And I press "Add a question page" + And I set the following fields to these values: + | Page title | Hardest question ever | + | Page contents | 1 + 1.87? | + | id_answer_editor_0 | 2.87 | + | id_response_editor_0 | Correct answer | + | id_jumpto_0 | End of lesson | + | id_score_0 | 1 | + | id_answer_editor_1 | 2.1:2.8 | + | id_response_editor_1 | Incorrect answer | + | id_jumpto_1 | This page | + | id_score_1 | 0 | + And I press "Save page" + And I log out + + Scenario: Attempt an online lesson successfully as a student (standard separator) + Given I entered the course "Course 1" as "student1" in the app + And I press "Basic lesson" in the app + When I press "Start" in the app + Then I should find "1 + 1.87?" in the app + + When I set the field "Your answer" to "2,87" in the app + And I press "Submit" in the app + Then I should find "One or more questions have no answer given" in the app + + When I press "Continue" in the app + And I set the field "Your answer" to "2.87" in the app + And I press "Submit" in the app + Then I should find "Correct answer" in the app + And I should find "2.87" in the app + And I should not find "Incorrect answer" in the app + + When I press "Continue" in the app + Then I should find "Congratulations - end of lesson reached" in the app + And I should find "Your score is 1 (out of 1)." in the app + + Scenario: Attempt an online lesson successfully as a student (custom separator) and review as teacher + Given the following "language customisations" exist: + | component | stringid | value | + | core_langconfig | decsep | , | + And the following config values are set as admin: + | customlangstrings | "core.decsep|,|en" | tool_mobile | + And I entered the course "Course 1" as "student1" in the app + And I press "Basic lesson" in the app + When I press "Start" in the app + Then I should find "1 + 1.87?" in the app + + When I set the field "Your answer" to "2,87" in the app + And I press "Submit" in the app + Then I should find "Correct answer" in the app + And I should find "2,87" in the app + And I should not find "Incorrect answer" in the app + + When I press "Continue" in the app + Then I should find "Congratulations - end of lesson reached" in the app + And I should find "Your score is 1 (out of 1)." in the app + + When I press "Review lesson" in the app + Then the field "Your answer" matches value "2,87" in the app + + When I press the back button in the app + And I press "Start" in the app + And I set the following fields to these values in the app: + | Your answer | 2.87 | + And I press "Submit" in the app + Then I should find "Correct answer" in the app + And I should find "2,87" in the app + And I should not find "Incorrect answer" in the app + + When I press "Continue" in the app + Then I should find "Congratulations - end of lesson reached" in the app + And I should find "Your score is 1 (out of 1)." in the app + + When I press "Review lesson" in the app + Then the field "Your answer" matches value "2,87" in the app + + Scenario: Attempt an offline lesson successfully as a student (standard separator) + Given I entered the course "Course 1" as "student1" in the app + When I press "Course downloads" in the app + And I press "Download" within "Offline lesson" "ion-item" in the app + Then I should find "Downloaded" within "Offline lesson" "ion-item" in the app + + When I press the back button in the app + And I press "Offline lesson" in the app + And I switch network connection to offline + And I press "Start" in the app + Then I should find "1 + 1.87?" in the app + + When I set the field "Your answer" to "2,87" in the app + And I press "Submit" in the app + Then I should find "One or more questions have no answer given" in the app + + When I press "Continue" in the app + And I set the field "Your answer" to "2.87" in the app + And I press "Submit" in the app + Then I should find "Correct answer" in the app + And I should find "2.87" in the app + And I should not find "Incorrect answer" in the app + + When I press "Continue" in the app + Then I should find "Congratulations - end of lesson reached" in the app + And I should find "Your score is 1 (out of 1)." in the app + + When I switch network connection to wifi + And I press the back button in the app + Then I should find "An offline attempt was synchronised" in the app + + Scenario: Attempt an offline lesson successfully as a student (custom separator) + Given the following "language customisations" exist: + | component | stringid | value | + | core_langconfig | decsep | , | + And the following config values are set as admin: + | customlangstrings | core.decsep\|,\|en | tool_mobile | + And I entered the course "Course 1" as "student1" in the app + When I press "Course downloads" in the app + And I press "Download" within "Offline lesson" "ion-item" in the app + Then I should find "Downloaded" within "Offline lesson" "ion-item" in the app + + When I press the back button in the app + And I press "Offline lesson" in the app + And I switch network connection to offline + And I press "Start" in the app + Then I should find "1 + 1.87?" in the app + + When I set the field "Your answer" to "2,87" in the app + And I press "Submit" in the app + Then I should find "Correct answer" in the app + And I should find "2,87" in the app + And I should not find "Incorrect answer" in the app + + When I press "Continue" in the app + Then I should find "Congratulations - end of lesson reached" in the app + And I should find "Your score is 1 (out of 1)." in the app + + When I switch network connection to wifi + And I press the back button in the app + Then I should find "An offline attempt was synchronised" in the app + + When I press "Start" in the app + And I set the following fields to these values in the app: + | Your answer | 2.87 | + And I press "Submit" in the app + Then I should find "Correct answer" in the app + And I should find "2,87" in the app + And I should not find "Incorrect answer" in the app + + When I press "Continue" in the app + Then I should find "Congratulations - end of lesson reached" in the app + And I should find "Your score is 1 (out of 1)." in the app diff --git a/src/core/services/utils/utils.ts b/src/core/services/utils/utils.ts index 555b6eb14..395afc37e 100644 --- a/src/core/services/utils/utils.ts +++ b/src/core/services/utils/utils.ts @@ -560,8 +560,7 @@ export class CoreUtilsProvider { const localeSeparator = Translate.instant('core.decsep'); - // Convert float to string. - const floatString = float + ''; + const floatString = String(float); return floatString.replace('.', localeSeparator); } @@ -1538,7 +1537,7 @@ export class CoreUtilsProvider { } /** - * Converts locale specific floating point/comma number back to standard PHP float value. + * Converts locale specific floating point/comma number back to a standard float number. * Do NOT try to do any math operations before this conversion on any user submitted floats! * Based on Moodle's unformat_float function. * @@ -1546,8 +1545,7 @@ export class CoreUtilsProvider { * @param strict If true, then check the input and return false if it is not a valid number. * @returns False if bad format, empty string if empty value or the parsed float if not. */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - unformatFloat(localeFloat: any, strict?: boolean): false | '' | number { + unformatFloat(localeFloat: string | number | null | undefined, strict?: boolean): false | '' | number { // Bad format on input type number. if (localeFloat === undefined) { return false; @@ -1559,7 +1557,7 @@ export class CoreUtilsProvider { } // Convert float to string. - localeFloat += ''; + localeFloat = String(localeFloat); localeFloat = localeFloat.trim(); if (localeFloat == '') { @@ -1569,10 +1567,12 @@ export class CoreUtilsProvider { localeFloat = localeFloat.replace(' ', ''); // No spaces - those might be used as thousand separators. localeFloat = localeFloat.replace(Translate.instant('core.decsep'), '.'); - const parsedFloat = parseFloat(localeFloat); + // Use Number instead of parseFloat because the latter truncates the number when it finds ",", while Number returns NaN. + // If the number still has "," then it means it's not a valid separator. + const parsedFloat = Number(localeFloat); // Bad format. - if (strict && (!isFinite(localeFloat) || isNaN(parsedFloat))) { + if (strict && (!isFinite(parsedFloat) || isNaN(parsedFloat))) { return false; }