MOBILE-3535 lesson: Handle localised decimal separator
parent
2bedc67695
commit
fae7467c0c
|
@ -27,6 +27,7 @@ import {
|
||||||
AddonModLessonProvider,
|
AddonModLessonProvider,
|
||||||
} from './lesson';
|
} from './lesson';
|
||||||
import { CoreTime } from '@singletons/time';
|
import { CoreTime } from '@singletons/time';
|
||||||
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper service that provides some features for quiz.
|
* Helper service that provides some features for quiz.
|
||||||
|
@ -200,17 +201,14 @@ export class AddonModLessonHelperProvider {
|
||||||
return question;
|
return question;
|
||||||
}
|
}
|
||||||
|
|
||||||
let type = 'text';
|
|
||||||
|
|
||||||
switch (pageData.page?.qtype) {
|
switch (pageData.page?.qtype) {
|
||||||
case AddonModLessonProvider.LESSON_PAGE_TRUEFALSE:
|
case AddonModLessonProvider.LESSON_PAGE_TRUEFALSE:
|
||||||
case AddonModLessonProvider.LESSON_PAGE_MULTICHOICE:
|
case AddonModLessonProvider.LESSON_PAGE_MULTICHOICE:
|
||||||
return this.getMultiChoiceQuestionData(questionForm, question, fieldContainer);
|
return this.getMultiChoiceQuestionData(questionForm, question, fieldContainer);
|
||||||
|
|
||||||
case AddonModLessonProvider.LESSON_PAGE_NUMERICAL:
|
case AddonModLessonProvider.LESSON_PAGE_NUMERICAL:
|
||||||
type = 'number';
|
|
||||||
case AddonModLessonProvider.LESSON_PAGE_SHORTANSWER:
|
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: {
|
case AddonModLessonProvider.LESSON_PAGE_ESSAY: {
|
||||||
return this.getEssayQuestionData(questionForm, question, fieldContainer);
|
return this.getEssayQuestionData(questionForm, question, fieldContainer);
|
||||||
|
@ -301,21 +299,21 @@ export class AddonModLessonHelperProvider {
|
||||||
* @param questionForm The form group where to add the controls.
|
* @param questionForm The form group where to add the controls.
|
||||||
* @param question Basic question data.
|
* @param question Basic question data.
|
||||||
* @param fieldContainer HTMLElement containing the data.
|
* @param fieldContainer HTMLElement containing the data.
|
||||||
* @param type Type of the input.
|
* @param questionType Type of the question.
|
||||||
* @returns Question data.
|
* @returns Question data.
|
||||||
*/
|
*/
|
||||||
protected getInputQuestionData(
|
protected getInputQuestionData(
|
||||||
questionForm: FormGroup,
|
questionForm: FormGroup,
|
||||||
question: AddonModLessonQuestion,
|
question: AddonModLessonQuestion,
|
||||||
fieldContainer: HTMLElement,
|
fieldContainer: HTMLElement,
|
||||||
type: string,
|
questionType: number,
|
||||||
): AddonModLessonInputQuestion {
|
): AddonModLessonInputQuestion {
|
||||||
|
|
||||||
const inputQuestion = <AddonModLessonInputQuestion> question;
|
const inputQuestion = <AddonModLessonInputQuestion> question;
|
||||||
inputQuestion.template = 'shortanswer';
|
inputQuestion.template = 'shortanswer';
|
||||||
|
|
||||||
// Get the input.
|
// Get the input.
|
||||||
const input = <HTMLInputElement> fieldContainer.querySelector('input[type="text"], input[type="number"]');
|
const input = fieldContainer.querySelector<HTMLInputElement>('input[type="text"], input[type="number"]');
|
||||||
if (!input) {
|
if (!input) {
|
||||||
return inputQuestion;
|
return inputQuestion;
|
||||||
}
|
}
|
||||||
|
@ -324,11 +322,14 @@ export class AddonModLessonHelperProvider {
|
||||||
id: input.id,
|
id: input.id,
|
||||||
name: input.name,
|
name: input.name,
|
||||||
maxlength: input.maxLength,
|
maxlength: input.maxLength,
|
||||||
type,
|
type: 'text', // Use text for numerical questions too to allow different decimal separators.
|
||||||
};
|
};
|
||||||
|
|
||||||
// Init the control.
|
// 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;
|
return inputQuestion;
|
||||||
}
|
}
|
||||||
|
|
|
@ -682,19 +682,20 @@ export class AddonModLessonProvider {
|
||||||
pageIndex: Record<number, AddonModLessonPageWSData>,
|
pageIndex: Record<number, AddonModLessonPageWSData>,
|
||||||
result: AddonModLessonCheckAnswerResult,
|
result: AddonModLessonCheckAnswerResult,
|
||||||
): void {
|
): void {
|
||||||
|
// In LMS, this unformat float is done by the 'float' form field.
|
||||||
const parsedAnswer = parseFloat(<string> data.answer);
|
const parsedAnswer = CoreUtils.unformatFloat(<string> data.answer, true);
|
||||||
|
|
||||||
// Set defaults.
|
// Set defaults.
|
||||||
result.response = '';
|
result.response = '';
|
||||||
result.newpageid = 0;
|
result.newpageid = 0;
|
||||||
|
|
||||||
if (!data.answer || isNaN(parsedAnswer)) {
|
if (!data.answer || parsedAnswer === false || parsedAnswer === '') {
|
||||||
result.noanswer = true;
|
result.noanswer = true;
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data.answer = String(parsedAnswer); // Store the parsed answer in the supplied data so it uses the standard separator.
|
||||||
result.useranswer = parsedAnswer;
|
result.useranswer = parsedAnswer;
|
||||||
result.studentanswer = result.userresponse = String(result.useranswer);
|
result.studentanswer = result.userresponse = String(result.useranswer);
|
||||||
|
|
||||||
|
@ -3321,7 +3322,7 @@ export class AddonModLessonProvider {
|
||||||
// Only 1 answer, add it to the table.
|
// Only 1 answer, add it to the table.
|
||||||
result.feedback = this.addAnswerAndResponseToFeedback(
|
result.feedback = this.addAnswerAndResponseToFeedback(
|
||||||
result.feedback,
|
result.feedback,
|
||||||
result.studentanswer,
|
CoreUtils.formatFloat(result.studentanswer),
|
||||||
result.studentanswerformat || 1,
|
result.studentanswerformat || 1,
|
||||||
result.response,
|
result.response,
|
||||||
className,
|
className,
|
||||||
|
|
|
@ -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
|
|
@ -560,8 +560,7 @@ export class CoreUtilsProvider {
|
||||||
|
|
||||||
const localeSeparator = Translate.instant('core.decsep');
|
const localeSeparator = Translate.instant('core.decsep');
|
||||||
|
|
||||||
// Convert float to string.
|
const floatString = String(float);
|
||||||
const floatString = float + '';
|
|
||||||
|
|
||||||
return floatString.replace('.', localeSeparator);
|
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!
|
* Do NOT try to do any math operations before this conversion on any user submitted floats!
|
||||||
* Based on Moodle's unformat_float function.
|
* 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.
|
* @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.
|
* @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: string | number | null | undefined, strict?: boolean): false | '' | number {
|
||||||
unformatFloat(localeFloat: any, strict?: boolean): false | '' | number {
|
|
||||||
// Bad format on input type number.
|
// Bad format on input type number.
|
||||||
if (localeFloat === undefined) {
|
if (localeFloat === undefined) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -1559,7 +1557,7 @@ export class CoreUtilsProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert float to string.
|
// Convert float to string.
|
||||||
localeFloat += '';
|
localeFloat = String(localeFloat);
|
||||||
localeFloat = localeFloat.trim();
|
localeFloat = localeFloat.trim();
|
||||||
|
|
||||||
if (localeFloat == '') {
|
if (localeFloat == '') {
|
||||||
|
@ -1569,10 +1567,12 @@ export class CoreUtilsProvider {
|
||||||
localeFloat = localeFloat.replace(' ', ''); // No spaces - those might be used as thousand separators.
|
localeFloat = localeFloat.replace(' ', ''); // No spaces - those might be used as thousand separators.
|
||||||
localeFloat = localeFloat.replace(Translate.instant('core.decsep'), '.');
|
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.
|
// Bad format.
|
||||||
if (strict && (!isFinite(localeFloat) || isNaN(parsedFloat))) {
|
if (strict && (!isFinite(parsedFloat) || isNaN(parsedFloat))) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue