Skip to content

Commit

Permalink
MDL-40227 mod_lesson: Use localised float formatting
Browse files Browse the repository at this point in the history
* Leverage PARAM_LOCALISEDFLOAT
* Store all numbers with standard '.' dec formatting
* Show all numbers based on locale settings
* Behat test to cover different cases using numeric questions and modified locale setting
  • Loading branch information
Peter committed Aug 6, 2020
1 parent 94fdac9 commit 407a666
Show file tree
Hide file tree
Showing 6 changed files with 482 additions and 16 deletions.
79 changes: 79 additions & 0 deletions mod/lesson/classes/local/numeric/helper.php
@@ -0,0 +1,79 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.

/**
* Lesson's numeric helper lib.
*
* Contains any helper functions for the numeric pagetyep
*
* @package mod_lesson
* @copyright 2020 Peter Dias <peter@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_lesson\local\numeric;

/**
* Lesson numeric page helper
*
* @copyright 2020 Peter Dias<peter@moodle.com>
* @package core_lesson
*/
class helper {

/**
* Helper function to unformat a given numeric value from locale specific values with n:n signifying ranges to standards
* with decimal point numbers/ranges
*
* @param string $value The value to be formatted
* @return string|float|bool $formattedvalue unformatted value
* String - If it is a range it will return a value e.g. 2:4
* Float - if it's a properly formatted float
* Null - If empty and could not be converted
*/
public static function lesson_unformat_numeric_value(string $value) {
if (strpos($value, ':')) {
list($min, $max) = explode(':', $value);
$formattedvalue = unformat_float($min) . ':' . unformat_float($max);
} else {
$formattedvalue = unformat_float($value);
}

return $formattedvalue;
}

/**
* Helper function to format a given value into locale specific values with n:n signifying ranges
*
* @param string|number $value The value to be formatted
* @return string $formattedvalue Formatted value OR $value if not numeric
*/
public static function lesson_format_numeric_value($value) : string {
$formattedvalue = $value;
if (strpos($value, ':')) {
list($min, $max) = explode(':', $value);
$formattedvalue = $min . ':' . $max;
if (is_numeric($min) && is_numeric($max)) {
$formattedvalue = format_float($min, strlen($min), true, true) . ':'
. format_float($max, strlen($max), true, true);
}
} else {
$formattedvalue = is_numeric($value) ? format_float($value, strlen($value), true, true) : $value;
}

return $formattedvalue;
}

}
2 changes: 2 additions & 0 deletions mod/lesson/lang/en/lesson.php
Expand Up @@ -399,6 +399,8 @@
$string['numberofpagesviewedheader'] = 'Number of questions answered';
$string['numberofpagesviewednotice'] = 'Number of questions answered: {$a->nquestions} (You should answer at least {$a->minquestions})';
$string['numerical'] = 'Numerical';
$string['numericanswer_help'] = 'You can specify a number, or a range of numbers by using colon. For example 2:5 means any answer between 2 and 5 including them are correct.';
$string['numericanswer'] = 'Numeric answer';
$string['offlinedatamessage'] = 'You have worked on this attempt using a mobile device. Data was last saved to this site {$a} ago. Please check that you do not have any unsaved work.';
$string['ongoing'] = 'Display ongoing score';
$string['ongoing_help'] = 'If enabled, each page will display the student\'s current points earned out of the total possible thus far.';
Expand Down
14 changes: 12 additions & 2 deletions mod/lesson/locallib.php
Expand Up @@ -1458,9 +1458,11 @@ protected final function add_score($name, $label=null, $value=null) {
* @param string $label, null means default
* @param bool $required
* @param string $format
* @param array $help Add help text via the addHelpButton. Must be an array which contains the string identifier and
* component as it's elements
* @return void
*/
protected final function add_answer($count, $label = null, $required = false, $format= '') {
protected final function add_answer($count, $label = null, $required = false, $format= '', array $help = []) {
if ($label === null) {
$label = get_string('answer', 'lesson');
}
Expand All @@ -1473,13 +1475,17 @@ protected final function add_answer($count, $label = null, $required = false, $f
$this->_form->setDefault('answer_editor['.$count.']', array('text' => '', 'format' => FORMAT_HTML));
} else {
$this->_form->addElement('text', 'answer_editor['.$count.']', $label,
array('size' => '50', 'maxlength' => '200'));
array('size' => '50', 'maxlength' => '200'));
$this->_form->setType('answer_editor['.$count.']', PARAM_TEXT);
}

if ($required) {
$this->_form->addRule('answer_editor['.$count.']', get_string('required'), 'required', null, 'client');
}

if ($help) {
$this->_form->addHelpButton("answer_editor[$count]", $help['identifier'], $help['component']);
}
}
/**
* Convenience function: Adds an response editor
Expand Down Expand Up @@ -4530,6 +4536,7 @@ public function update($properties, $context = null, $maxbytes = null) {
$this->answers[$i]->lessonid = $this->lesson->id;
$this->answers[$i]->pageid = $this->id;
$this->answers[$i]->timecreated = $this->timecreated;
$this->answers[$i]->answer = null;
}

if (isset($properties->answer_editor[$i])) {
Expand All @@ -4542,6 +4549,9 @@ public function update($properties, $context = null, $maxbytes = null) {
$this->answers[$i]->answer = $properties->answer_editor[$i];
$this->answers[$i]->answerformat = FORMAT_MOODLE;
}
} else {
// If there is no data posted which means we want to reset the stored values.
$this->answers[$i]->answer = null;
}

if (!empty($properties->response_editor[$i]) && is_array($properties->response_editor[$i])) {
Expand Down
98 changes: 84 additions & 14 deletions mod/lesson/pagetypes/numerical.php
Expand Up @@ -28,6 +28,8 @@
/** Numerical question type */
define("LESSON_PAGE_NUMERICAL", "8");

use mod_lesson\local\numeric\helper;

class lesson_page_type_numerical extends lesson_page {

protected $type = lesson_page::TYPE_QUESTION;
Expand All @@ -48,8 +50,9 @@ public function get_idstring() {
return $this->typeidstring;
}
public function display($renderer, $attempt) {
global $USER, $CFG, $PAGE;
$mform = new lesson_display_answer_form_shortanswer($CFG->wwwroot.'/mod/lesson/continue.php', array('contents'=>$this->get_contents(), 'lessonid'=>$this->lesson->id));
global $USER, $PAGE;
$mform = new lesson_display_answer_form_numerical(new moodle_url('/mod/lesson/continue.php'),
array('contents' => $this->get_contents(), 'lessonid' => $this->lesson->id));
$data = new stdClass;
$data->id = $PAGE->cm->id;
$data->pageid = $this->properties->id;
Expand Down Expand Up @@ -109,10 +112,10 @@ public function update($properties, $context = null, $maxbytes = null) {
}

public function check_answer() {
global $CFG;
$result = parent::check_answer();

$mform = new lesson_display_answer_form_shortanswer($CFG->wwwroot.'/mod/lesson/continue.php', array('contents'=>$this->get_contents()));
$mform = new lesson_display_answer_form_numerical(new moodle_url('/mod/lesson/continue.php'),
array('contents' => $this->get_contents()));
$data = $mform->get_data();
require_sesskey();

Expand All @@ -124,12 +127,11 @@ public function check_answer() {
$result->response = '';
$result->newpageid = 0;

if (!isset($data->answer) || !is_numeric($data->answer)) {
if (!isset($data->answer)) {
$result->noanswer = true;
return $result;
} else {
// Just doing default PARAM_RAW, not doing PARAM_INT because it could be a float.
$result->useranswer = (float)$data->answer;
$result->useranswer = $data->answer;
}
$result->studentanswer = $result->userresponse = $result->useranswer;
$answers = $this->get_answers();
Expand Down Expand Up @@ -201,7 +203,8 @@ public function display_answers(html_table $table) {
} else {
$cells[] = '<label class="correct">' . get_string('answer', 'lesson') . ' ' . $i . '</label>:';
}
$cells[] = format_text($answer->answer, $answer->answerformat, $options);
$formattedanswer = helper::lesson_format_numeric_value($answer->answer);
$cells[] = format_text($formattedanswer, $answer->answerformat, $options);
$table->data[] = new html_table_row($cells);

$cells = array();
Expand Down Expand Up @@ -258,7 +261,8 @@ public function report_answers($answerpage, $answerdata, $useranswer, $pagestats
unset($stats["total"]);
foreach ($stats as $valentered => $ntimes) {
$data = '<input class="form-control" type="text" size="50" ' .
'disabled="disabled" readonly="readonly" value="'.s($valentered).'" />';
'disabled="disabled" readonly="readonly" value="'.
s(format_float($valentered, strlen($valentered), true, true)).'" />';
$percent = $ntimes / $total * 100;
$percent = round($percent, 2);
$percent .= "% ".get_string("enteredthis", "lesson");
Expand All @@ -272,7 +276,8 @@ public function report_answers($answerpage, $answerdata, $useranswer, $pagestats
empty($answerdata->answers)))) {
// Get in here when the user answered or for the last answer.
$data = '<input class="form-control" type="text" size="50" ' .
'disabled="disabled" readonly="readonly" value="'.s($useranswer->useranswer).'">';
'disabled="disabled" readonly="readonly" value="'.
s(format_float($useranswer->useranswer, strlen($useranswer->useranswer), true, true)).'">';
if (isset($pagestats[$this->properties->id][$useranswer->useranswer])) {
$percent = $pagestats[$this->properties->id][$useranswer->useranswer] / $pagestats[$this->properties->id]["total"] * 100;
$percent = round($percent, 2);
Expand Down Expand Up @@ -321,6 +326,12 @@ public function report_answers($answerpage, $answerdata, $useranswer, $pagestats
*/
public function update_form_data(stdClass $data) : stdClass {
$answercount = count($this->get_answers());

// If no answers provided, then we don't need to check anything.
if (!$answercount) {
return $data;
}

// Check for other answer entry.
$lastanswer = $data->{'answer_editor[' . ($answercount - 1) . ']'};
if (strpos($lastanswer, LESSON_OTHER_ANSWERS) !== false) {
Expand Down Expand Up @@ -354,7 +365,10 @@ public function custom_definition() {
$answercount = $this->_customdata['lesson']->maxanswers;
for ($i = 0; $i < $answercount; $i++) {
$this->_form->addElement('header', 'answertitle'.$i, get_string('answer').' '.($i+1));
$this->add_answer($i, null, ($i < 1));
$this->add_answer($i, null, ($i < 1), '', [
'identifier' => 'numericanswer',
'component' => 'mod_lesson'
]);
$this->add_response($i);
$this->add_jumpto($i, null, ($i == 0 ? LESSON_NEXTPAGE : LESSON_THISPAGE));
$this->add_score($i, null, ($i===0)?1:0);
Expand All @@ -367,6 +381,64 @@ public function custom_definition() {
$this->add_jumpto($newcount, get_string('allotheranswersjump', 'lesson'), LESSON_NEXTPAGE);
$this->add_score($newcount, get_string('allotheranswersscore', 'lesson'), 0);
}

/**
* We call get data when storing the data into the db. Override to format the floats properly
*
* @return object|void
*/
public function get_data() : ?stdClass {
$data = parent::get_data();

if (!empty($data->answer_editor)) {
foreach ($data->answer_editor as $key => $answer) {
$data->answer_editor[$key] = helper::lesson_unformat_numeric_value($answer);
}
}

return $data;
}

/**
* Return submitted data if properly submitted or returns NULL if validation fails or
* if there is no submitted data with formatted numbers
*
* @return object submitted data; NULL if not valid or not submitted or cancelled
*/
public function get_submitted_data() : ?stdClass {
$data = parent::get_submitted_data();

if (!empty($data->answer_editor)) {
foreach ($data->answer_editor as $key => $answer) {
$data->answer_editor[$key] = helper::lesson_unformat_numeric_value($answer);
}
}

return $data;
}

/**
* Load in existing data as form defaults. Usually new entry defaults are stored directly in
* form definition (new entry form); this function is used to load in data where values
* already exist and data is being edited (edit entry form) after formatting numbers
*
*
* @param stdClass|array $defaults object or array of default values
*/
public function set_data($defaults) {
if (is_object($defaults)) {
$defaults = (array) $defaults;
}

$editor = 'answer_editor';
foreach ($defaults as $key => $answer) {
if (substr($key, 0, strlen($editor)) == $editor) {
$defaults[$key] = helper::lesson_format_numeric_value($answer);
}
}

parent::set_data($defaults);
}
}

class lesson_display_answer_form_numerical extends moodleform {
Expand Down Expand Up @@ -402,14 +474,12 @@ public function definition() {
$mform->addElement('hidden', 'pageid');
$mform->setType('pageid', PARAM_INT);

$mform->addElement('text', 'answer', get_string('youranswer', 'lesson'), $attrs);
$mform->setType('answer', PARAM_FLOAT);
$mform->addElement('float', 'answer', get_string('youranswer', 'lesson'), $attrs);

if ($hasattempt) {
$this->add_action_buttons(null, get_string("nextpage", "lesson"));
} else {
$this->add_action_buttons(null, get_string("submit", "lesson"));
}
}

}

0 comments on commit 407a666

Please sign in to comment.