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 multiple-choice question type. 19 * 20 * @package qtype 21 * @subpackage calculatedmulti 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 multiple-choice questions 32 * when upgrading 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_calculatedmulti_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 protected $order; 60 61 public function question_summary() { 62 return ''; // Done later, after we know which dataset is used. 63 } 64 65 public function right_answer() { 66 if ($this->question->options->single) { 67 foreach ($this->question->options->answers as $ans) { 68 if ($ans->fraction > 0.999) { 69 return $this->to_text($this->replace_expressions_in_text($ans->answer)); 70 } 71 } 72 73 } else { 74 $rightbits = array(); 75 foreach ($this->question->options->answers as $ans) { 76 if ($ans->fraction >= 0.000001) { 77 $rightbits[] = $this->to_text($this->replace_expressions_in_text($ans->answer)); 78 } 79 } 80 return implode('; ', $rightbits); 81 } 82 } 83 84 protected function explode_answer($state) { 85 if (strpos($state->answer, '-') < 7) { 86 // Broken state, skip it. 87 throw new coding_exception("Brokes state {$state->id} for calcluatedmulti 88 question {$state->question}. (It did not specify a dataset."); 89 } 90 list($datasetbit, $answer) = explode('-', $state->answer, 2); 91 $selecteditem = substr($datasetbit, 7); 92 93 if (is_null($this->selecteditem)) { 94 $this->load_dataset($selecteditem); 95 } else if ($this->selecteditem != $selecteditem) { 96 $this->logger->log_assumption("Different states for calcluatedmulti question 97 {$state->question} used different dataset items. Ignoring the change 98 in state {$state->id} and coninuting to use item {$this->selecteditem}."); 99 } 100 101 if (strpos($answer, ':') !== false) { 102 list($order, $responses) = explode(':', $answer); 103 return $responses; 104 } else { 105 // Sometimes, a bug means that a state is missing the <order>: bit, 106 // We need to deal with that. 107 $this->logger->log_assumption("Dealing with missing order information 108 in attempt at multiple choice question {$this->question->id}"); 109 return $answer; 110 } 111 } 112 113 public function response_summary($state) { 114 $responses = $this->explode_answer($state); 115 if ($this->question->options->single) { 116 if (is_numeric($responses)) { 117 if (array_key_exists($responses, $this->question->options->answers)) { 118 return $this->to_text($this->replace_expressions_in_text( 119 $this->question->options->answers[$responses]->answer)); 120 } else { 121 $this->logger->log_assumption("Dealing with a place where the 122 student selected a choice that was later deleted for 123 multiple choice question {$this->question->id}"); 124 return '[CHOICE THAT WAS LATER DELETED]'; 125 } 126 } else { 127 return null; 128 } 129 130 } else { 131 if (!empty($responses)) { 132 $responses = explode(',', $responses); 133 $bits = array(); 134 foreach ($responses as $response) { 135 if (array_key_exists($response, $this->question->options->answers)) { 136 $bits[] = $this->to_text($this->replace_expressions_in_text( 137 $this->question->options->answers[$response]->answer)); 138 } else { 139 $this->logger->log_assumption("Dealing with a place where the 140 student selected a choice that was later deleted for 141 multiple choice question {$this->question->id}"); 142 $bits[] = '[CHOICE THAT WAS LATER DELETED]'; 143 } 144 } 145 return implode('; ', $bits); 146 } else { 147 return null; 148 } 149 } 150 } 151 152 public function was_answered($state) { 153 $responses = $this->explode_answer($state); 154 if ($this->question->options->single) { 155 return is_numeric($responses); 156 } else { 157 return !empty($responses); 158 } 159 } 160 161 public function set_first_step_data_elements($state, &$data) { 162 $this->explode_answer($state); 163 $this->updater->qa->questionsummary = $this->to_text( 164 $this->replace_expressions_in_text($this->question->questiontext)); 165 $this->updater->qa->rightanswer = $this->right_answer($this->question); 166 167 foreach ($this->values as $name => $value) { 168 $data['_var_' . $name] = $value; 169 } 170 171 list($datasetbit, $answer) = explode('-', $state->answer, 2); 172 list($order, $responses) = explode(':', $answer); 173 $data['_order'] = $order; 174 $this->order = explode(',', $order); 175 } 176 177 public function supply_missing_first_step_data(&$data) { 178 $data['_order'] = implode(',', array_keys($this->question->options->answers)); 179 } 180 181 public function set_data_elements_for_step($state, &$data) { 182 $responses = $this->explode_answer($state); 183 if ($this->question->options->single) { 184 if (is_numeric($responses)) { 185 $flippedorder = array_combine(array_values($this->order), array_keys($this->order)); 186 if (array_key_exists($responses, $flippedorder)) { 187 $data['answer'] = $flippedorder[$responses]; 188 } else { 189 $data['answer'] = '-1'; 190 } 191 } 192 193 } else { 194 $responses = explode(',', $responses); 195 foreach ($this->order as $key => $ansid) { 196 if (in_array($ansid, $responses)) { 197 $data['choice' . $key] = 1; 198 } else { 199 $data['choice' . $key] = 0; 200 } 201 } 202 } 203 } 204 205 public function load_dataset($selecteditem) { 206 $this->selecteditem = $selecteditem; 207 $this->updater->qa->variant = $selecteditem; 208 $this->values = $this->qeupdater->load_dataset( 209 $this->question->id, $selecteditem); 210 211 // Prepare an array for {@link substitute_values()}. 212 $this->search = array(); 213 $this->safevalue = array(); 214 $this->prettyvalue = array(); 215 foreach ($this->values as $name => $value) { 216 if (!is_numeric($value)) { 217 $a = new stdClass(); 218 $a->name = '{' . $name . '}'; 219 $a->value = $value; 220 throw new moodle_exception('notvalidnumber', 'qtype_calculated', '', $a); 221 } 222 223 $this->search[] = '{' . $name . '}'; 224 $this->safevalue[] = '(' . $value . ')'; 225 $this->prettyvalue[] = $this->format_float($value); 226 } 227 } 228 229 /** 230 * This function should be identical to 231 * {@link qtype_calculated_variable_substituter::format_float()}. Except that 232 * we do not try to do locale-aware replacement of the decimal point. 233 * 234 * Having to copy it here is a pain, but it is the standard rule about not 235 * using library code (which may change in future) in upgrade code, which 236 * exists at a point in time. 237 * 238 * Display a float properly formatted with a certain number of decimal places. 239 * @param number $x the number to format 240 * @param int $length restrict to this many decimal places or significant 241 * figures. If null, the number is not rounded. 242 * @param int format 1 => decimalformat, 2 => significantfigures. 243 * @return string formtted number. 244 */ 245 public function format_float($x, $length = null, $format = null) { 246 if (!is_null($length) && !is_null($format)) { 247 if ($format == 1) { 248 // Decimal places. 249 $x = sprintf('%.' . $length . 'F', $x); 250 } else if ($format == 2) { 251 // Significant figures. 252 $x = sprintf('%.' . $length . 'g', $x); 253 } 254 } 255 return $x; 256 } 257 258 /** 259 * Evaluate an expression using the variable values. 260 * @param string $expression the expression. A PHP expression with placeholders 261 * like {a} for where the variables need to go. 262 * @return float the computed result. 263 */ 264 public function calculate($expression) { 265 return $this->calculate_raw($this->substitute_values_for_eval($expression)); 266 } 267 268 /** 269 * Evaluate an expression after the variable values have been substituted. 270 * @param string $expression the expression. A PHP expression with placeholders 271 * like {a} for where the variables need to go. 272 * @return float the computed result. 273 */ 274 protected function calculate_raw($expression) { 275 try { 276 // In older PHP versions this this is a way to validate code passed to eval. 277 // The trick came from http://php.net/manual/en/function.eval.php. 278 if (@eval('return true; $result = ' . $expression . ';')) { 279 return eval('return ' . $expression . ';'); 280 } 281 } catch (Throwable $e) { 282 // PHP7 and later now throws ParseException and friends from eval(), 283 // which is much better. 284 } 285 // In either case of an invalid $expression, we end here. 286 return '[Invalid expression ' . $expression . ']'; 287 } 288 289 /** 290 * Substitute variable placehodlers like {a} with their value wrapped in (). 291 * @param string $expression the expression. A PHP expression with placeholders 292 * like {a} for where the variables need to go. 293 * @return string the expression with each placeholder replaced by the 294 * corresponding value. 295 */ 296 protected function substitute_values_for_eval($expression) { 297 return str_replace($this->search, $this->safevalue, $expression); 298 } 299 300 /** 301 * Substitute variable placehodlers like {a} with their value without wrapping 302 * the value in anything. 303 * @param string $text some content with placeholders 304 * like {a} for where the variables need to go. 305 * @return string the expression with each placeholder replaced by the 306 * corresponding value. 307 */ 308 protected function substitute_values_pretty($text) { 309 return str_replace($this->search, $this->prettyvalue, $text); 310 } 311 312 /** 313 * Replace any embedded variables (like {a}) or formulae (like {={a} + {b}}) 314 * in some text with the corresponding values. 315 * @param string $text the text to process. 316 * @return string the text with values substituted. 317 */ 318 public function replace_expressions_in_text($text, $length = null, $format = null) { 319 $vs = $this; // Can't see to use $this in a PHP closure. 320 $text = preg_replace_callback(qtype_calculated::FORMULAS_IN_TEXT_REGEX, 321 function ($matches) use ($vs, $format, $length) { 322 return $vs->format_float($vs->calculate($matches[1]), $length, $format); 323 }, $text); 324 return $this->substitute_values_pretty($text); 325 } 326 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body