See Release Notes
Long Term Support Release
Differences Between: [Versions 39 and 401] [Versions 39 and 402] [Versions 39 and 403]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 /** 18 * Upgrade library code for the calculated question type. 19 * 20 * @package qtype 21 * @subpackage calculated 22 * @copyright 2011 The Open University 23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 */ 25 26 27 defined('MOODLE_INTERNAL') || die(); 28 29 30 /** 31 * Class for converting attempt data for calculated questions when upgrading 32 * attempts to the new question engine. 33 * 34 * This class is used by the code in question/engine/upgrade/upgradelib.php. 35 * 36 * @copyright 2011 The Open University 37 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 38 */ 39 class qtype_calculated_qe2_attempt_updater extends question_qtype_attempt_updater { 40 protected $selecteditem = null; 41 /** @var array variable name => value */ 42 protected $values; 43 44 /** @var array variable names wrapped in {...}. Used by {@link substitute_values()}. */ 45 protected $search; 46 47 /** 48 * @var array variable values, with negative numbers wrapped in (...). 49 * Used by {@link substitute_values()}. 50 */ 51 protected $safevalue; 52 53 /** 54 * @var array variable values, with negative numbers wrapped in (...). 55 * Used by {@link substitute_values()}. 56 */ 57 protected $prettyvalue; 58 59 public function question_summary() { 60 return ''; // Done later, after we know which dataset is used. 61 } 62 63 public function right_answer() { 64 foreach ($this->question->options->answers as $ans) { 65 if ($ans->fraction > 0.999) { 66 $right = $this->calculate($ans->answer); 67 68 if (empty($this->question->options->units)) { 69 return $right; 70 } 71 72 $unit = reset($this->question->options->units); 73 $unit = $unit->unit; 74 if (!empty($this->question->options->unitsleft)) { 75 return $unit . ' ' . $right; 76 } else { 77 return $right . ' ' . $unit; 78 } 79 } 80 } 81 } 82 83 protected function parse_response($state) { 84 if (strpos($state->answer, '-') < 7) { 85 // Broken state, skip it. 86 throw new coding_exception("Brokes state {$state->id} for calculated 87 question {$state->question}. (It did not specify a dataset."); 88 } 89 list($datasetbit, $realanswer) = explode('-', $state->answer, 2); 90 $selecteditem = substr($datasetbit, 7); 91 92 if (is_null($this->selecteditem)) { 93 $this->load_dataset($selecteditem); 94 } else if ($this->selecteditem != $selecteditem) { 95 $this->logger->log_assumption("Different states for calculated question 96 {$state->question} used different dataset items. Ignoring the change 97 in state {$state->id} and coninuting to use item {$this->selecteditem}."); 98 } 99 100 if (!$realanswer) { 101 return array('', ''); 102 } 103 104 if (strpos($realanswer, '|||||') === false) { 105 $answer = $realanswer; 106 $unit = ''; 107 } else { 108 list($answer, $unit) = explode('|||||', $realanswer, 2); 109 } 110 111 return array($answer, $unit); 112 } 113 114 public function response_summary($state) { 115 list($answer, $unit) = $this->parse_response($state); 116 117 if (empty($answer) && empty($unit)) { 118 $resp = null; 119 } else { 120 $resp = $answer; 121 } 122 123 if (!empty($unit)) { 124 if (!empty($this->question->options->unitsleft)) { 125 $resp = trim($unit . ' ' . $resp); 126 } else { 127 $resp = trim($resp . ' ' . $unit); 128 } 129 } 130 131 return $resp; 132 } 133 134 public function was_answered($state) { 135 return !empty($state->answer); 136 } 137 138 public function set_first_step_data_elements($state, &$data) { 139 $this->parse_response($state); 140 $this->updater->qa->questionsummary = $this->to_text( 141 $this->replace_expressions_in_text($this->question->questiontext)); 142 $this->updater->qa->rightanswer = $this->right_answer($this->question); 143 144 foreach ($this->values as $name => $value) { 145 $data['_var_' . $name] = $value; 146 } 147 148 $data['_separators'] = '.$,'; 149 } 150 151 public function supply_missing_first_step_data(&$data) { 152 $data['_separators'] = '.$,'; 153 } 154 155 public function set_data_elements_for_step($state, &$data) { 156 if (empty($state->answer)) { 157 return; 158 } 159 160 list($answer, $unit) = $this->parse_response($state); 161 if (!empty($this->question->options->showunits) && 162 $this->question->options->showunits == 1) { 163 // Multichoice units. 164 $data['answer'] = $answer; 165 $data['unit'] = $unit; 166 } else if (!empty($this->question->options->unitsleft)) { 167 if (!empty($unit)) { 168 $data['answer'] = $unit . ' ' . $answer; 169 } else { 170 $data['answer'] = $answer; 171 } 172 } else { 173 if (!empty($unit)) { 174 $data['answer'] = $answer . ' ' . $unit; 175 } else { 176 $data['answer'] = $answer; 177 } 178 } 179 } 180 181 public function load_dataset($selecteditem) { 182 $this->selecteditem = $selecteditem; 183 $this->updater->qa->variant = $selecteditem; 184 $this->values = $this->qeupdater->load_dataset( 185 $this->question->id, $selecteditem); 186 187 // Prepare an array for {@link substitute_values()}. 188 $this->search = array(); 189 $this->safevalue = array(); 190 $this->prettyvalue = array(); 191 foreach ($this->values as $name => $value) { 192 if (!is_numeric($value)) { 193 $a = new stdClass(); 194 $a->name = '{' . $name . '}'; 195 $a->value = $value; 196 throw new moodle_exception('notvalidnumber', 'qtype_calculated', '', $a); 197 } 198 199 $this->search[] = '{' . $name . '}'; 200 $this->safevalue[] = '(' . $value . ')'; 201 $this->prettyvalue[] = $this->format_float($value); 202 } 203 } 204 205 /** 206 * This function should be identical to 207 * {@link qtype_calculated_variable_substituter::format_float()}. Except that 208 * we do not try to do locale-aware replacement of the decimal point. 209 * 210 * Having to copy it here is a pain, but it is the standard rule about not 211 * using library code (which may change in future) in upgrade code, which 212 * exists at a point in time. 213 * 214 * Display a float properly formatted with a certain number of decimal places. 215 * @param number $x the number to format 216 * @param int $length restrict to this many decimal places or significant 217 * figures. If null, the number is not rounded. 218 * @param int format 1 => decimalformat, 2 => significantfigures. 219 * @return string formtted number. 220 */ 221 public function format_float($x, $length = null, $format = null) { 222 if (!is_null($length) && !is_null($format)) { 223 if ($format == 1) { 224 // Decimal places. 225 $x = sprintf('%.' . $length . 'F', $x); 226 } else if ($format == 2) { 227 // Significant figures. 228 $x = sprintf('%.' . $length . 'g', $x); 229 } 230 } 231 return $x; 232 } 233 234 /** 235 * Evaluate an expression using the variable values. 236 * @param string $expression the expression. A PHP expression with placeholders 237 * like {a} for where the variables need to go. 238 * @return float the computed result. 239 */ 240 public function calculate($expression) { 241 return $this->calculate_raw($this->substitute_values_for_eval($expression)); 242 } 243 244 /** 245 * Evaluate an expression after the variable values have been substituted. 246 * @param string $expression the expression. A PHP expression with placeholders 247 * like {a} for where the variables need to go. 248 * @return float the computed result. 249 */ 250 protected function calculate_raw($expression) { 251 try { 252 // In older PHP versions this this is a way to validate code passed to eval. 253 // The trick came from http://php.net/manual/en/function.eval.php. 254 if (@eval('return true; $result = ' . $expression . ';')) { 255 return eval('return ' . $expression . ';'); 256 } 257 } catch (Throwable $e) { 258 // PHP7 and later now throws ParseException and friends from eval(), 259 // which is much better. 260 } 261 // In either case of an invalid $expression, we end here. 262 return '[Invalid expression ' . $expression . ']'; 263 } 264 265 /** 266 * Substitute variable placehodlers like {a} with their value wrapped in (). 267 * @param string $expression the expression. A PHP expression with placeholders 268 * like {a} for where the variables need to go. 269 * @return string the expression with each placeholder replaced by the 270 * corresponding value. 271 */ 272 protected function substitute_values_for_eval($expression) { 273 return str_replace($this->search, $this->safevalue, $expression); 274 } 275 276 /** 277 * Substitute variable placehodlers like {a} with their value without wrapping 278 * the value in anything. 279 * @param string $text some content with placeholders 280 * like {a} for where the variables need to go. 281 * @return string the expression with each placeholder replaced by the 282 * corresponding value. 283 */ 284 protected function substitute_values_pretty($text) { 285 return str_replace($this->search, $this->prettyvalue, $text); 286 } 287 288 /** 289 * Replace any embedded variables (like {a}) or formulae (like {={a} + {b}}) 290 * in some text with the corresponding values. 291 * @param string $text the text to process. 292 * @return string the text with values substituted. 293 */ 294 public function replace_expressions_in_text($text, $length = null, $format = null) { 295 $vs = $this; // Can't see to use $this in a PHP closure. 296 $text = preg_replace_callback('~\{=([^{}]*(?:\{[^{}]+}[^{}]*)*)}~', 297 function ($matches) use ($vs, $format, $length) { 298 return $vs->format_float($vs->calculate($matches[1]), $length, $format); 299 }, $text); 300 return $this->substitute_values_pretty($text); 301 } 302 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body