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