| 1 | efrain | 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 |  * Question type class for the calculated question type.
 | 
        
           |  |  | 19 |  *
 | 
        
           |  |  | 20 |  * @package    qtype
 | 
        
           |  |  | 21 |  * @subpackage calculated
 | 
        
           |  |  | 22 |  * @copyright  1999 onwards Martin Dougiamas {@link http://moodle.com}
 | 
        
           |  |  | 23 |  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 | 
        
           |  |  | 24 |  */
 | 
        
           |  |  | 25 |   | 
        
           |  |  | 26 |   | 
        
           |  |  | 27 | defined('MOODLE_INTERNAL') || die();
 | 
        
           |  |  | 28 |   | 
        
           |  |  | 29 | require_once($CFG->dirroot . '/question/type/questiontypebase.php');
 | 
        
           |  |  | 30 | require_once($CFG->dirroot . '/question/type/questionbase.php');
 | 
        
           |  |  | 31 | require_once($CFG->dirroot . '/question/type/numerical/question.php');
 | 
        
           |  |  | 32 |   | 
        
           |  |  | 33 |   | 
        
           |  |  | 34 | /**
 | 
        
           |  |  | 35 |  * The calculated question type.
 | 
        
           |  |  | 36 |  *
 | 
        
           |  |  | 37 |  * @copyright  1999 onwards Martin Dougiamas {@link http://moodle.com}
 | 
        
           |  |  | 38 |  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 | 
        
           |  |  | 39 |  */
 | 
        
           |  |  | 40 | class qtype_calculated extends question_type {
 | 
        
           |  |  | 41 |     /**
 | 
        
           | 11 | efrain | 42 |      * @var string a placeholder is a letter, followed by zero or more alphanum chars (as well as space, - and _ for readability).
 | 
        
           | 1 | efrain | 43 |      */
 | 
        
           | 11 | efrain | 44 |     const PLACEHOLDER_REGEX_PART = '[[:alpha:]][[:alpha:][:digit:]\-_\s]*';
 | 
        
           | 1 | efrain | 45 |   | 
        
           |  |  | 46 |     /**
 | 
        
           |  |  | 47 |      * @var string REGEXP for a placeholder, wrapped in its {...} delimiters, with capturing brackets around the name.
 | 
        
           |  |  | 48 |      */
 | 
        
           |  |  | 49 |     const PLACEHODLER_REGEX = '~\{(' . self::PLACEHOLDER_REGEX_PART . ')\}~';
 | 
        
           |  |  | 50 |   | 
        
           |  |  | 51 |     /**
 | 
        
           |  |  | 52 |      * @var string Regular expression that finds the formulas in content, with capturing brackets to get the forumlas.
 | 
        
           |  |  | 53 |      */
 | 
        
           |  |  | 54 |     const FORMULAS_IN_TEXT_REGEX = '~\{=([^{}]*(?:\{' . self::PLACEHOLDER_REGEX_PART . '\}[^{}]*)*)\}~';
 | 
        
           |  |  | 55 |   | 
        
           |  |  | 56 |     const MAX_DATASET_ITEMS = 100;
 | 
        
           |  |  | 57 |   | 
        
           |  |  | 58 |     public $wizardpagesnumber = 3;
 | 
        
           |  |  | 59 |   | 
        
           |  |  | 60 |     public function get_question_options($question) {
 | 
        
           |  |  | 61 |         // First get the datasets and default options.
 | 
        
           |  |  | 62 |         // The code is used for calculated, calculatedsimple and calculatedmulti qtypes.
 | 
        
           |  |  | 63 |         global $CFG, $DB, $OUTPUT;
 | 
        
           |  |  | 64 |         parent::get_question_options($question);
 | 
        
           |  |  | 65 |         if (!$question->options = $DB->get_record('question_calculated_options',
 | 
        
           |  |  | 66 |                 ['question' => $question->id])) {
 | 
        
           |  |  | 67 |             $question->options = new stdClass();
 | 
        
           |  |  | 68 |             $question->options->synchronize = 0;
 | 
        
           |  |  | 69 |             $question->options->single = 0;
 | 
        
           |  |  | 70 |             $question->options->answernumbering = 'abc';
 | 
        
           |  |  | 71 |             $question->options->shuffleanswers = 0;
 | 
        
           |  |  | 72 |             $question->options->correctfeedback = '';
 | 
        
           |  |  | 73 |             $question->options->partiallycorrectfeedback = '';
 | 
        
           |  |  | 74 |             $question->options->incorrectfeedback = '';
 | 
        
           |  |  | 75 |             $question->options->correctfeedbackformat = 0;
 | 
        
           |  |  | 76 |             $question->options->partiallycorrectfeedbackformat = 0;
 | 
        
           |  |  | 77 |             $question->options->incorrectfeedbackformat = 0;
 | 
        
           |  |  | 78 |         }
 | 
        
           |  |  | 79 |   | 
        
           |  |  | 80 |         if (!$question->options->answers = $DB->get_records_sql("
 | 
        
           |  |  | 81 |             SELECT a.*, c.tolerance, c.tolerancetype, c.correctanswerlength, c.correctanswerformat
 | 
        
           |  |  | 82 |             FROM {question_answers} a,
 | 
        
           |  |  | 83 |                  {question_calculated} c
 | 
        
           |  |  | 84 |             WHERE a.question = ?
 | 
        
           |  |  | 85 |             AND   a.id = c.answer
 | 
        
           |  |  | 86 |             ORDER BY a.id ASC", [$question->id])) {
 | 
        
           |  |  | 87 |                 return false;
 | 
        
           |  |  | 88 |         }
 | 
        
           |  |  | 89 |   | 
        
           |  |  | 90 |         if ($this->get_virtual_qtype()->name() == 'numerical') {
 | 
        
           |  |  | 91 |             $this->get_virtual_qtype()->get_numerical_units($question);
 | 
        
           |  |  | 92 |             $this->get_virtual_qtype()->get_numerical_options($question);
 | 
        
           |  |  | 93 |         }
 | 
        
           |  |  | 94 |   | 
        
           |  |  | 95 |         $question->hints = $DB->get_records('question_hints',
 | 
        
           |  |  | 96 |                 ['questionid' => $question->id], 'id ASC');
 | 
        
           |  |  | 97 |   | 
        
           |  |  | 98 |         if (isset($question->export_process)&&$question->export_process) {
 | 
        
           |  |  | 99 |             $question->options->datasets = $this->get_datasets_for_export($question);
 | 
        
           |  |  | 100 |         }
 | 
        
           |  |  | 101 |         return true;
 | 
        
           |  |  | 102 |     }
 | 
        
           |  |  | 103 |   | 
        
           |  |  | 104 |     public function get_datasets_for_export($question) {
 | 
        
           |  |  | 105 |         global $DB, $CFG;
 | 
        
           |  |  | 106 |         $datasetdefs = [];
 | 
        
           |  |  | 107 |         if (!empty($question->id)) {
 | 
        
           |  |  | 108 |             $sql = "SELECT i.*
 | 
        
           |  |  | 109 |                       FROM {question_datasets} d, {question_dataset_definitions} i
 | 
        
           |  |  | 110 |                      WHERE d.question = ? AND d.datasetdefinition = i.id";
 | 
        
           |  |  | 111 |             if ($records = $DB->get_records_sql($sql, [$question->id])) {
 | 
        
           |  |  | 112 |                 foreach ($records as $r) {
 | 
        
           |  |  | 113 |                     $def = $r;
 | 
        
           |  |  | 114 |                     if ($def->category == '0') {
 | 
        
           |  |  | 115 |                         $def->status = 'private';
 | 
        
           |  |  | 116 |                     } else {
 | 
        
           |  |  | 117 |                         $def->status = 'shared';
 | 
        
           |  |  | 118 |                     }
 | 
        
           |  |  | 119 |                     $def->type = 'calculated';
 | 
        
           |  |  | 120 |                     list($distribution, $min, $max, $dec) = explode(':', $def->options, 4);
 | 
        
           |  |  | 121 |                     $def->distribution = $distribution;
 | 
        
           |  |  | 122 |                     $def->minimum = $min;
 | 
        
           |  |  | 123 |                     $def->maximum = $max;
 | 
        
           |  |  | 124 |                     $def->decimals = $dec;
 | 
        
           |  |  | 125 |                     if ($def->itemcount > 0) {
 | 
        
           |  |  | 126 |                         // Get the datasetitems.
 | 
        
           |  |  | 127 |                         $def->items = [];
 | 
        
           |  |  | 128 |                         if ($items = $this->get_database_dataset_items($def->id)) {
 | 
        
           |  |  | 129 |                             $n = 0;
 | 
        
           |  |  | 130 |                             foreach ($items as $ii) {
 | 
        
           |  |  | 131 |                                 $n++;
 | 
        
           |  |  | 132 |                                 $def->items[$n] = new stdClass();
 | 
        
           |  |  | 133 |                                 $def->items[$n]->itemnumber = $ii->itemnumber;
 | 
        
           |  |  | 134 |                                 $def->items[$n]->value = $ii->value;
 | 
        
           |  |  | 135 |                             }
 | 
        
           |  |  | 136 |                             $def->number_of_items = $n;
 | 
        
           |  |  | 137 |                         }
 | 
        
           |  |  | 138 |                     }
 | 
        
           |  |  | 139 |                     $datasetdefs["1-{$r->category}-{$r->name}"] = $def;
 | 
        
           |  |  | 140 |                 }
 | 
        
           |  |  | 141 |             }
 | 
        
           |  |  | 142 |         }
 | 
        
           |  |  | 143 |         return $datasetdefs;
 | 
        
           |  |  | 144 |     }
 | 
        
           |  |  | 145 |   | 
        
           |  |  | 146 |     public function save_question_options($question) {
 | 
        
           |  |  | 147 |         global $CFG, $DB;
 | 
        
           |  |  | 148 |   | 
        
           |  |  | 149 |         // Make it impossible to save bad formulas anywhere.
 | 
        
           |  |  | 150 |         $this->validate_question_data($question);
 | 
        
           |  |  | 151 |   | 
        
           |  |  | 152 |         // The code is used for calculated, calculatedsimple and calculatedmulti qtypes.
 | 
        
           |  |  | 153 |         $context = $question->context;
 | 
        
           |  |  | 154 |   | 
        
           |  |  | 155 |         // Calculated options.
 | 
        
           |  |  | 156 |         $update = true;
 | 
        
           |  |  | 157 |         $options = $DB->get_record('question_calculated_options',
 | 
        
           |  |  | 158 |                 ['question' => $question->id]);
 | 
        
           |  |  | 159 |         if (!$options) {
 | 
        
           |  |  | 160 |             $update = false;
 | 
        
           |  |  | 161 |             $options = new stdClass();
 | 
        
           |  |  | 162 |             $options->question = $question->id;
 | 
        
           |  |  | 163 |         }
 | 
        
           |  |  | 164 |         // As used only by calculated.
 | 
        
           |  |  | 165 |         if (isset($question->synchronize)) {
 | 
        
           |  |  | 166 |             $options->synchronize = $question->synchronize;
 | 
        
           |  |  | 167 |         } else {
 | 
        
           |  |  | 168 |             $options->synchronize = 0;
 | 
        
           |  |  | 169 |         }
 | 
        
           |  |  | 170 |         $options->single = 0;
 | 
        
           |  |  | 171 |         $options->answernumbering = $question->answernumbering;
 | 
        
           |  |  | 172 |         $options->shuffleanswers = $question->shuffleanswers;
 | 
        
           |  |  | 173 |   | 
        
           |  |  | 174 |         foreach (['correctfeedback', 'partiallycorrectfeedback',
 | 
        
           |  |  | 175 |                 'incorrectfeedback'] as $feedbackname) {
 | 
        
           |  |  | 176 |             $options->$feedbackname = '';
 | 
        
           |  |  | 177 |             $feedbackformat = $feedbackname . 'format';
 | 
        
           |  |  | 178 |             $options->$feedbackformat = 0;
 | 
        
           |  |  | 179 |         }
 | 
        
           |  |  | 180 |   | 
        
           |  |  | 181 |         if ($update) {
 | 
        
           |  |  | 182 |             $DB->update_record('question_calculated_options', $options);
 | 
        
           |  |  | 183 |         } else {
 | 
        
           |  |  | 184 |             $DB->insert_record('question_calculated_options', $options);
 | 
        
           |  |  | 185 |         }
 | 
        
           |  |  | 186 |   | 
        
           |  |  | 187 |         // Get old versions of the objects.
 | 
        
           |  |  | 188 |         $oldanswers = $DB->get_records('question_answers',
 | 
        
           |  |  | 189 |                 ['question' => $question->id], 'id ASC');
 | 
        
           |  |  | 190 |   | 
        
           |  |  | 191 |         $oldoptions = $DB->get_records('question_calculated',
 | 
        
           |  |  | 192 |                 ['question' => $question->id], 'answer ASC');
 | 
        
           |  |  | 193 |   | 
        
           |  |  | 194 |         // Save the units.
 | 
        
           |  |  | 195 |         $virtualqtype = $this->get_virtual_qtype();
 | 
        
           |  |  | 196 |   | 
        
           |  |  | 197 |         $result = $virtualqtype->save_units($question);
 | 
        
           |  |  | 198 |         if (isset($result->error)) {
 | 
        
           |  |  | 199 |             return $result;
 | 
        
           |  |  | 200 |         } else {
 | 
        
           |  |  | 201 |             $units = $result->units;
 | 
        
           |  |  | 202 |         }
 | 
        
           |  |  | 203 |   | 
        
           |  |  | 204 |         foreach ($question->answer as $key => $answerdata) {
 | 
        
           |  |  | 205 |             if (trim($answerdata) == '') {
 | 
        
           |  |  | 206 |                 continue;
 | 
        
           |  |  | 207 |             }
 | 
        
           |  |  | 208 |   | 
        
           |  |  | 209 |             // Update an existing answer if possible.
 | 
        
           |  |  | 210 |             $answer = array_shift($oldanswers);
 | 
        
           |  |  | 211 |             if (!$answer) {
 | 
        
           |  |  | 212 |                 $answer = new stdClass();
 | 
        
           |  |  | 213 |                 $answer->question = $question->id;
 | 
        
           |  |  | 214 |                 $answer->answer   = '';
 | 
        
           |  |  | 215 |                 $answer->feedback = '';
 | 
        
           |  |  | 216 |                 $answer->id       = $DB->insert_record('question_answers', $answer);
 | 
        
           |  |  | 217 |             }
 | 
        
           |  |  | 218 |   | 
        
           |  |  | 219 |             $answer->answer   = trim($answerdata);
 | 
        
           |  |  | 220 |             $answer->fraction = $question->fraction[$key];
 | 
        
           |  |  | 221 |             $answer->feedback = $this->import_or_save_files($question->feedback[$key],
 | 
        
           |  |  | 222 |                     $context, 'question', 'answerfeedback', $answer->id);
 | 
        
           |  |  | 223 |             $answer->feedbackformat = $question->feedback[$key]['format'];
 | 
        
           |  |  | 224 |   | 
        
           |  |  | 225 |             $DB->update_record("question_answers", $answer);
 | 
        
           |  |  | 226 |   | 
        
           |  |  | 227 |             // Set up the options object.
 | 
        
           |  |  | 228 |             if (!$options = array_shift($oldoptions)) {
 | 
        
           |  |  | 229 |                 $options = new stdClass();
 | 
        
           |  |  | 230 |             }
 | 
        
           |  |  | 231 |             $options->question            = $question->id;
 | 
        
           |  |  | 232 |             $options->answer              = $answer->id;
 | 
        
           |  |  | 233 |             $options->tolerance           = trim($question->tolerance[$key]);
 | 
        
           |  |  | 234 |             $options->tolerancetype       = trim($question->tolerancetype[$key]);
 | 
        
           |  |  | 235 |             $options->correctanswerlength = trim($question->correctanswerlength[$key]);
 | 
        
           |  |  | 236 |             $options->correctanswerformat = trim($question->correctanswerformat[$key]);
 | 
        
           |  |  | 237 |   | 
        
           |  |  | 238 |             // Save options.
 | 
        
           |  |  | 239 |             if (isset($options->id)) {
 | 
        
           |  |  | 240 |                 // Reusing existing record.
 | 
        
           |  |  | 241 |                 $DB->update_record('question_calculated', $options);
 | 
        
           |  |  | 242 |             } else {
 | 
        
           |  |  | 243 |                 // New options.
 | 
        
           |  |  | 244 |                 $DB->insert_record('question_calculated', $options);
 | 
        
           |  |  | 245 |             }
 | 
        
           |  |  | 246 |         }
 | 
        
           |  |  | 247 |   | 
        
           |  |  | 248 |         // Delete old answer records.
 | 
        
           |  |  | 249 |         if (!empty($oldanswers)) {
 | 
        
           |  |  | 250 |             foreach ($oldanswers as $oa) {
 | 
        
           |  |  | 251 |                 $DB->delete_records('question_answers', ['id' => $oa->id]);
 | 
        
           |  |  | 252 |             }
 | 
        
           |  |  | 253 |         }
 | 
        
           |  |  | 254 |   | 
        
           |  |  | 255 |         // Delete old answer records.
 | 
        
           |  |  | 256 |         if (!empty($oldoptions)) {
 | 
        
           |  |  | 257 |             foreach ($oldoptions as $oo) {
 | 
        
           |  |  | 258 |                 $DB->delete_records('question_calculated', ['id' => $oo->id]);
 | 
        
           |  |  | 259 |             }
 | 
        
           |  |  | 260 |         }
 | 
        
           |  |  | 261 |   | 
        
           |  |  | 262 |         $result = $virtualqtype->save_unit_options($question);
 | 
        
           |  |  | 263 |         if (isset($result->error)) {
 | 
        
           |  |  | 264 |             return $result;
 | 
        
           |  |  | 265 |         }
 | 
        
           |  |  | 266 |   | 
        
           |  |  | 267 |         $this->save_hints($question);
 | 
        
           |  |  | 268 |   | 
        
           |  |  | 269 |         if (isset($question->import_process)&&$question->import_process) {
 | 
        
           |  |  | 270 |             $this->import_datasets($question);
 | 
        
           |  |  | 271 |         }
 | 
        
           |  |  | 272 |         // Report any problems.
 | 
        
           |  |  | 273 |         if (!empty($result->notice)) {
 | 
        
           |  |  | 274 |             return $result;
 | 
        
           |  |  | 275 |         }
 | 
        
           |  |  | 276 |         return true;
 | 
        
           |  |  | 277 |     }
 | 
        
           |  |  | 278 |   | 
        
           |  |  | 279 |     public function import_datasets($question) {
 | 
        
           |  |  | 280 |         global $DB;
 | 
        
           |  |  | 281 |         $n = count($question->dataset);
 | 
        
           |  |  | 282 |         foreach ($question->dataset as $dataset) {
 | 
        
           |  |  | 283 |             // Name, type, option.
 | 
        
           |  |  | 284 |             $datasetdef = new stdClass();
 | 
        
           |  |  | 285 |             $datasetdef->name = $dataset->name;
 | 
        
           |  |  | 286 |             $datasetdef->type = 1;
 | 
        
           |  |  | 287 |             $datasetdef->options = $dataset->distribution . ':' . $dataset->min . ':' .
 | 
        
           |  |  | 288 |                     $dataset->max . ':' . $dataset->length;
 | 
        
           |  |  | 289 |             $datasetdef->itemcount = $dataset->itemcount;
 | 
        
           |  |  | 290 |             if ($dataset->status == 'private') {
 | 
        
           |  |  | 291 |                 $datasetdef->category = 0;
 | 
        
           |  |  | 292 |                 $todo = 'create';
 | 
        
           |  |  | 293 |             } else if ($dataset->status == 'shared') {
 | 
        
           |  |  | 294 |                 if ($sharedatasetdefs = $DB->get_records_select(
 | 
        
           |  |  | 295 |                     'question_dataset_definitions',
 | 
        
           |  |  | 296 |                     "type = '1'
 | 
        
           |  |  | 297 |                     AND " . $DB->sql_equal('name', '?') . "
 | 
        
           |  |  | 298 |                     AND category = ?
 | 
        
           |  |  | 299 |                     ORDER BY id DESC ", [$dataset->name, $question->category]
 | 
        
           |  |  | 300 |                 )) { // So there is at least one.
 | 
        
           |  |  | 301 |                     $sharedatasetdef = array_shift($sharedatasetdefs);
 | 
        
           |  |  | 302 |                     if ($sharedatasetdef->options == $datasetdef->options) {// Identical so use it.
 | 
        
           |  |  | 303 |                         $todo = 'useit';
 | 
        
           |  |  | 304 |                         $datasetdef = $sharedatasetdef;
 | 
        
           |  |  | 305 |                     } else { // Different so create a private one.
 | 
        
           |  |  | 306 |                         $datasetdef->category = 0;
 | 
        
           |  |  | 307 |                         $todo = 'create';
 | 
        
           |  |  | 308 |                     }
 | 
        
           |  |  | 309 |                 } else { // No so create one.
 | 
        
           |  |  | 310 |                     $datasetdef->category = $question->category;
 | 
        
           |  |  | 311 |                     $todo = 'create';
 | 
        
           |  |  | 312 |                 }
 | 
        
           |  |  | 313 |             }
 | 
        
           |  |  | 314 |             if ($todo == 'create') {
 | 
        
           |  |  | 315 |                 $datasetdef->id = $DB->insert_record('question_dataset_definitions', $datasetdef);
 | 
        
           |  |  | 316 |             }
 | 
        
           |  |  | 317 |             // Create relation to the dataset.
 | 
        
           |  |  | 318 |             $questiondataset = new stdClass();
 | 
        
           |  |  | 319 |             $questiondataset->question = $question->id;
 | 
        
           |  |  | 320 |             $questiondataset->datasetdefinition = $datasetdef->id;
 | 
        
           |  |  | 321 |             $DB->insert_record('question_datasets', $questiondataset);
 | 
        
           |  |  | 322 |             if ($todo == 'create') {
 | 
        
           |  |  | 323 |                 // Add the items.
 | 
        
           |  |  | 324 |                 foreach ($dataset->datasetitem as $dataitem) {
 | 
        
           |  |  | 325 |                     $datasetitem = new stdClass();
 | 
        
           |  |  | 326 |                     $datasetitem->definition = $datasetdef->id;
 | 
        
           |  |  | 327 |                     $datasetitem->itemnumber = $dataitem->itemnumber;
 | 
        
           |  |  | 328 |                     $datasetitem->value = $dataitem->value;
 | 
        
           |  |  | 329 |                     $DB->insert_record('question_dataset_items', $datasetitem);
 | 
        
           |  |  | 330 |                 }
 | 
        
           |  |  | 331 |             }
 | 
        
           |  |  | 332 |         }
 | 
        
           |  |  | 333 |     }
 | 
        
           |  |  | 334 |   | 
        
           |  |  | 335 |     /**
 | 
        
           |  |  | 336 |      * Initializes calculated answers for a given question.
 | 
        
           |  |  | 337 |      *
 | 
        
           |  |  | 338 |      * @param question_definition $question The question definition object.
 | 
        
           |  |  | 339 |      * @param stdClass $questiondata The question data object.
 | 
        
           |  |  | 340 |      */
 | 
        
           |  |  | 341 |     protected function initialise_calculated_answers(question_definition $question, stdClass $questiondata) {
 | 
        
           |  |  | 342 |         $question->answers = [];
 | 
        
           |  |  | 343 |         if (empty($questiondata->options->answers)) {
 | 
        
           |  |  | 344 |             return;
 | 
        
           |  |  | 345 |         }
 | 
        
           |  |  | 346 |         foreach ($questiondata->options->answers as $a) {
 | 
        
           |  |  | 347 |             $question->answers[$a->id] = new \qtype_calculated\qtype_calculated_answer($a->id, $a->answer,
 | 
        
           |  |  | 348 |                     $a->fraction, $a->feedback, $a->feedbackformat, $a->tolerance);
 | 
        
           |  |  | 349 |         }
 | 
        
           |  |  | 350 |     }
 | 
        
           |  |  | 351 |   | 
        
           |  |  | 352 |     protected function initialise_question_instance(question_definition $question, $questiondata) {
 | 
        
           |  |  | 353 |         parent::initialise_question_instance($question, $questiondata);
 | 
        
           |  |  | 354 |         $this->initialise_calculated_answers($question, $questiondata);
 | 
        
           |  |  | 355 |   | 
        
           |  |  | 356 |         foreach ($questiondata->options->answers as $a) {
 | 
        
           |  |  | 357 |             $question->answers[$a->id]->tolerancetype = $a->tolerancetype;
 | 
        
           |  |  | 358 |             $question->answers[$a->id]->correctanswerlength = $a->correctanswerlength;
 | 
        
           |  |  | 359 |             $question->answers[$a->id]->correctanswerformat = $a->correctanswerformat;
 | 
        
           |  |  | 360 |         }
 | 
        
           |  |  | 361 |   | 
        
           |  |  | 362 |         $question->synchronised = $questiondata->options->synchronize;
 | 
        
           |  |  | 363 |   | 
        
           |  |  | 364 |         $question->unitdisplay = $questiondata->options->showunits;
 | 
        
           |  |  | 365 |         $question->unitgradingtype = $questiondata->options->unitgradingtype;
 | 
        
           |  |  | 366 |         $question->unitpenalty = $questiondata->options->unitpenalty;
 | 
        
           |  |  | 367 |         $question->ap = question_bank::get_qtype(
 | 
        
           |  |  | 368 |                 'numerical')->make_answer_processor(
 | 
        
           |  |  | 369 |                 $questiondata->options->units, $questiondata->options->unitsleft);
 | 
        
           |  |  | 370 |   | 
        
           |  |  | 371 |         $question->datasetloader = new qtype_calculated_dataset_loader($questiondata->id);
 | 
        
           |  |  | 372 |     }
 | 
        
           |  |  | 373 |   | 
        
           |  |  | 374 |     public function finished_edit_wizard($form) {
 | 
        
           |  |  | 375 |         return isset($form->savechanges);
 | 
        
           |  |  | 376 |     }
 | 
        
           |  |  | 377 |     public function wizardpagesnumber() {
 | 
        
           |  |  | 378 |         return 3;
 | 
        
           |  |  | 379 |     }
 | 
        
           |  |  | 380 |     // This gets called by editquestion.php after the standard question is saved.
 | 
        
           |  |  | 381 |     public function print_next_wizard_page($question, $form, $course) {
 | 
        
           |  |  | 382 |         global $CFG, $SESSION, $COURSE;
 | 
        
           |  |  | 383 |   | 
        
           |  |  | 384 |         // Catch invalid navigation & reloads.
 | 
        
           |  |  | 385 |         if (empty($question->id) && empty($SESSION->calculated)) {
 | 
        
           |  |  | 386 |             redirect('edit.php?courseid='.$COURSE->id, 'The page you are loading has expired.', 3);
 | 
        
           |  |  | 387 |         }
 | 
        
           |  |  | 388 |   | 
        
           |  |  | 389 |         // See where we're coming from.
 | 
        
           |  |  | 390 |         switch($form->wizardpage) {
 | 
        
           |  |  | 391 |             case 'question':
 | 
        
           |  |  | 392 |                 require("{$CFG->dirroot}/question/type/calculated/datasetdefinitions.php");
 | 
        
           |  |  | 393 |                 break;
 | 
        
           |  |  | 394 |             case 'datasetdefinitions':
 | 
        
           |  |  | 395 |             case 'datasetitems':
 | 
        
           |  |  | 396 |                 require("{$CFG->dirroot}/question/type/calculated/datasetitems.php");
 | 
        
           |  |  | 397 |                 break;
 | 
        
           |  |  | 398 |             default:
 | 
        
           |  |  | 399 |                 throw new \moodle_exception('invalidwizardpage', 'question');
 | 
        
           |  |  | 400 |                 break;
 | 
        
           |  |  | 401 |         }
 | 
        
           |  |  | 402 |     }
 | 
        
           |  |  | 403 |   | 
        
           |  |  | 404 |     // This gets called by question2.php after the standard question is saved.
 | 
        
           |  |  | 405 |     public function &next_wizard_form($submiturl, $question, $wizardnow) {
 | 
        
           |  |  | 406 |         global $CFG, $SESSION, $COURSE;
 | 
        
           |  |  | 407 |   | 
        
           |  |  | 408 |         // Catch invalid navigation & reloads.
 | 
        
           |  |  | 409 |         if (empty($question->id) && empty($SESSION->calculated)) {
 | 
        
           |  |  | 410 |             redirect('edit.php?courseid=' . $COURSE->id,
 | 
        
           |  |  | 411 |                     'The page you are loading has expired. Cannot get next wizard form.', 3);
 | 
        
           |  |  | 412 |         }
 | 
        
           |  |  | 413 |         if (empty($question->id)) {
 | 
        
           |  |  | 414 |             $question = $SESSION->calculated->questionform;
 | 
        
           |  |  | 415 |         }
 | 
        
           |  |  | 416 |   | 
        
           |  |  | 417 |         // See where we're coming from.
 | 
        
           |  |  | 418 |         switch($wizardnow) {
 | 
        
           |  |  | 419 |             case 'datasetdefinitions':
 | 
        
           |  |  | 420 |                 require("{$CFG->dirroot}/question/type/calculated/datasetdefinitions_form.php");
 | 
        
           |  |  | 421 |                 $mform = new question_dataset_dependent_definitions_form(
 | 
        
           |  |  | 422 |                         "{$submiturl}?wizardnow=datasetdefinitions", $question);
 | 
        
           |  |  | 423 |                 break;
 | 
        
           |  |  | 424 |             case 'datasetitems':
 | 
        
           |  |  | 425 |                 require("{$CFG->dirroot}/question/type/calculated/datasetitems_form.php");
 | 
        
           |  |  | 426 |                 $regenerate = optional_param('forceregeneration', false, PARAM_BOOL);
 | 
        
           |  |  | 427 |                 $mform = new question_dataset_dependent_items_form(
 | 
        
           |  |  | 428 |                         "{$submiturl}?wizardnow=datasetitems", $question, $regenerate);
 | 
        
           |  |  | 429 |                 break;
 | 
        
           |  |  | 430 |             default:
 | 
        
           |  |  | 431 |                 throw new \moodle_exception('invalidwizardpage', 'question');
 | 
        
           |  |  | 432 |                 break;
 | 
        
           |  |  | 433 |         }
 | 
        
           |  |  | 434 |   | 
        
           |  |  | 435 |         return $mform;
 | 
        
           |  |  | 436 |     }
 | 
        
           |  |  | 437 |   | 
        
           |  |  | 438 |     /**
 | 
        
           |  |  | 439 |      * This method should be overriden if you want to include a special heading or some other
 | 
        
           |  |  | 440 |      * html on a question editing page besides the question editing form.
 | 
        
           |  |  | 441 |      *
 | 
        
           |  |  | 442 |      * @param question_edit_form $mform a child of question_edit_form
 | 
        
           |  |  | 443 |      * @param object $question
 | 
        
           |  |  | 444 |      * @param string $wizardnow is '' for first page.
 | 
        
           |  |  | 445 |      */
 | 
        
           |  |  | 446 |     public function display_question_editing_page($mform, $question, $wizardnow) {
 | 
        
           |  |  | 447 |         global $OUTPUT;
 | 
        
           |  |  | 448 |         switch ($wizardnow) {
 | 
        
           |  |  | 449 |             case '':
 | 
        
           |  |  | 450 |                 // On the first page, the default display is fine.
 | 
        
           |  |  | 451 |                 parent::display_question_editing_page($mform, $question, $wizardnow);
 | 
        
           |  |  | 452 |                 return;
 | 
        
           |  |  | 453 |   | 
        
           |  |  | 454 |             case 'datasetdefinitions':
 | 
        
           |  |  | 455 |                 echo $OUTPUT->heading_with_help(
 | 
        
           |  |  | 456 |                         get_string('choosedatasetproperties', 'qtype_calculated'),
 | 
        
           |  |  | 457 |                         'questiondatasets', 'qtype_calculated');
 | 
        
           |  |  | 458 |                 break;
 | 
        
           |  |  | 459 |   | 
        
           |  |  | 460 |             case 'datasetitems':
 | 
        
           |  |  | 461 |                 echo $OUTPUT->heading_with_help(get_string('editdatasets', 'qtype_calculated'),
 | 
        
           |  |  | 462 |                         'questiondatasets', 'qtype_calculated');
 | 
        
           |  |  | 463 |                 break;
 | 
        
           |  |  | 464 |         }
 | 
        
           |  |  | 465 |   | 
        
           |  |  | 466 |         $mform->display();
 | 
        
           |  |  | 467 |     }
 | 
        
           |  |  | 468 |   | 
        
           |  |  | 469 |     /**
 | 
        
           |  |  | 470 |      * Verify that the equations in part of the question are OK.
 | 
        
           |  |  | 471 |      * We throw an exception here because this should have already been validated
 | 
        
           |  |  | 472 |      * by the form. This is just a last line of defence to prevent a question
 | 
        
           |  |  | 473 |      * being stored in the database if it has bad formulas. This saves us from,
 | 
        
           |  |  | 474 |      * for example, malicious imports.
 | 
        
           |  |  | 475 |      * @param string $text containing equations.
 | 
        
           |  |  | 476 |      */
 | 
        
           |  |  | 477 |     protected function validate_text($text) {
 | 
        
           |  |  | 478 |         $error = qtype_calculated_find_formula_errors_in_text($text);
 | 
        
           |  |  | 479 |         if ($error) {
 | 
        
           |  |  | 480 |             throw new coding_exception($error);
 | 
        
           |  |  | 481 |         }
 | 
        
           |  |  | 482 |     }
 | 
        
           |  |  | 483 |   | 
        
           |  |  | 484 |     /**
 | 
        
           |  |  | 485 |      * Verify that an answer is OK.
 | 
        
           |  |  | 486 |      * We throw an exception here because this should have already been validated
 | 
        
           |  |  | 487 |      * by the form. This is just a last line of defence to prevent a question
 | 
        
           |  |  | 488 |      * being stored in the database if it has bad formulas. This saves us from,
 | 
        
           |  |  | 489 |      * for example, malicious imports.
 | 
        
           |  |  | 490 |      * @param string $text containing equations.
 | 
        
           |  |  | 491 |      */
 | 
        
           |  |  | 492 |     protected function validate_answer($answer) {
 | 
        
           |  |  | 493 |         $error = qtype_calculated_find_formula_errors($answer);
 | 
        
           |  |  | 494 |         if ($error) {
 | 
        
           |  |  | 495 |             throw new coding_exception($error);
 | 
        
           |  |  | 496 |         }
 | 
        
           |  |  | 497 |     }
 | 
        
           |  |  | 498 |   | 
        
           |  |  | 499 |     /**
 | 
        
           |  |  | 500 |      * Validate data before save.
 | 
        
           |  |  | 501 |      * @param stdClass $question data from the form / import file.
 | 
        
           |  |  | 502 |      */
 | 
        
           |  |  | 503 |     protected function validate_question_data($question) {
 | 
        
           |  |  | 504 |         $this->validate_text($question->questiontext); // Yes, really no ['text'].
 | 
        
           |  |  | 505 |   | 
        
           |  |  | 506 |         if (isset($question->generalfeedback['text'])) {
 | 
        
           |  |  | 507 |             $this->validate_text($question->generalfeedback['text']);
 | 
        
           |  |  | 508 |         } else if (isset($question->generalfeedback)) {
 | 
        
           |  |  | 509 |             $this->validate_text($question->generalfeedback); // Because question import is weird.
 | 
        
           |  |  | 510 |         }
 | 
        
           |  |  | 511 |   | 
        
           |  |  | 512 |         foreach ($question->answer as $key => $answer) {
 | 
        
           |  |  | 513 |             $this->validate_answer($answer);
 | 
        
           |  |  | 514 |             $this->validate_text($question->feedback[$key]['text']);
 | 
        
           |  |  | 515 |         }
 | 
        
           |  |  | 516 |     }
 | 
        
           |  |  | 517 |   | 
        
           |  |  | 518 |     /**
 | 
        
           |  |  | 519 |      * Remove prefix #{..}# if exists.
 | 
        
           |  |  | 520 |      * @param $name a question name,
 | 
        
           |  |  | 521 |      * @return string the cleaned up question name.
 | 
        
           |  |  | 522 |      */
 | 
        
           |  |  | 523 |     public function clean_technical_prefix_from_question_name($name) {
 | 
        
           |  |  | 524 |         return preg_replace('~#\{([^[:space:]]*)#~', '', $name);
 | 
        
           |  |  | 525 |     }
 | 
        
           |  |  | 526 |   | 
        
           |  |  | 527 |     /**
 | 
        
           |  |  | 528 |      * This method prepare the $datasets in a format similar to dadatesetdefinitions_form.php
 | 
        
           |  |  | 529 |      * so that they can be saved
 | 
        
           |  |  | 530 |      * using the function save_dataset_definitions($form)
 | 
        
           |  |  | 531 |      * when creating a new calculated question or
 | 
        
           |  |  | 532 |      * when editing an already existing calculated question
 | 
        
           |  |  | 533 |      * or by  function save_as_new_dataset_definitions($form, $initialid)
 | 
        
           |  |  | 534 |      * when saving as new an already existing calculated question.
 | 
        
           |  |  | 535 |      *
 | 
        
           |  |  | 536 |      * @param object $form
 | 
        
           |  |  | 537 |      * @param int $questionfromid default = '0'
 | 
        
           |  |  | 538 |      */
 | 
        
           |  |  | 539 |     public function preparedatasets($form, $questionfromid = '0') {
 | 
        
           |  |  | 540 |   | 
        
           |  |  | 541 |         // The dataset names present in the edit_question_form and edit_calculated_form
 | 
        
           |  |  | 542 |         // are retrieved.
 | 
        
           |  |  | 543 |         $possibledatasets = $this->find_dataset_names($form->questiontext);
 | 
        
           |  |  | 544 |         $mandatorydatasets = [];
 | 
        
           |  |  | 545 |         foreach ($form->answer as $key => $answer) {
 | 
        
           |  |  | 546 |             $mandatorydatasets += $this->find_dataset_names($answer);
 | 
        
           |  |  | 547 |         }
 | 
        
           |  |  | 548 |         // If there are identical datasetdefs already saved in the original question
 | 
        
           |  |  | 549 |         // either when editing a question or saving as new,
 | 
        
           |  |  | 550 |         // they are retrieved using $questionfromid.
 | 
        
           |  |  | 551 |         if ($questionfromid != '0') {
 | 
        
           |  |  | 552 |             $form->id = $questionfromid;
 | 
        
           |  |  | 553 |         }
 | 
        
           |  |  | 554 |         $datasets = [];
 | 
        
           |  |  | 555 |         $key = 0;
 | 
        
           |  |  | 556 |         // Always prepare the mandatorydatasets present in the answers.
 | 
        
           |  |  | 557 |         // The $options are not used here.
 | 
        
           |  |  | 558 |         foreach ($mandatorydatasets as $datasetname) {
 | 
        
           |  |  | 559 |             if (!isset($datasets[$datasetname])) {
 | 
        
           |  |  | 560 |                 list($options, $selected) =
 | 
        
           |  |  | 561 |                     $this->dataset_options($form, $datasetname);
 | 
        
           |  |  | 562 |                 $datasets[$datasetname] = '';
 | 
        
           |  |  | 563 |                 $form->dataset[$key] = $selected;
 | 
        
           |  |  | 564 |                 $key++;
 | 
        
           |  |  | 565 |             }
 | 
        
           |  |  | 566 |         }
 | 
        
           |  |  | 567 |         // Do not prepare possibledatasets when creating a question.
 | 
        
           |  |  | 568 |         // They will defined and stored with datasetdefinitions_form.php.
 | 
        
           |  |  | 569 |         // The $options are not used here.
 | 
        
           |  |  | 570 |         if ($questionfromid != '0') {
 | 
        
           |  |  | 571 |   | 
        
           |  |  | 572 |             foreach ($possibledatasets as $datasetname) {
 | 
        
           |  |  | 573 |                 if (!isset($datasets[$datasetname])) {
 | 
        
           |  |  | 574 |                     list($options, $selected) =
 | 
        
           |  |  | 575 |                         $this->dataset_options($form, $datasetname, false);
 | 
        
           |  |  | 576 |                     $datasets[$datasetname] = '';
 | 
        
           |  |  | 577 |                     $form->dataset[$key] = $selected;
 | 
        
           |  |  | 578 |                     $key++;
 | 
        
           |  |  | 579 |                 }
 | 
        
           |  |  | 580 |             }
 | 
        
           |  |  | 581 |         }
 | 
        
           |  |  | 582 |         return $datasets;
 | 
        
           |  |  | 583 |     }
 | 
        
           |  |  | 584 |     public function addnamecategory(&$question) {
 | 
        
           |  |  | 585 |         global $DB;
 | 
        
           |  |  | 586 |         $categorydatasetdefs = $DB->get_records_sql(
 | 
        
           |  |  | 587 |             "SELECT  a.*
 | 
        
           |  |  | 588 |                FROM {question_datasets} b, {question_dataset_definitions} a
 | 
        
           |  |  | 589 |               WHERE a.id = b.datasetdefinition
 | 
        
           |  |  | 590 |                 AND a.type = '1'
 | 
        
           |  |  | 591 |                 AND a.category != 0
 | 
        
           |  |  | 592 |                 AND b.question = ?
 | 
        
           |  |  | 593 |            ORDER BY a.name ", [$question->id]);
 | 
        
           |  |  | 594 |         $questionname = $this->clean_technical_prefix_from_question_name($question->name);
 | 
        
           |  |  | 595 |   | 
        
           |  |  | 596 |         if (!empty($categorydatasetdefs)) {
 | 
        
           |  |  | 597 |             // There is at least one with the same name.
 | 
        
           |  |  | 598 |             $questionname = '#' . $questionname;
 | 
        
           |  |  | 599 |             foreach ($categorydatasetdefs as $def) {
 | 
        
           |  |  | 600 |                 if (strlen($def->name) + strlen($questionname) < 250) {
 | 
        
           |  |  | 601 |                     $questionname = '{' . $def->name . '}' . $questionname;
 | 
        
           |  |  | 602 |                 }
 | 
        
           |  |  | 603 |             }
 | 
        
           |  |  | 604 |             $questionname = '#' . $questionname;
 | 
        
           |  |  | 605 |         }
 | 
        
           |  |  | 606 |         $DB->set_field('question', 'name', $questionname, ['id' => $question->id]);
 | 
        
           |  |  | 607 |     }
 | 
        
           |  |  | 608 |   | 
        
           |  |  | 609 |     /**
 | 
        
           |  |  | 610 |      * this version save the available data at the different steps of the question editing process
 | 
        
           |  |  | 611 |      * without using global $SESSION as storage between steps
 | 
        
           |  |  | 612 |      * at the first step $wizardnow = 'question'
 | 
        
           |  |  | 613 |      *  when creating a new question
 | 
        
           |  |  | 614 |      *  when modifying a question
 | 
        
           |  |  | 615 |      *  when copying as a new question
 | 
        
           |  |  | 616 |      *  the general parameters and answers are saved using parent::save_question
 | 
        
           |  |  | 617 |      *  then the datasets are prepared and saved
 | 
        
           |  |  | 618 |      * at the second step $wizardnow = 'datasetdefinitions'
 | 
        
           |  |  | 619 |      *  the datadefs final type are defined as private, category or not a datadef
 | 
        
           |  |  | 620 |      * at the third step $wizardnow = 'datasetitems'
 | 
        
           |  |  | 621 |      *  the datadefs parameters and the data items are created or defined
 | 
        
           |  |  | 622 |      *
 | 
        
           |  |  | 623 |      * @param object question
 | 
        
           |  |  | 624 |      * @param object $form
 | 
        
           |  |  | 625 |      * @param int $course
 | 
        
           |  |  | 626 |      * @param PARAM_ALPHA $wizardnow should be added as we are coming from question2.php
 | 
        
           |  |  | 627 |      */
 | 
        
           |  |  | 628 |     public function save_question($question, $form) {
 | 
        
           |  |  | 629 |         global $DB;
 | 
        
           |  |  | 630 |   | 
        
           |  |  | 631 |         if ($this->wizardpagesnumber() == 1 || $question->qtype == 'calculatedsimple') {
 | 
        
           |  |  | 632 |             $question = parent::save_question($question, $form);
 | 
        
           |  |  | 633 |             return $question;
 | 
        
           |  |  | 634 |         }
 | 
        
           |  |  | 635 |   | 
        
           |  |  | 636 |         $wizardnow = optional_param('wizardnow', '', PARAM_ALPHA);
 | 
        
           |  |  | 637 |         $id = optional_param('id', 0, PARAM_INT); // Question id.
 | 
        
           |  |  | 638 |         // In case 'question':
 | 
        
           |  |  | 639 |         // For a new question $form->id is empty
 | 
        
           |  |  | 640 |         // when saving as new question.
 | 
        
           |  |  | 641 |         // The $question->id = 0, $form is $data from question2.php
 | 
        
           |  |  | 642 |         // and $data->makecopy is defined as $data->id is the initial question id.
 | 
        
           |  |  | 643 |         // Edit case. If it is a new question we don't necessarily need to
 | 
        
           |  |  | 644 |         // return a valid question object.
 | 
        
           |  |  | 645 |   | 
        
           |  |  | 646 |         // See where we're coming from.
 | 
        
           |  |  | 647 |         switch($wizardnow) {
 | 
        
           |  |  | 648 |             case '' :
 | 
        
           |  |  | 649 |             case 'question': // Coming from the first page, creating the second.
 | 
        
           |  |  | 650 |                 if (empty($form->id)) { // Or a new question $form->id is empty.
 | 
        
           |  |  | 651 |                     $question = parent::save_question($question, $form);
 | 
        
           |  |  | 652 |                     // Prepare the datasets using default $questionfromid.
 | 
        
           |  |  | 653 |                     $this->preparedatasets($form);
 | 
        
           |  |  | 654 |                     $form->id = $question->id;
 | 
        
           |  |  | 655 |                     $this->save_dataset_definitions($form);
 | 
        
           |  |  | 656 |                     if (isset($form->synchronize) && $form->synchronize == 2) {
 | 
        
           |  |  | 657 |                         $this->addnamecategory($question);
 | 
        
           |  |  | 658 |                     }
 | 
        
           |  |  | 659 |                 } else {
 | 
        
           |  |  | 660 |                     $questionfromid = $form->id;
 | 
        
           |  |  | 661 |                     $question = parent::save_question($question, $form);
 | 
        
           |  |  | 662 |                     // Prepare the datasets.
 | 
        
           |  |  | 663 |                     $this->preparedatasets($form, $questionfromid);
 | 
        
           |  |  | 664 |                     $form->id = $question->id;
 | 
        
           |  |  | 665 |                     $this->save_as_new_dataset_definitions($form, $questionfromid);
 | 
        
           |  |  | 666 |                     if (isset($form->synchronize) && $form->synchronize == 2) {
 | 
        
           |  |  | 667 |                         $this->addnamecategory($question);
 | 
        
           |  |  | 668 |                     }
 | 
        
           |  |  | 669 |                 }
 | 
        
           |  |  | 670 |                 break;
 | 
        
           |  |  | 671 |             case 'datasetdefinitions':
 | 
        
           |  |  | 672 |                 // Calculated options.
 | 
        
           |  |  | 673 |                 // It cannot go here without having done the first page,
 | 
        
           |  |  | 674 |                 // so the question_calculated_options should exist.
 | 
        
           |  |  | 675 |                 // We only need to update the synchronize field.
 | 
        
           |  |  | 676 |                 if (isset($form->synchronize)) {
 | 
        
           |  |  | 677 |                     $optionssynchronize = $form->synchronize;
 | 
        
           |  |  | 678 |                 } else {
 | 
        
           |  |  | 679 |                     $optionssynchronize = 0;
 | 
        
           |  |  | 680 |                 }
 | 
        
           |  |  | 681 |                 $DB->set_field('question_calculated_options', 'synchronize', $optionssynchronize,
 | 
        
           |  |  | 682 |                         ['question' => $question->id]);
 | 
        
           |  |  | 683 |                 if (isset($form->synchronize) && $form->synchronize == 2) {
 | 
        
           |  |  | 684 |                     $this->addnamecategory($question);
 | 
        
           |  |  | 685 |                 }
 | 
        
           |  |  | 686 |   | 
        
           |  |  | 687 |                 $this->save_dataset_definitions($form);
 | 
        
           |  |  | 688 |                 break;
 | 
        
           |  |  | 689 |             case 'datasetitems':
 | 
        
           |  |  | 690 |                 $this->save_dataset_items($question, $form);
 | 
        
           |  |  | 691 |                 $this->save_question_calculated($question, $form);
 | 
        
           |  |  | 692 |                 break;
 | 
        
           |  |  | 693 |             default:
 | 
        
           |  |  | 694 |                 throw new \moodle_exception('invalidwizardpage', 'question');
 | 
        
           |  |  | 695 |                 break;
 | 
        
           |  |  | 696 |         }
 | 
        
           |  |  | 697 |         return $question;
 | 
        
           |  |  | 698 |     }
 | 
        
           |  |  | 699 |   | 
        
           |  |  | 700 |     public function delete_question($questionid, $contextid) {
 | 
        
           |  |  | 701 |         global $DB;
 | 
        
           |  |  | 702 |   | 
        
           |  |  | 703 |         $DB->delete_records('question_calculated', ['question' => $questionid]);
 | 
        
           |  |  | 704 |         $DB->delete_records('question_calculated_options', ['question' => $questionid]);
 | 
        
           |  |  | 705 |         $DB->delete_records('question_numerical_units', ['question' => $questionid]);
 | 
        
           |  |  | 706 |         if ($datasets = $DB->get_records('question_datasets', ['question' => $questionid])) {
 | 
        
           |  |  | 707 |             foreach ($datasets as $dataset) {
 | 
        
           |  |  | 708 |                 if (!$DB->get_records_select('question_datasets',
 | 
        
           |  |  | 709 |                         "question != ? AND datasetdefinition = ? ",
 | 
        
           |  |  | 710 |                         [$questionid, $dataset->datasetdefinition])) {
 | 
        
           |  |  | 711 |                     $DB->delete_records('question_dataset_definitions',
 | 
        
           |  |  | 712 |                             ['id' => $dataset->datasetdefinition]);
 | 
        
           |  |  | 713 |                     $DB->delete_records('question_dataset_items',
 | 
        
           |  |  | 714 |                             ['definition' => $dataset->datasetdefinition]);
 | 
        
           |  |  | 715 |                 }
 | 
        
           |  |  | 716 |             }
 | 
        
           |  |  | 717 |         }
 | 
        
           |  |  | 718 |         $DB->delete_records('question_datasets', ['question' => $questionid]);
 | 
        
           |  |  | 719 |   | 
        
           |  |  | 720 |         parent::delete_question($questionid, $contextid);
 | 
        
           |  |  | 721 |     }
 | 
        
           |  |  | 722 |   | 
        
           |  |  | 723 |     public function get_random_guess_score($questiondata) {
 | 
        
           |  |  | 724 |         foreach ($questiondata->options->answers as $aid => $answer) {
 | 
        
           |  |  | 725 |             if ('*' == trim($answer->answer)) {
 | 
        
           |  |  | 726 |                 return max($answer->fraction - $questiondata->options->unitpenalty, 0);
 | 
        
           |  |  | 727 |             }
 | 
        
           |  |  | 728 |         }
 | 
        
           |  |  | 729 |         return 0;
 | 
        
           |  |  | 730 |     }
 | 
        
           |  |  | 731 |   | 
        
           |  |  | 732 |     public function supports_dataset_item_generation() {
 | 
        
           |  |  | 733 |         // Calculated support generation of randomly distributed number data.
 | 
        
           |  |  | 734 |         return true;
 | 
        
           |  |  | 735 |     }
 | 
        
           |  |  | 736 |   | 
        
           |  |  | 737 |     public function custom_generator_tools_part($mform, $idx, $j) {
 | 
        
           |  |  | 738 |   | 
        
           |  |  | 739 |         $minmaxgrp = [];
 | 
        
           |  |  | 740 |         $minmaxgrp[] = $mform->createElement('float', "calcmin[{$idx}]",
 | 
        
           |  |  | 741 |                 get_string('calcmin', 'qtype_calculated'));
 | 
        
           |  |  | 742 |         $minmaxgrp[] = $mform->createElement('float', "calcmax[{$idx}]",
 | 
        
           |  |  | 743 |                 get_string('calcmax', 'qtype_calculated'));
 | 
        
           |  |  | 744 |         $mform->addGroup($minmaxgrp, 'minmaxgrp',
 | 
        
           |  |  | 745 |                 get_string('minmax', 'qtype_calculated'), ' - ', false);
 | 
        
           |  |  | 746 |   | 
        
           |  |  | 747 |         $precisionoptions = range(0, 10);
 | 
        
           |  |  | 748 |         $mform->addElement('select', "calclength[{$idx}]",
 | 
        
           |  |  | 749 |                 get_string('calclength', 'qtype_calculated'), $precisionoptions);
 | 
        
           |  |  | 750 |   | 
        
           |  |  | 751 |         $distriboptions = ['uniform' => get_string('uniform', 'qtype_calculated'),
 | 
        
           |  |  | 752 |                 'loguniform' => get_string('loguniform', 'qtype_calculated')];
 | 
        
           |  |  | 753 |         $mform->addElement('select', "calcdistribution[{$idx}]",
 | 
        
           |  |  | 754 |                 get_string('calcdistribution', 'qtype_calculated'), $distriboptions);
 | 
        
           |  |  | 755 |     }
 | 
        
           |  |  | 756 |   | 
        
           |  |  | 757 |     public function custom_generator_set_data($datasetdefs, $formdata) {
 | 
        
           |  |  | 758 |         $idx = 1;
 | 
        
           |  |  | 759 |         foreach ($datasetdefs as $datasetdef) {
 | 
        
           |  |  | 760 |             if (preg_match('~^(uniform|loguniform):([^:]*):([^:]*):([0-9]*)$~',
 | 
        
           |  |  | 761 |                     $datasetdef->options, $regs)) {
 | 
        
           |  |  | 762 |                 $formdata["calcdistribution[{$idx}]"] = $regs[1];
 | 
        
           |  |  | 763 |                 $formdata["calcmin[{$idx}]"] = $regs[2];
 | 
        
           |  |  | 764 |                 $formdata["calcmax[{$idx}]"] = $regs[3];
 | 
        
           |  |  | 765 |                 $formdata["calclength[{$idx}]"] = $regs[4];
 | 
        
           |  |  | 766 |             }
 | 
        
           |  |  | 767 |             $idx++;
 | 
        
           |  |  | 768 |         }
 | 
        
           |  |  | 769 |         return $formdata;
 | 
        
           |  |  | 770 |     }
 | 
        
           |  |  | 771 |   | 
        
           |  |  | 772 |     public function custom_generator_tools($datasetdef) {
 | 
        
           |  |  | 773 |         global $OUTPUT;
 | 
        
           |  |  | 774 |         if (preg_match('~^(uniform|loguniform):([^:]*):([^:]*):([0-9]*)$~',
 | 
        
           |  |  | 775 |                 $datasetdef->options, $regs)) {
 | 
        
           |  |  | 776 |             $defid = "{$datasetdef->type}-{$datasetdef->category}-{$datasetdef->name}";
 | 
        
           |  |  | 777 |             for ($i = 0; $i < 10; ++$i) {
 | 
        
           |  |  | 778 |                 $lengthoptions[$i] = get_string(($regs[1] == 'uniform'
 | 
        
           |  |  | 779 |                     ? 'decimals'
 | 
        
           |  |  | 780 |                     : 'significantfigures'), 'qtype_calculated', $i);
 | 
        
           |  |  | 781 |             }
 | 
        
           |  |  | 782 |             $menu1 = html_writer::label(get_string('lengthoption', 'qtype_calculated'),
 | 
        
           |  |  | 783 |                 'menucalclength', false, ['class' => 'accesshide']);
 | 
        
           | 1441 | ariadna | 784 |             $menu1 .= html_writer::select($lengthoptions, 'calclength[]', $regs[4], null, ['class' => 'form-select']);
 | 
        
           | 1 | efrain | 785 |   | 
        
           |  |  | 786 |             $options = ['uniform' => get_string('uniformbit', 'qtype_calculated'),
 | 
        
           |  |  | 787 |                 'loguniform' => get_string('loguniformbit', 'qtype_calculated')];
 | 
        
           |  |  | 788 |             $menu2 = html_writer::label(get_string('distributionoption', 'qtype_calculated'),
 | 
        
           |  |  | 789 |                 'menucalcdistribution', false, ['class' => 'accesshide']);
 | 
        
           | 1441 | ariadna | 790 |             $menu2 .= html_writer::select($options, 'calcdistribution[]', $regs[1], null, ['class' => 'form-select']);
 | 
        
           | 1 | efrain | 791 |             return '<input type="submit" class="btn btn-secondary" onclick="'
 | 
        
           |  |  | 792 |                 . "getElementById('addform').regenerateddefid.value='{$defid}'; return true;"
 | 
        
           |  |  | 793 |                 .'" value="'. get_string('generatevalue', 'qtype_calculated') . '"/><br/>'
 | 
        
           |  |  | 794 |                 . '<input type="text" class="form-control" size="3" name="calcmin[]" '
 | 
        
           |  |  | 795 |                 . " value=\"{$regs[2]}\"/> & <input name=\"calcmax[]\" "
 | 
        
           |  |  | 796 |                 . ' type="text" class="form-control" size="3" value="' . $regs[3] .'"/> '
 | 
        
           |  |  | 797 |                 . $menu1 . '<br/>'
 | 
        
           |  |  | 798 |                 . $menu2;
 | 
        
           |  |  | 799 |         } else {
 | 
        
           |  |  | 800 |             return '';
 | 
        
           |  |  | 801 |         }
 | 
        
           |  |  | 802 |     }
 | 
        
           |  |  | 803 |   | 
        
           |  |  | 804 |   | 
        
           |  |  | 805 |     public function update_dataset_options($datasetdefs, $form) {
 | 
        
           |  |  | 806 |         global $OUTPUT;
 | 
        
           |  |  | 807 |         // Do we have information about new options ?
 | 
        
           |  |  | 808 |         if (empty($form->definition) || empty($form->calcmin)
 | 
        
           |  |  | 809 |                 ||empty($form->calcmax) || empty($form->calclength)
 | 
        
           |  |  | 810 |                 || empty($form->calcdistribution)) {
 | 
        
           |  |  | 811 |             // I guess not.
 | 
        
           |  |  | 812 |   | 
        
           |  |  | 813 |         } else {
 | 
        
           |  |  | 814 |             // Looks like we just could have some new information here.
 | 
        
           |  |  | 815 |             $uniquedefs = array_values(array_unique($form->definition));
 | 
        
           |  |  | 816 |             foreach ($uniquedefs as $key => $defid) {
 | 
        
           |  |  | 817 |                 if (isset($datasetdefs[$defid])
 | 
        
           |  |  | 818 |                         && is_numeric($form->calcmin[$key + 1])
 | 
        
           |  |  | 819 |                         && is_numeric($form->calcmax[$key + 1])
 | 
        
           |  |  | 820 |                         && is_numeric($form->calclength[$key + 1])) {
 | 
        
           |  |  | 821 |                     switch     ($form->calcdistribution[$key + 1]) {
 | 
        
           |  |  | 822 |                         case 'uniform': case 'loguniform':
 | 
        
           |  |  | 823 |                                 $datasetdefs[$defid]->options =
 | 
        
           |  |  | 824 |                                 $form->calcdistribution[$key + 1] . ':'
 | 
        
           |  |  | 825 |                                 . $form->calcmin[$key + 1] . ':'
 | 
        
           |  |  | 826 |                                 . $form->calcmax[$key + 1] . ':'
 | 
        
           |  |  | 827 |                                 . $form->calclength[$key + 1];
 | 
        
           |  |  | 828 |                             break;
 | 
        
           |  |  | 829 |                         default:
 | 
        
           |  |  | 830 |                             echo $OUTPUT->notification(
 | 
        
           |  |  | 831 |                                     "Unexpected distribution ".$form->calcdistribution[$key + 1]);
 | 
        
           |  |  | 832 |                     }
 | 
        
           |  |  | 833 |                 }
 | 
        
           |  |  | 834 |             }
 | 
        
           |  |  | 835 |         }
 | 
        
           |  |  | 836 |   | 
        
           |  |  | 837 |         // Look for empty options, on which we set default values.
 | 
        
           |  |  | 838 |         foreach ($datasetdefs as $defid => $def) {
 | 
        
           |  |  | 839 |             if (empty($def->options)) {
 | 
        
           |  |  | 840 |                 $datasetdefs[$defid]->options = 'uniform:1.0:10.0:1';
 | 
        
           |  |  | 841 |             }
 | 
        
           |  |  | 842 |         }
 | 
        
           |  |  | 843 |         return $datasetdefs;
 | 
        
           |  |  | 844 |     }
 | 
        
           |  |  | 845 |   | 
        
           |  |  | 846 |     public function save_question_calculated($question, $fromform) {
 | 
        
           |  |  | 847 |         global $DB;
 | 
        
           |  |  | 848 |   | 
        
           |  |  | 849 |         foreach ($question->options->answers as $key => $answer) {
 | 
        
           |  |  | 850 |             if ($options = $DB->get_record('question_calculated', ['answer' => $key])) {
 | 
        
           |  |  | 851 |                 $options->tolerance = trim($fromform->tolerance[$key]);
 | 
        
           |  |  | 852 |                 $options->tolerancetype  = trim($fromform->tolerancetype[$key]);
 | 
        
           |  |  | 853 |                 $options->correctanswerlength  = trim($fromform->correctanswerlength[$key]);
 | 
        
           |  |  | 854 |                 $options->correctanswerformat  = trim($fromform->correctanswerformat[$key]);
 | 
        
           |  |  | 855 |                 $DB->update_record('question_calculated', $options);
 | 
        
           |  |  | 856 |             }
 | 
        
           |  |  | 857 |         }
 | 
        
           |  |  | 858 |     }
 | 
        
           |  |  | 859 |   | 
        
           |  |  | 860 |     /**
 | 
        
           |  |  | 861 |      * This function get the dataset items using id as unique parameter and return an
 | 
        
           |  |  | 862 |      * array with itemnumber as index sorted ascendant
 | 
        
           |  |  | 863 |      * If the multiple records with the same itemnumber exist, only the newest one
 | 
        
           |  |  | 864 |      * i.e with the greatest id is used, the others are ignored but not deleted.
 | 
        
           |  |  | 865 |      * MDL-19210
 | 
        
           |  |  | 866 |      */
 | 
        
           |  |  | 867 |     public function get_database_dataset_items($definition) {
 | 
        
           |  |  | 868 |         global $CFG, $DB;
 | 
        
           |  |  | 869 |         $databasedataitems = $DB->get_records_sql( // Hint: Use the number as a key.
 | 
        
           |  |  | 870 |             " SELECT id , itemnumber, definition,  value
 | 
        
           |  |  | 871 |             FROM {question_dataset_items}
 | 
        
           |  |  | 872 |             WHERE definition = $definition order by id DESC ", [$definition]);
 | 
        
           |  |  | 873 |         $dataitems = [];
 | 
        
           |  |  | 874 |         foreach ($databasedataitems as $id => $dataitem) {
 | 
        
           |  |  | 875 |             if (!isset($dataitems[$dataitem->itemnumber])) {
 | 
        
           |  |  | 876 |                 $dataitems[$dataitem->itemnumber] = $dataitem;
 | 
        
           |  |  | 877 |             }
 | 
        
           |  |  | 878 |         }
 | 
        
           |  |  | 879 |         ksort($dataitems);
 | 
        
           |  |  | 880 |         return $dataitems;
 | 
        
           |  |  | 881 |     }
 | 
        
           |  |  | 882 |   | 
        
           |  |  | 883 |     public function save_dataset_items($question, $fromform) {
 | 
        
           |  |  | 884 |         global $CFG, $DB;
 | 
        
           |  |  | 885 |         $synchronize = false;
 | 
        
           |  |  | 886 |         if (isset($fromform->nextpageparam['forceregeneration'])) {
 | 
        
           |  |  | 887 |             $regenerate = $fromform->nextpageparam['forceregeneration'];
 | 
        
           |  |  | 888 |         } else {
 | 
        
           |  |  | 889 |             $regenerate = 0;
 | 
        
           |  |  | 890 |         }
 | 
        
           |  |  | 891 |         if (empty($question->options)) {
 | 
        
           |  |  | 892 |             $this->get_question_options($question);
 | 
        
           |  |  | 893 |         }
 | 
        
           |  |  | 894 |         if (!empty($question->options->synchronize)) {
 | 
        
           |  |  | 895 |             $synchronize = true;
 | 
        
           |  |  | 896 |         }
 | 
        
           |  |  | 897 |   | 
        
           |  |  | 898 |         // Get the old datasets for this question.
 | 
        
           |  |  | 899 |         $datasetdefs = $this->get_dataset_definitions($question->id, []);
 | 
        
           |  |  | 900 |         // Handle generator options...
 | 
        
           |  |  | 901 |         $olddatasetdefs = fullclone($datasetdefs);
 | 
        
           |  |  | 902 |         $datasetdefs = $this->update_dataset_options($datasetdefs, $fromform);
 | 
        
           |  |  | 903 |         $maxnumber = -1;
 | 
        
           |  |  | 904 |         foreach ($datasetdefs as $defid => $datasetdef) {
 | 
        
           |  |  | 905 |             if (isset($datasetdef->id)
 | 
        
           |  |  | 906 |                     && $datasetdef->options != $olddatasetdefs[$defid]->options) {
 | 
        
           |  |  | 907 |                 // Save the new value for options.
 | 
        
           |  |  | 908 |                 $DB->update_record('question_dataset_definitions', $datasetdef);
 | 
        
           |  |  | 909 |   | 
        
           |  |  | 910 |             }
 | 
        
           |  |  | 911 |             // Get maxnumber.
 | 
        
           |  |  | 912 |             if ($maxnumber == -1 || $datasetdef->itemcount < $maxnumber) {
 | 
        
           |  |  | 913 |                 $maxnumber = $datasetdef->itemcount;
 | 
        
           |  |  | 914 |             }
 | 
        
           |  |  | 915 |         }
 | 
        
           |  |  | 916 |         // Handle adding and removing of dataset items.
 | 
        
           |  |  | 917 |         $i = 1;
 | 
        
           |  |  | 918 |         if ($maxnumber > self::MAX_DATASET_ITEMS) {
 | 
        
           |  |  | 919 |             $maxnumber = self::MAX_DATASET_ITEMS;
 | 
        
           |  |  | 920 |         }
 | 
        
           |  |  | 921 |   | 
        
           |  |  | 922 |         ksort($fromform->definition);
 | 
        
           |  |  | 923 |         foreach ($fromform->definition as $key => $defid) {
 | 
        
           |  |  | 924 |             // If the delete button has not been pressed then skip the datasetitems
 | 
        
           |  |  | 925 |             // in the 'add item' part of the form.
 | 
        
           |  |  | 926 |             if ($i > count($datasetdefs) * $maxnumber) {
 | 
        
           |  |  | 927 |                 break;
 | 
        
           |  |  | 928 |             }
 | 
        
           |  |  | 929 |             $addeditem = new stdClass();
 | 
        
           |  |  | 930 |             $addeditem->definition = $datasetdefs[$defid]->id;
 | 
        
           |  |  | 931 |             $addeditem->value = $fromform->number[$i];
 | 
        
           |  |  | 932 |             $addeditem->itemnumber = ceil($i / count($datasetdefs));
 | 
        
           |  |  | 933 |   | 
        
           |  |  | 934 |             if ($fromform->itemid[$i]) {
 | 
        
           |  |  | 935 |                 // Reuse any previously used record.
 | 
        
           |  |  | 936 |                 $addeditem->id = $fromform->itemid[$i];
 | 
        
           |  |  | 937 |                 $DB->update_record('question_dataset_items', $addeditem);
 | 
        
           |  |  | 938 |             } else {
 | 
        
           |  |  | 939 |                 $DB->insert_record('question_dataset_items', $addeditem);
 | 
        
           |  |  | 940 |             }
 | 
        
           |  |  | 941 |   | 
        
           |  |  | 942 |             $i++;
 | 
        
           |  |  | 943 |         }
 | 
        
           |  |  | 944 |         if (isset($addeditem->itemnumber) && $maxnumber < $addeditem->itemnumber
 | 
        
           |  |  | 945 |                 && $addeditem->itemnumber < self::MAX_DATASET_ITEMS) {
 | 
        
           |  |  | 946 |             $maxnumber = $addeditem->itemnumber;
 | 
        
           |  |  | 947 |             foreach ($datasetdefs as $key => $newdef) {
 | 
        
           |  |  | 948 |                 if (isset($newdef->id) && $newdef->itemcount <= $maxnumber) {
 | 
        
           |  |  | 949 |                     $newdef->itemcount = $maxnumber;
 | 
        
           |  |  | 950 |                     // Save the new value for options.
 | 
        
           |  |  | 951 |                     $DB->update_record('question_dataset_definitions', $newdef);
 | 
        
           |  |  | 952 |                 }
 | 
        
           |  |  | 953 |             }
 | 
        
           |  |  | 954 |         }
 | 
        
           |  |  | 955 |         // Adding supplementary items.
 | 
        
           |  |  | 956 |         $numbertoadd = 0;
 | 
        
           |  |  | 957 |         if (isset($fromform->addbutton) && $fromform->selectadd > 0 &&
 | 
        
           |  |  | 958 |                 $maxnumber < self::MAX_DATASET_ITEMS) {
 | 
        
           |  |  | 959 |             $numbertoadd = $fromform->selectadd;
 | 
        
           |  |  | 960 |             if (self::MAX_DATASET_ITEMS - $maxnumber < $numbertoadd) {
 | 
        
           |  |  | 961 |                 $numbertoadd = self::MAX_DATASET_ITEMS - $maxnumber;
 | 
        
           |  |  | 962 |             }
 | 
        
           |  |  | 963 |             // Add the other items.
 | 
        
           |  |  | 964 |             // Generate a new dataset item (or reuse an old one).
 | 
        
           |  |  | 965 |             foreach ($datasetdefs as $defid => $datasetdef) {
 | 
        
           |  |  | 966 |                 // In case that for category datasets some new items has been added,
 | 
        
           |  |  | 967 |                 // get actual values.
 | 
        
           |  |  | 968 |                 // Fix regenerate for this datadefs.
 | 
        
           |  |  | 969 |                 $defregenerate = 0;
 | 
        
           |  |  | 970 |                 if ($synchronize &&
 | 
        
           |  |  | 971 |                         !empty ($fromform->nextpageparam["datasetregenerate[{$datasetdef->name}"])) {
 | 
        
           |  |  | 972 |                     $defregenerate = 1;
 | 
        
           |  |  | 973 |                 } else if (!$synchronize &&
 | 
        
           |  |  | 974 |                         (($regenerate == 1 && $datasetdef->category == 0) ||$regenerate == 2)) {
 | 
        
           |  |  | 975 |                     $defregenerate = 1;
 | 
        
           |  |  | 976 |                 }
 | 
        
           |  |  | 977 |                 if (isset($datasetdef->id)) {
 | 
        
           |  |  | 978 |                     $datasetdefs[$defid]->items =
 | 
        
           |  |  | 979 |                             $this->get_database_dataset_items($datasetdef->id);
 | 
        
           |  |  | 980 |                 }
 | 
        
           |  |  | 981 |                 for ($numberadded = $maxnumber + 1; $numberadded <= $maxnumber + $numbertoadd; $numberadded++) {
 | 
        
           |  |  | 982 |                     if (isset($datasetdefs[$defid]->items[$numberadded])) {
 | 
        
           |  |  | 983 |                         // In case of regenerate it modifies the already existing record.
 | 
        
           |  |  | 984 |                         if ($defregenerate) {
 | 
        
           |  |  | 985 |                             $datasetitem = new stdClass();
 | 
        
           |  |  | 986 |                             $datasetitem->id = $datasetdefs[$defid]->items[$numberadded]->id;
 | 
        
           |  |  | 987 |                             $datasetitem->definition = $datasetdef->id;
 | 
        
           |  |  | 988 |                             $datasetitem->itemnumber = $numberadded;
 | 
        
           |  |  | 989 |                             $datasetitem->value =
 | 
        
           |  |  | 990 |                                     $this->generate_dataset_item($datasetdef->options);
 | 
        
           |  |  | 991 |                             $DB->update_record('question_dataset_items', $datasetitem);
 | 
        
           |  |  | 992 |                         }
 | 
        
           |  |  | 993 |                         // If not regenerate do nothing as there is already a record.
 | 
        
           |  |  | 994 |                     } else {
 | 
        
           |  |  | 995 |                         $datasetitem = new stdClass();
 | 
        
           |  |  | 996 |                         $datasetitem->definition = $datasetdef->id;
 | 
        
           |  |  | 997 |                         $datasetitem->itemnumber = $numberadded;
 | 
        
           |  |  | 998 |                         if ($this->supports_dataset_item_generation()) {
 | 
        
           |  |  | 999 |                             $datasetitem->value =
 | 
        
           |  |  | 1000 |                                     $this->generate_dataset_item($datasetdef->options);
 | 
        
           |  |  | 1001 |                         } else {
 | 
        
           |  |  | 1002 |                             $datasetitem->value = '';
 | 
        
           |  |  | 1003 |                         }
 | 
        
           |  |  | 1004 |                         $DB->insert_record('question_dataset_items', $datasetitem);
 | 
        
           |  |  | 1005 |                     }
 | 
        
           |  |  | 1006 |                 }// For number added.
 | 
        
           |  |  | 1007 |             }// Datasetsdefs end.
 | 
        
           |  |  | 1008 |             $maxnumber += $numbertoadd;
 | 
        
           |  |  | 1009 |             foreach ($datasetdefs as $key => $newdef) {
 | 
        
           |  |  | 1010 |                 if (isset($newdef->id) && $newdef->itemcount <= $maxnumber) {
 | 
        
           |  |  | 1011 |                     $newdef->itemcount = $maxnumber;
 | 
        
           |  |  | 1012 |                     // Save the new value for options.
 | 
        
           |  |  | 1013 |                     $DB->update_record('question_dataset_definitions', $newdef);
 | 
        
           |  |  | 1014 |                 }
 | 
        
           |  |  | 1015 |             }
 | 
        
           |  |  | 1016 |         }
 | 
        
           |  |  | 1017 |   | 
        
           |  |  | 1018 |         if (isset($fromform->deletebutton)) {
 | 
        
           |  |  | 1019 |             if (isset($fromform->selectdelete)) {
 | 
        
           |  |  | 1020 |                 $newmaxnumber = $maxnumber - $fromform->selectdelete;
 | 
        
           |  |  | 1021 |             } else {
 | 
        
           |  |  | 1022 |                 $newmaxnumber = $maxnumber - 1;
 | 
        
           |  |  | 1023 |             }
 | 
        
           |  |  | 1024 |             if ($newmaxnumber < 0) {
 | 
        
           |  |  | 1025 |                 $newmaxnumber = 0;
 | 
        
           |  |  | 1026 |             }
 | 
        
           |  |  | 1027 |             foreach ($datasetdefs as $datasetdef) {
 | 
        
           |  |  | 1028 |                 if ($datasetdef->itemcount == $maxnumber) {
 | 
        
           |  |  | 1029 |                     $datasetdef->itemcount = $newmaxnumber;
 | 
        
           |  |  | 1030 |                     $DB->update_record('question_dataset_definitions', $datasetdef);
 | 
        
           |  |  | 1031 |                 }
 | 
        
           |  |  | 1032 |             }
 | 
        
           |  |  | 1033 |         }
 | 
        
           |  |  | 1034 |     }
 | 
        
           |  |  | 1035 |     public function generate_dataset_item($options) {
 | 
        
           |  |  | 1036 |         if (!preg_match('~^(uniform|loguniform):([^:]*):([^:]*):([0-9]*)$~',
 | 
        
           |  |  | 1037 |                 $options, $regs)) {
 | 
        
           |  |  | 1038 |             // Unknown options...
 | 
        
           |  |  | 1039 |             return false;
 | 
        
           |  |  | 1040 |         }
 | 
        
           |  |  | 1041 |         if ($regs[1] == 'uniform') {
 | 
        
           |  |  | 1042 |             $nbr = $regs[2] + ($regs[3] - $regs[2]) * mt_rand() / mt_getrandmax();
 | 
        
           |  |  | 1043 |             return sprintf("%.".$regs[4].'f', $nbr);
 | 
        
           |  |  | 1044 |   | 
        
           |  |  | 1045 |         } else if ($regs[1] == 'loguniform') {
 | 
        
           |  |  | 1046 |             $log0 = log(abs($regs[2])); // It would have worked the other way to.
 | 
        
           |  |  | 1047 |             $nbr = exp($log0 + (log(abs($regs[3])) - $log0) * mt_rand() / mt_getrandmax());
 | 
        
           |  |  | 1048 |             return sprintf("%.".$regs[4].'f', $nbr);
 | 
        
           |  |  | 1049 |   | 
        
           |  |  | 1050 |         } else {
 | 
        
           |  |  | 1051 |             throw new \moodle_exception('disterror', 'question', '', $regs[1]);
 | 
        
           |  |  | 1052 |         }
 | 
        
           |  |  | 1053 |         return '';
 | 
        
           |  |  | 1054 |     }
 | 
        
           |  |  | 1055 |   | 
        
           |  |  | 1056 |     public function comment_header($question) {
 | 
        
           |  |  | 1057 |         $strheader = '';
 | 
        
           |  |  | 1058 |         $delimiter = '';
 | 
        
           |  |  | 1059 |   | 
        
           |  |  | 1060 |         $answers = $question->options->answers;
 | 
        
           |  |  | 1061 |   | 
        
           |  |  | 1062 |         foreach ($answers as $key => $answer) {
 | 
        
           |  |  | 1063 |             $ans = shorten_text($answer->answer, 17, true);
 | 
        
           |  |  | 1064 |             $strheader .= $delimiter.$ans;
 | 
        
           |  |  | 1065 |             $delimiter = '<br/><br/><br/>';
 | 
        
           |  |  | 1066 |         }
 | 
        
           |  |  | 1067 |         return $strheader;
 | 
        
           |  |  | 1068 |     }
 | 
        
           |  |  | 1069 |   | 
        
           |  |  | 1070 |     public function comment_on_datasetitems($qtypeobj, $questionid, $questiontext,
 | 
        
           |  |  | 1071 |             $answers, $data, $number) {
 | 
        
           |  |  | 1072 |         global $DB;
 | 
        
           |  |  | 1073 |         $comment = new stdClass();
 | 
        
           |  |  | 1074 |         $comment->stranswers = [];
 | 
        
           |  |  | 1075 |         $comment->outsidelimit = false;
 | 
        
           |  |  | 1076 |         $comment->answers = [];
 | 
        
           |  |  | 1077 |         // Find a default unit.
 | 
        
           |  |  | 1078 |         $unit = '';
 | 
        
           |  |  | 1079 |         if (!empty($questionid)) {
 | 
        
           |  |  | 1080 |             $units = $DB->get_records('question_numerical_units',
 | 
        
           |  |  | 1081 |                 ['question' => $questionid, 'multiplier' => 1.0],
 | 
        
           |  |  | 1082 |                 'id ASC', '*', 0, 1);
 | 
        
           |  |  | 1083 |             if ($units) {
 | 
        
           |  |  | 1084 |                 $unit = reset($units);
 | 
        
           |  |  | 1085 |                 $unit = $unit->unit;
 | 
        
           |  |  | 1086 |             }
 | 
        
           |  |  | 1087 |         }
 | 
        
           |  |  | 1088 |   | 
        
           |  |  | 1089 |         $answers = fullclone($answers);
 | 
        
           |  |  | 1090 |         $delimiter = ': ';
 | 
        
           |  |  | 1091 |         $virtualqtype = $qtypeobj->get_virtual_qtype();
 | 
        
           |  |  | 1092 |         foreach ($answers as $key => $answer) {
 | 
        
           |  |  | 1093 |             $error = qtype_calculated_find_formula_errors($answer->answer);
 | 
        
           |  |  | 1094 |             if ($error) {
 | 
        
           |  |  | 1095 |                 $comment->stranswers[$key] = $error;
 | 
        
           |  |  | 1096 |                 continue;
 | 
        
           |  |  | 1097 |             }
 | 
        
           |  |  | 1098 |             $formula = $this->substitute_variables($answer->answer, $data);
 | 
        
           |  |  | 1099 |             $formattedanswer = qtype_calculated_calculate_answer(
 | 
        
           |  |  | 1100 |                 $answer->answer, $data, $answer->tolerance,
 | 
        
           |  |  | 1101 |                 $answer->tolerancetype, $answer->correctanswerlength,
 | 
        
           |  |  | 1102 |                 $answer->correctanswerformat, $unit);
 | 
        
           |  |  | 1103 |             if ($formula === '*') {
 | 
        
           |  |  | 1104 |                 $answer->min = ' ';
 | 
        
           |  |  | 1105 |                 $formattedanswer->answer = $answer->answer;
 | 
        
           |  |  | 1106 |             } else {
 | 
        
           |  |  | 1107 |                 eval('$ansvalue = '.$formula.';');
 | 
        
           |  |  | 1108 |                 $ans = new qtype_numerical_answer(0, $ansvalue, 0, '', 0, $answer->tolerance);
 | 
        
           |  |  | 1109 |                 $ans->tolerancetype = $answer->tolerancetype;
 | 
        
           |  |  | 1110 |                 list($answer->min, $answer->max) = $ans->get_tolerance_interval($answer);
 | 
        
           |  |  | 1111 |             }
 | 
        
           |  |  | 1112 |             if ($answer->min === '') {
 | 
        
           |  |  | 1113 |                 // This should mean that something is wrong.
 | 
        
           |  |  | 1114 |                 $comment->stranswers[$key] = " {$formattedanswer->answer}".'<br/><br/>';
 | 
        
           |  |  | 1115 |             } else if ($formula === '*') {
 | 
        
           |  |  | 1116 |                 $comment->stranswers[$key] = $formula . ' = ' .
 | 
        
           |  |  | 1117 |                         get_string('anyvalue', 'qtype_calculated') . '<br/><br/><br/>';
 | 
        
           |  |  | 1118 |             } else {
 | 
        
           |  |  | 1119 |                 $formula = shorten_text($formula, 57, true);
 | 
        
           |  |  | 1120 |                 $comment->stranswers[$key] = $formula . ' = ' . $formattedanswer->answer . '<br/>';
 | 
        
           |  |  | 1121 |                 $correcttrue = new stdClass();
 | 
        
           |  |  | 1122 |                 $correcttrue->correct = $formattedanswer->answer;
 | 
        
           |  |  | 1123 |                 $correcttrue->true = '';
 | 
        
           |  |  | 1124 |                 if ((float) $formattedanswer->answer < $answer->min ||
 | 
        
           |  |  | 1125 |                     (float) $formattedanswer->answer > $answer->max) {
 | 
        
           |  |  | 1126 |                     $comment->outsidelimit = true;
 | 
        
           |  |  | 1127 |                     $comment->answers[$key] = $key;
 | 
        
           |  |  | 1128 |                     $comment->stranswers[$key] .=
 | 
        
           |  |  | 1129 |                             get_string('trueansweroutsidelimits', 'qtype_calculated', $correcttrue);
 | 
        
           |  |  | 1130 |                 } else {
 | 
        
           |  |  | 1131 |                     $comment->stranswers[$key] .=
 | 
        
           |  |  | 1132 |                             get_string('trueanswerinsidelimits', 'qtype_calculated', $correcttrue);
 | 
        
           |  |  | 1133 |                 }
 | 
        
           |  |  | 1134 |                 $comment->stranswers[$key] .= '<br/>';
 | 
        
           |  |  | 1135 |                 $comment->stranswers[$key] .= get_string('min', 'qtype_calculated') .
 | 
        
           |  |  | 1136 |                         $delimiter . $answer->min . ' --- ';
 | 
        
           |  |  | 1137 |                 $comment->stranswers[$key] .= get_string('max', 'qtype_calculated') .
 | 
        
           |  |  | 1138 |                         $delimiter . $answer->max;
 | 
        
           |  |  | 1139 |             }
 | 
        
           |  |  | 1140 |         }
 | 
        
           |  |  | 1141 |         return fullclone($comment);
 | 
        
           |  |  | 1142 |     }
 | 
        
           |  |  | 1143 |   | 
        
           |  |  | 1144 |     public function tolerance_types() {
 | 
        
           |  |  | 1145 |         return [
 | 
        
           |  |  | 1146 |             '1' => get_string('relative', 'qtype_numerical'),
 | 
        
           |  |  | 1147 |             '2' => get_string('nominal', 'qtype_numerical'),
 | 
        
           |  |  | 1148 |             '3' => get_string('geometric', 'qtype_numerical'),
 | 
        
           |  |  | 1149 |         ];
 | 
        
           |  |  | 1150 |     }
 | 
        
           |  |  | 1151 |   | 
        
           |  |  | 1152 |     public function dataset_options($form, $name, $mandatory = true,
 | 
        
           |  |  | 1153 |             $renameabledatasets = false) {
 | 
        
           |  |  | 1154 |         // Takes datasets from the parent implementation but
 | 
        
           |  |  | 1155 |         // filters options that are currently not accepted by calculated.
 | 
        
           |  |  | 1156 |         // It also determines a default selection.
 | 
        
           |  |  | 1157 |         // Param $renameabledatasets not implemented anywhere.
 | 
        
           |  |  | 1158 |   | 
        
           |  |  | 1159 |         list($options, $selected) = $this->dataset_options_from_database(
 | 
        
           |  |  | 1160 |                 $form, $name, '', 'qtype_calculated');
 | 
        
           |  |  | 1161 |   | 
        
           |  |  | 1162 |         foreach ($options as $key => $whatever) {
 | 
        
           |  |  | 1163 |             if (!preg_match('~^1-~', $key) && $key != '0') {
 | 
        
           |  |  | 1164 |                 unset($options[$key]);
 | 
        
           |  |  | 1165 |             }
 | 
        
           |  |  | 1166 |         }
 | 
        
           |  |  | 1167 |         if (!$selected) {
 | 
        
           |  |  | 1168 |             if ($mandatory) {
 | 
        
           |  |  | 1169 |                 $selected = "1-0-{$name}"; // Default.
 | 
        
           |  |  | 1170 |             } else {
 | 
        
           |  |  | 1171 |                 $selected = '0'; // Default.
 | 
        
           |  |  | 1172 |             }
 | 
        
           |  |  | 1173 |         }
 | 
        
           |  |  | 1174 |         return [$options, $selected];
 | 
        
           |  |  | 1175 |     }
 | 
        
           |  |  | 1176 |   | 
        
           |  |  | 1177 |     public function construct_dataset_menus($form, $mandatorydatasets,
 | 
        
           |  |  | 1178 |             $optionaldatasets) {
 | 
        
           |  |  | 1179 |         global $OUTPUT;
 | 
        
           |  |  | 1180 |         $datasetmenus = [];
 | 
        
           |  |  | 1181 |         foreach ($mandatorydatasets as $datasetname) {
 | 
        
           |  |  | 1182 |             if (!isset($datasetmenus[$datasetname])) {
 | 
        
           |  |  | 1183 |                 list($options, $selected) =
 | 
        
           |  |  | 1184 |                     $this->dataset_options($form, $datasetname);
 | 
        
           |  |  | 1185 |                 unset($options['0']); // Mandatory...
 | 
        
           |  |  | 1186 |                 $datasetmenus[$datasetname] = html_writer::select(
 | 
        
           |  |  | 1187 |                         $options, 'dataset[]', $selected, null);
 | 
        
           |  |  | 1188 |             }
 | 
        
           |  |  | 1189 |         }
 | 
        
           |  |  | 1190 |         foreach ($optionaldatasets as $datasetname) {
 | 
        
           |  |  | 1191 |             if (!isset($datasetmenus[$datasetname])) {
 | 
        
           |  |  | 1192 |                 list($options, $selected) =
 | 
        
           |  |  | 1193 |                     $this->dataset_options($form, $datasetname);
 | 
        
           |  |  | 1194 |                 $datasetmenus[$datasetname] = html_writer::select(
 | 
        
           |  |  | 1195 |                         $options, 'dataset[]', $selected, null);
 | 
        
           |  |  | 1196 |             }
 | 
        
           |  |  | 1197 |         }
 | 
        
           |  |  | 1198 |         return $datasetmenus;
 | 
        
           |  |  | 1199 |     }
 | 
        
           |  |  | 1200 |   | 
        
           |  |  | 1201 |     public function substitute_variables($str, $dataset) {
 | 
        
           |  |  | 1202 |         global $OUTPUT;
 | 
        
           |  |  | 1203 |         // Testing for wrong numerical values.
 | 
        
           |  |  | 1204 |         // All calculations used this function so testing here should be OK.
 | 
        
           |  |  | 1205 |   | 
        
           |  |  | 1206 |         foreach ($dataset as $name => $value) {
 | 
        
           |  |  | 1207 |             $val = $value;
 | 
        
           |  |  | 1208 |             if (! is_numeric($val)) {
 | 
        
           |  |  | 1209 |                 $a = new stdClass();
 | 
        
           |  |  | 1210 |                 $a->name = '{'.$name.'}';
 | 
        
           |  |  | 1211 |                 $a->value = $value;
 | 
        
           |  |  | 1212 |                 echo $OUTPUT->notification(get_string('notvalidnumber', 'qtype_calculated', $a));
 | 
        
           |  |  | 1213 |                 $val = 1.0;
 | 
        
           |  |  | 1214 |             }
 | 
        
           |  |  | 1215 |             if ($val <= 0) { // MDL-36025 Use parentheses for "-0" .
 | 
        
           |  |  | 1216 |                 $str = str_replace('{'.$name.'}', '('.$val.')', $str);
 | 
        
           |  |  | 1217 |             } else {
 | 
        
           |  |  | 1218 |                 $str = str_replace('{'.$name.'}', $val, $str);
 | 
        
           |  |  | 1219 |             }
 | 
        
           |  |  | 1220 |         }
 | 
        
           |  |  | 1221 |         return $str;
 | 
        
           |  |  | 1222 |     }
 | 
        
           |  |  | 1223 |   | 
        
           |  |  | 1224 |     public function evaluate_equations($str, $dataset) {
 | 
        
           |  |  | 1225 |         $formula = $this->substitute_variables($str, $dataset);
 | 
        
           |  |  | 1226 |         if ($error = qtype_calculated_find_formula_errors($formula)) {
 | 
        
           |  |  | 1227 |             return $error;
 | 
        
           |  |  | 1228 |         }
 | 
        
           |  |  | 1229 |         return $str;
 | 
        
           |  |  | 1230 |     }
 | 
        
           |  |  | 1231 |   | 
        
           |  |  | 1232 |     public function substitute_variables_and_eval($str, $dataset) {
 | 
        
           |  |  | 1233 |         $formula = $this->substitute_variables($str, $dataset);
 | 
        
           |  |  | 1234 |         if ($error = qtype_calculated_find_formula_errors($formula)) {
 | 
        
           |  |  | 1235 |             return $error;
 | 
        
           |  |  | 1236 |         }
 | 
        
           |  |  | 1237 |         // Calculate the correct answer.
 | 
        
           |  |  | 1238 |         if (empty($formula)) {
 | 
        
           |  |  | 1239 |             $str = '';
 | 
        
           |  |  | 1240 |         } else if ($formula === '*') {
 | 
        
           |  |  | 1241 |             $str = '*';
 | 
        
           |  |  | 1242 |         } else {
 | 
        
           |  |  | 1243 |             $str = null;
 | 
        
           |  |  | 1244 |             eval('$str = '.$formula.';');
 | 
        
           |  |  | 1245 |         }
 | 
        
           |  |  | 1246 |         return $str;
 | 
        
           |  |  | 1247 |     }
 | 
        
           |  |  | 1248 |   | 
        
           |  |  | 1249 |     public function get_dataset_definitions($questionid, $newdatasets) {
 | 
        
           |  |  | 1250 |         global $DB;
 | 
        
           |  |  | 1251 |         // Get the existing datasets for this question.
 | 
        
           |  |  | 1252 |         $datasetdefs = [];
 | 
        
           |  |  | 1253 |         if (!empty($questionid)) {
 | 
        
           |  |  | 1254 |             global $CFG;
 | 
        
           |  |  | 1255 |             $sql = "SELECT i.*
 | 
        
           |  |  | 1256 |                       FROM {question_datasets} d, {question_dataset_definitions} i
 | 
        
           |  |  | 1257 |                      WHERE d.question = ? AND d.datasetdefinition = i.id
 | 
        
           |  |  | 1258 |                   ORDER BY i.id";
 | 
        
           |  |  | 1259 |             if ($records = $DB->get_records_sql($sql, [$questionid])) {
 | 
        
           |  |  | 1260 |                 foreach ($records as $r) {
 | 
        
           |  |  | 1261 |                     $datasetdefs["{$r->type}-{$r->category}-{$r->name}"] = $r;
 | 
        
           |  |  | 1262 |                 }
 | 
        
           |  |  | 1263 |             }
 | 
        
           |  |  | 1264 |         }
 | 
        
           |  |  | 1265 |   | 
        
           |  |  | 1266 |         foreach ($newdatasets as $dataset) {
 | 
        
           |  |  | 1267 |             if (!$dataset) {
 | 
        
           |  |  | 1268 |                 continue; // The no dataset case...
 | 
        
           |  |  | 1269 |             }
 | 
        
           |  |  | 1270 |   | 
        
           |  |  | 1271 |             if (!isset($datasetdefs[$dataset])) {
 | 
        
           |  |  | 1272 |                 // Make new datasetdef.
 | 
        
           |  |  | 1273 |                 list($type, $category, $name) = explode('-', $dataset, 3);
 | 
        
           |  |  | 1274 |                 $datasetdef = new stdClass();
 | 
        
           |  |  | 1275 |                 $datasetdef->type = $type;
 | 
        
           |  |  | 1276 |                 $datasetdef->name = $name;
 | 
        
           |  |  | 1277 |                 $datasetdef->category  = $category;
 | 
        
           |  |  | 1278 |                 $datasetdef->itemcount = 0;
 | 
        
           |  |  | 1279 |                 $datasetdef->options   = 'uniform:1.0:10.0:1';
 | 
        
           |  |  | 1280 |                 $datasetdefs[$dataset] = clone($datasetdef);
 | 
        
           |  |  | 1281 |             }
 | 
        
           |  |  | 1282 |         }
 | 
        
           |  |  | 1283 |         return $datasetdefs;
 | 
        
           |  |  | 1284 |     }
 | 
        
           |  |  | 1285 |   | 
        
           |  |  | 1286 |     public function save_dataset_definitions($form) {
 | 
        
           |  |  | 1287 |         global $DB;
 | 
        
           |  |  | 1288 |         // Save synchronize.
 | 
        
           |  |  | 1289 |   | 
        
           |  |  | 1290 |         if (empty($form->dataset)) {
 | 
        
           |  |  | 1291 |             $form->dataset = [];
 | 
        
           |  |  | 1292 |         }
 | 
        
           |  |  | 1293 |         // Save datasets.
 | 
        
           |  |  | 1294 |         $datasetdefinitions = $this->get_dataset_definitions($form->id, $form->dataset);
 | 
        
           |  |  | 1295 |         $tmpdatasets = array_flip($form->dataset);
 | 
        
           |  |  | 1296 |         $defids = array_keys($datasetdefinitions);
 | 
        
           |  |  | 1297 |         foreach ($defids as $defid) {
 | 
        
           |  |  | 1298 |             $datasetdef = &$datasetdefinitions[$defid];
 | 
        
           |  |  | 1299 |             if (isset($datasetdef->id)) {
 | 
        
           |  |  | 1300 |                 if (!isset($tmpdatasets[$defid])) {
 | 
        
           |  |  | 1301 |                     // This dataset is not used any more, delete it.
 | 
        
           |  |  | 1302 |                     $DB->delete_records('question_datasets',
 | 
        
           |  |  | 1303 |                             ['question' => $form->id, 'datasetdefinition' => $datasetdef->id]);
 | 
        
           |  |  | 1304 |                     if ($datasetdef->category == 0) {
 | 
        
           |  |  | 1305 |                         // Question local dataset.
 | 
        
           |  |  | 1306 |                         $DB->delete_records('question_dataset_definitions',
 | 
        
           |  |  | 1307 |                                 ['id' => $datasetdef->id]);
 | 
        
           |  |  | 1308 |                         $DB->delete_records('question_dataset_items',
 | 
        
           |  |  | 1309 |                                 ['definition' => $datasetdef->id]);
 | 
        
           |  |  | 1310 |                     }
 | 
        
           |  |  | 1311 |                 }
 | 
        
           |  |  | 1312 |                 // This has already been saved or just got deleted.
 | 
        
           |  |  | 1313 |                 unset($datasetdefinitions[$defid]);
 | 
        
           |  |  | 1314 |                 continue;
 | 
        
           |  |  | 1315 |             }
 | 
        
           |  |  | 1316 |   | 
        
           |  |  | 1317 |             $datasetdef->id = $DB->insert_record('question_dataset_definitions', $datasetdef);
 | 
        
           |  |  | 1318 |   | 
        
           |  |  | 1319 |             if (0 != $datasetdef->category) {
 | 
        
           |  |  | 1320 |                 // We need to look for already existing datasets in the category.
 | 
        
           |  |  | 1321 |                 // First creating the datasetdefinition above
 | 
        
           |  |  | 1322 |                 // then we can manage to automatically take care of some possible realtime concurrence.
 | 
        
           |  |  | 1323 |   | 
        
           |  |  | 1324 |                 if ($olderdatasetdefs = $DB->get_records_select('question_dataset_definitions',
 | 
        
           |  |  | 1325 |                         'type = ? AND name = ? AND category = ? AND id < ?
 | 
        
           |  |  | 1326 |                         ORDER BY id DESC',
 | 
        
           |  |  | 1327 |                         [$datasetdef->type, $datasetdef->name,
 | 
        
           |  |  | 1328 |                                 $datasetdef->category, $datasetdef->id])) {
 | 
        
           |  |  | 1329 |   | 
        
           |  |  | 1330 |                     while ($olderdatasetdef = array_shift($olderdatasetdefs)) {
 | 
        
           |  |  | 1331 |                         $DB->delete_records('question_dataset_definitions',
 | 
        
           |  |  | 1332 |                                 ['id' => $datasetdef->id]);
 | 
        
           |  |  | 1333 |                         $datasetdef = $olderdatasetdef;
 | 
        
           |  |  | 1334 |                     }
 | 
        
           |  |  | 1335 |                 }
 | 
        
           |  |  | 1336 |             }
 | 
        
           |  |  | 1337 |   | 
        
           |  |  | 1338 |             // Create relation to this dataset.
 | 
        
           |  |  | 1339 |             $questiondataset = new stdClass();
 | 
        
           |  |  | 1340 |             $questiondataset->question = $form->id;
 | 
        
           |  |  | 1341 |             $questiondataset->datasetdefinition = $datasetdef->id;
 | 
        
           |  |  | 1342 |             $DB->insert_record('question_datasets', $questiondataset);
 | 
        
           |  |  | 1343 |             unset($datasetdefinitions[$defid]);
 | 
        
           |  |  | 1344 |         }
 | 
        
           |  |  | 1345 |   | 
        
           |  |  | 1346 |         // Remove local obsolete datasets as well as relations
 | 
        
           |  |  | 1347 |         // to datasets in other categories.
 | 
        
           |  |  | 1348 |         if (!empty($datasetdefinitions)) {
 | 
        
           |  |  | 1349 |             foreach ($datasetdefinitions as $def) {
 | 
        
           |  |  | 1350 |                 $DB->delete_records('question_datasets',
 | 
        
           |  |  | 1351 |                         ['question' => $form->id, 'datasetdefinition' => $def->id]);
 | 
        
           |  |  | 1352 |   | 
        
           |  |  | 1353 |                 if ($def->category == 0) { // Question local dataset.
 | 
        
           |  |  | 1354 |                     $DB->delete_records('question_dataset_definitions',
 | 
        
           |  |  | 1355 |                             ['id' => $def->id]);
 | 
        
           |  |  | 1356 |                     $DB->delete_records('question_dataset_items',
 | 
        
           |  |  | 1357 |                             ['definition' => $def->id]);
 | 
        
           |  |  | 1358 |                 }
 | 
        
           |  |  | 1359 |             }
 | 
        
           |  |  | 1360 |         }
 | 
        
           |  |  | 1361 |     }
 | 
        
           |  |  | 1362 |     /** This function create a copy of the datasets (definition and dataitems)
 | 
        
           |  |  | 1363 |      * from the preceding question if they remain in the new question
 | 
        
           |  |  | 1364 |      * otherwise its create the datasets that have been added as in the
 | 
        
           |  |  | 1365 |      * save_dataset_definitions()
 | 
        
           |  |  | 1366 |      */
 | 
        
           |  |  | 1367 |     public function save_as_new_dataset_definitions($form, $initialid) {
 | 
        
           |  |  | 1368 |         global $CFG, $DB;
 | 
        
           |  |  | 1369 |         // Get the datasets from the intial question.
 | 
        
           |  |  | 1370 |         $datasetdefinitions = $this->get_dataset_definitions($initialid, $form->dataset);
 | 
        
           |  |  | 1371 |         // Param $tmpdatasets contains those of the new question.
 | 
        
           |  |  | 1372 |         $tmpdatasets = array_flip($form->dataset);
 | 
        
           |  |  | 1373 |         $defids = array_keys($datasetdefinitions);// New datasets.
 | 
        
           |  |  | 1374 |         foreach ($defids as $defid) {
 | 
        
           |  |  | 1375 |             $datasetdef = &$datasetdefinitions[$defid];
 | 
        
           |  |  | 1376 |             if (isset($datasetdef->id)) {
 | 
        
           |  |  | 1377 |                 // This dataset exist in the initial question.
 | 
        
           |  |  | 1378 |                 if (!isset($tmpdatasets[$defid])) {
 | 
        
           |  |  | 1379 |                     // Do not exist in the new question so ignore.
 | 
        
           |  |  | 1380 |                     unset($datasetdefinitions[$defid]);
 | 
        
           |  |  | 1381 |                     continue;
 | 
        
           |  |  | 1382 |                 }
 | 
        
           |  |  | 1383 |                 // Create a copy but not for category one.
 | 
        
           |  |  | 1384 |                 if (0 == $datasetdef->category) {
 | 
        
           |  |  | 1385 |                     $olddatasetid = $datasetdef->id;
 | 
        
           |  |  | 1386 |                     $olditemcount = $datasetdef->itemcount;
 | 
        
           |  |  | 1387 |                     $datasetdef->itemcount = 0;
 | 
        
           |  |  | 1388 |                     $datasetdef->id = $DB->insert_record('question_dataset_definitions',
 | 
        
           |  |  | 1389 |                             $datasetdef);
 | 
        
           |  |  | 1390 |                     // Copy the dataitems.
 | 
        
           |  |  | 1391 |                     $olditems = $this->get_database_dataset_items($olddatasetid);
 | 
        
           |  |  | 1392 |                     if (count($olditems) > 0) {
 | 
        
           |  |  | 1393 |                         $itemcount = 0;
 | 
        
           |  |  | 1394 |                         foreach ($olditems as $item) {
 | 
        
           |  |  | 1395 |                             $item->definition = $datasetdef->id;
 | 
        
           |  |  | 1396 |                             $DB->insert_record('question_dataset_items', $item);
 | 
        
           |  |  | 1397 |                             $itemcount++;
 | 
        
           |  |  | 1398 |                         }
 | 
        
           |  |  | 1399 |                         // Update item count to olditemcount if
 | 
        
           |  |  | 1400 |                         // at least this number of items has been recover from the database.
 | 
        
           |  |  | 1401 |                         if ($olditemcount <= $itemcount) {
 | 
        
           |  |  | 1402 |                             $datasetdef->itemcount = $olditemcount;
 | 
        
           |  |  | 1403 |                         } else {
 | 
        
           |  |  | 1404 |                             $datasetdef->itemcount = $itemcount;
 | 
        
           |  |  | 1405 |                         }
 | 
        
           |  |  | 1406 |                         $DB->update_record('question_dataset_definitions', $datasetdef);
 | 
        
           |  |  | 1407 |                     } // End of  copy the dataitems.
 | 
        
           |  |  | 1408 |                 }// End of  copy the datasetdef.
 | 
        
           |  |  | 1409 |                 // Create relation to the new question with this
 | 
        
           |  |  | 1410 |                 // copy as new datasetdef from the initial question.
 | 
        
           |  |  | 1411 |                 $questiondataset = new stdClass();
 | 
        
           |  |  | 1412 |                 $questiondataset->question = $form->id;
 | 
        
           |  |  | 1413 |                 $questiondataset->datasetdefinition = $datasetdef->id;
 | 
        
           |  |  | 1414 |                 $DB->insert_record('question_datasets', $questiondataset);
 | 
        
           |  |  | 1415 |                 unset($datasetdefinitions[$defid]);
 | 
        
           |  |  | 1416 |                 continue;
 | 
        
           |  |  | 1417 |             }// End of datasetdefs from the initial question.
 | 
        
           |  |  | 1418 |             // Really new one code similar to save_dataset_definitions().
 | 
        
           |  |  | 1419 |             $datasetdef->id = $DB->insert_record('question_dataset_definitions', $datasetdef);
 | 
        
           |  |  | 1420 |   | 
        
           |  |  | 1421 |             if (0 != $datasetdef->category) {
 | 
        
           |  |  | 1422 |                 // We need to look for already existing
 | 
        
           |  |  | 1423 |                 // datasets in the category.
 | 
        
           |  |  | 1424 |                 // By first creating the datasetdefinition above we
 | 
        
           |  |  | 1425 |                 // can manage to automatically take care of
 | 
        
           |  |  | 1426 |                 // some possible realtime concurrence.
 | 
        
           |  |  | 1427 |                 if ($olderdatasetdefs = $DB->get_records_select('question_dataset_definitions',
 | 
        
           |  |  | 1428 |                         "type = ? AND " . $DB->sql_equal('name', '?') . " AND category = ? AND id < ?
 | 
        
           |  |  | 1429 |                         ORDER BY id DESC",
 | 
        
           |  |  | 1430 |                         [$datasetdef->type, $datasetdef->name,
 | 
        
           |  |  | 1431 |                                 $datasetdef->category, $datasetdef->id])) {
 | 
        
           |  |  | 1432 |   | 
        
           |  |  | 1433 |                     while ($olderdatasetdef = array_shift($olderdatasetdefs)) {
 | 
        
           |  |  | 1434 |                         $DB->delete_records('question_dataset_definitions',
 | 
        
           |  |  | 1435 |                                 ['id' => $datasetdef->id]);
 | 
        
           |  |  | 1436 |                         $datasetdef = $olderdatasetdef;
 | 
        
           |  |  | 1437 |                     }
 | 
        
           |  |  | 1438 |                 }
 | 
        
           |  |  | 1439 |             }
 | 
        
           |  |  | 1440 |   | 
        
           |  |  | 1441 |             // Create relation to this dataset.
 | 
        
           |  |  | 1442 |             $questiondataset = new stdClass();
 | 
        
           |  |  | 1443 |             $questiondataset->question = $form->id;
 | 
        
           |  |  | 1444 |             $questiondataset->datasetdefinition = $datasetdef->id;
 | 
        
           |  |  | 1445 |             $DB->insert_record('question_datasets', $questiondataset);
 | 
        
           |  |  | 1446 |             unset($datasetdefinitions[$defid]);
 | 
        
           |  |  | 1447 |         }
 | 
        
           |  |  | 1448 |   | 
        
           |  |  | 1449 |         // Remove local obsolete datasets as well as relations
 | 
        
           |  |  | 1450 |         // to datasets in other categories.
 | 
        
           |  |  | 1451 |         if (!empty($datasetdefinitions)) {
 | 
        
           |  |  | 1452 |             foreach ($datasetdefinitions as $def) {
 | 
        
           |  |  | 1453 |                 $DB->delete_records('question_datasets',
 | 
        
           |  |  | 1454 |                         ['question' => $form->id, 'datasetdefinition' => $def->id]);
 | 
        
           |  |  | 1455 |   | 
        
           |  |  | 1456 |                 if ($def->category == 0) { // Question local dataset.
 | 
        
           |  |  | 1457 |                     $DB->delete_records('question_dataset_definitions',
 | 
        
           |  |  | 1458 |                             ['id' => $def->id]);
 | 
        
           |  |  | 1459 |                     $DB->delete_records('question_dataset_items',
 | 
        
           |  |  | 1460 |                             ['definition' => $def->id]);
 | 
        
           |  |  | 1461 |                 }
 | 
        
           |  |  | 1462 |             }
 | 
        
           |  |  | 1463 |         }
 | 
        
           |  |  | 1464 |     }
 | 
        
           |  |  | 1465 |   | 
        
           |  |  | 1466 |     // Dataset functionality.
 | 
        
           |  |  | 1467 |     public function pick_question_dataset($question, $datasetitem) {
 | 
        
           |  |  | 1468 |         // Select a dataset in the following format:
 | 
        
           |  |  | 1469 |         // an array indexed by the variable names (d.name) pointing to the value
 | 
        
           |  |  | 1470 |         // to be substituted.
 | 
        
           |  |  | 1471 |         global $CFG, $DB;
 | 
        
           |  |  | 1472 |         if (!$dataitems = $DB->get_records_sql(
 | 
        
           |  |  | 1473 |                 "SELECT i.id, d.name, i.value
 | 
        
           |  |  | 1474 |                    FROM {question_dataset_definitions} d,
 | 
        
           |  |  | 1475 |                         {question_dataset_items} i,
 | 
        
           |  |  | 1476 |                         {question_datasets} q
 | 
        
           |  |  | 1477 |                   WHERE q.question = ?
 | 
        
           |  |  | 1478 |                     AND q.datasetdefinition = d.id
 | 
        
           |  |  | 1479 |                     AND d.id = i.definition
 | 
        
           |  |  | 1480 |                     AND i.itemnumber = ?
 | 
        
           |  |  | 1481 |                ORDER BY i.id DESC ", [$question->id, $datasetitem])) {
 | 
        
           |  |  | 1482 |             $a = new stdClass();
 | 
        
           |  |  | 1483 |             $a->id = $question->id;
 | 
        
           |  |  | 1484 |             $a->item = $datasetitem;
 | 
        
           |  |  | 1485 |             throw new \moodle_exception('cannotgetdsfordependent', 'question', '', $a);
 | 
        
           |  |  | 1486 |         }
 | 
        
           |  |  | 1487 |         $dataset = [];
 | 
        
           |  |  | 1488 |         foreach ($dataitems as $id => $dataitem) {
 | 
        
           |  |  | 1489 |             if (!isset($dataset[$dataitem->name])) {
 | 
        
           |  |  | 1490 |                 $dataset[$dataitem->name] = $dataitem->value;
 | 
        
           |  |  | 1491 |             }
 | 
        
           |  |  | 1492 |         }
 | 
        
           |  |  | 1493 |         return $dataset;
 | 
        
           |  |  | 1494 |     }
 | 
        
           |  |  | 1495 |   | 
        
           |  |  | 1496 |     public function dataset_options_from_database($form, $name, $prefix = '',
 | 
        
           |  |  | 1497 |             $langfile = 'qtype_calculated') {
 | 
        
           |  |  | 1498 |         global $CFG, $DB;
 | 
        
           |  |  | 1499 |         $type = 1; // Only type = 1 (i.e. old 'LITERAL') has ever been used.
 | 
        
           |  |  | 1500 |         // First options - it is not a dataset...
 | 
        
           |  |  | 1501 |         $options['0'] = get_string($prefix.'nodataset', $langfile);
 | 
        
           |  |  | 1502 |         // New question no local.
 | 
        
           |  |  | 1503 |         if (!isset($form->id) || $form->id == 0) {
 | 
        
           |  |  | 1504 |             $key = "{$type}-0-{$name}";
 | 
        
           |  |  | 1505 |             $options[$key] = get_string($prefix."newlocal{$type}", $langfile);
 | 
        
           |  |  | 1506 |             $currentdatasetdef = new stdClass();
 | 
        
           |  |  | 1507 |             $currentdatasetdef->type = '0';
 | 
        
           |  |  | 1508 |         } else {
 | 
        
           |  |  | 1509 |             // Construct question local options.
 | 
        
           |  |  | 1510 |             $sql = "SELECT a.*
 | 
        
           |  |  | 1511 |                 FROM {question_dataset_definitions} a, {question_datasets} b
 | 
        
           |  |  | 1512 |                WHERE a.id = b.datasetdefinition AND a.type = '1' AND b.question = ? AND " . $DB->sql_equal('a.name', '?');
 | 
        
           |  |  | 1513 |             $currentdatasetdef = $DB->get_record_sql($sql, [$form->id, $name]);
 | 
        
           |  |  | 1514 |             if (!$currentdatasetdef) {
 | 
        
           |  |  | 1515 |                 $currentdatasetdef = new stdClass();
 | 
        
           |  |  | 1516 |                 $currentdatasetdef->type = '0';
 | 
        
           |  |  | 1517 |             }
 | 
        
           |  |  | 1518 |             $key = "{$type}-0-{$name}";
 | 
        
           |  |  | 1519 |             if ($currentdatasetdef->type == $type
 | 
        
           |  |  | 1520 |                     && $currentdatasetdef->category == 0) {
 | 
        
           |  |  | 1521 |                 $options[$key] = get_string($prefix."keptlocal{$type}", $langfile);
 | 
        
           |  |  | 1522 |             } else {
 | 
        
           |  |  | 1523 |                 $options[$key] = get_string($prefix."newlocal{$type}", $langfile);
 | 
        
           |  |  | 1524 |             }
 | 
        
           |  |  | 1525 |         }
 | 
        
           |  |  | 1526 |         // Construct question category options.
 | 
        
           |  |  | 1527 |         $categorydatasetdefs = $DB->get_records_sql(
 | 
        
           |  |  | 1528 |             "SELECT b.question, a.*
 | 
        
           |  |  | 1529 |             FROM {question_datasets} b,
 | 
        
           |  |  | 1530 |             {question_dataset_definitions} a
 | 
        
           |  |  | 1531 |             WHERE a.id = b.datasetdefinition
 | 
        
           |  |  | 1532 |             AND a.type = '1'
 | 
        
           |  |  | 1533 |             AND a.category = ?
 | 
        
           |  |  | 1534 |             AND " . $DB->sql_equal('a.name', '?'), [$form->category, $name]);
 | 
        
           |  |  | 1535 |         $type = 1;
 | 
        
           |  |  | 1536 |         $key = "{$type}-{$form->category}-{$name}";
 | 
        
           |  |  | 1537 |         if (!empty($categorydatasetdefs)) {
 | 
        
           |  |  | 1538 |             // There is at least one with the same name.
 | 
        
           |  |  | 1539 |             if (isset($form->id) && isset($categorydatasetdefs[$form->id])) {
 | 
        
           |  |  | 1540 |                 // It is already used by this question.
 | 
        
           |  |  | 1541 |                 $options[$key] = get_string($prefix."keptcategory{$type}", $langfile);
 | 
        
           |  |  | 1542 |             } else {
 | 
        
           |  |  | 1543 |                 $options[$key] = get_string($prefix."existingcategory{$type}", $langfile);
 | 
        
           |  |  | 1544 |             }
 | 
        
           |  |  | 1545 |         } else {
 | 
        
           |  |  | 1546 |             $options[$key] = get_string($prefix."newcategory{$type}", $langfile);
 | 
        
           |  |  | 1547 |         }
 | 
        
           |  |  | 1548 |         // All done!
 | 
        
           |  |  | 1549 |         return [$options, $currentdatasetdef->type
 | 
        
           |  |  | 1550 |             ? "{$currentdatasetdef->type}-{$currentdatasetdef->category}-{$name}"
 | 
        
           |  |  | 1551 |             : ''];
 | 
        
           |  |  | 1552 |     }
 | 
        
           |  |  | 1553 |   | 
        
           |  |  | 1554 |     /**
 | 
        
           |  |  | 1555 |      * Find the names of all datasets mentioned in a piece of question content like the question text.
 | 
        
           |  |  | 1556 |      * @param $text the text to analyse.
 | 
        
           |  |  | 1557 |      * @return array with dataset name for both key and value.
 | 
        
           |  |  | 1558 |      */
 | 
        
           |  |  | 1559 |     public function find_dataset_names($text) {
 | 
        
           |  |  | 1560 |         preg_match_all(self::PLACEHODLER_REGEX, $text, $matches);
 | 
        
           |  |  | 1561 |         return array_combine($matches[1], $matches[1]);
 | 
        
           |  |  | 1562 |     }
 | 
        
           |  |  | 1563 |   | 
        
           |  |  | 1564 |     /**
 | 
        
           |  |  | 1565 |      * Find all the formulas in a bit of text.
 | 
        
           |  |  | 1566 |      *
 | 
        
           |  |  | 1567 |      * For example, called with "What is {a} plus {b}? (Hint, it is not {={a}*{b}}.)" this
 | 
        
           |  |  | 1568 |      * returns ['{a}*{b}'].
 | 
        
           |  |  | 1569 |      *
 | 
        
           |  |  | 1570 |      * @param $text text to analyse.
 | 
        
           |  |  | 1571 |      * @return array where they keys an values are the formulas.
 | 
        
           |  |  | 1572 |      */
 | 
        
           |  |  | 1573 |     public function find_formulas($text) {
 | 
        
           |  |  | 1574 |         preg_match_all(self::FORMULAS_IN_TEXT_REGEX, $text, $matches);
 | 
        
           |  |  | 1575 |         return array_combine($matches[1], $matches[1]);
 | 
        
           |  |  | 1576 |     }
 | 
        
           |  |  | 1577 |   | 
        
           |  |  | 1578 |     /**
 | 
        
           |  |  | 1579 |      * This function retrieve the item count of the available category shareable
 | 
        
           |  |  | 1580 |      * wild cards that is added as a comment displayed when a wild card with
 | 
        
           |  |  | 1581 |      * the same name is displayed in datasetdefinitions_form.php
 | 
        
           |  |  | 1582 |      */
 | 
        
           |  |  | 1583 |     public function get_dataset_definitions_category($form) {
 | 
        
           |  |  | 1584 |         global $CFG, $DB;
 | 
        
           |  |  | 1585 |         $datasetdefs = [];
 | 
        
           |  |  | 1586 |         $lnamemax = 30;
 | 
        
           |  |  | 1587 |         if (!empty($form->category)) {
 | 
        
           |  |  | 1588 |             $sql = "SELECT i.*, d.*
 | 
        
           |  |  | 1589 |                       FROM {question_datasets} d, {question_dataset_definitions} i
 | 
        
           |  |  | 1590 |                      WHERE i.id = d.datasetdefinition AND i.category = ?";
 | 
        
           |  |  | 1591 |             if ($records = $DB->get_records_sql($sql, [$form->category])) {
 | 
        
           |  |  | 1592 |                 foreach ($records as $r) {
 | 
        
           |  |  | 1593 |                     if (!isset ($datasetdefs["{$r->name}"])) {
 | 
        
           |  |  | 1594 |                         $datasetdefs["{$r->name}"] = $r->itemcount;
 | 
        
           |  |  | 1595 |                     }
 | 
        
           |  |  | 1596 |                 }
 | 
        
           |  |  | 1597 |             }
 | 
        
           |  |  | 1598 |         }
 | 
        
           |  |  | 1599 |         return $datasetdefs;
 | 
        
           |  |  | 1600 |     }
 | 
        
           |  |  | 1601 |   | 
        
           |  |  | 1602 |     /**
 | 
        
           |  |  | 1603 |      * This function build a table showing the available category shareable
 | 
        
           |  |  | 1604 |      * wild cards, their name, their definition (Min, Max, Decimal) , the item count
 | 
        
           |  |  | 1605 |      * and the name of the question where they are used.
 | 
        
           |  |  | 1606 |      * This table is intended to be add before the question text to help the user use
 | 
        
           |  |  | 1607 |      * these wild cards
 | 
        
           |  |  | 1608 |      */
 | 
        
           |  |  | 1609 |     public function print_dataset_definitions_category($form) {
 | 
        
           |  |  | 1610 |         global $CFG, $DB;
 | 
        
           |  |  | 1611 |         $datasetdefs = [];
 | 
        
           |  |  | 1612 |         $lnamemax = 22;
 | 
        
           |  |  | 1613 |         $namestr          = get_string('name');
 | 
        
           |  |  | 1614 |         $rangeofvaluestr  = get_string('minmax', 'qtype_calculated');
 | 
        
           |  |  | 1615 |         $questionusingstr = get_string('usedinquestion', 'qtype_calculated');
 | 
        
           |  |  | 1616 |         $itemscountstr    = get_string('itemscount', 'qtype_calculated');
 | 
        
           |  |  | 1617 |         $text = '';
 | 
        
           |  |  | 1618 |         if (!empty($form->category)) {
 | 
        
           |  |  | 1619 |             list($category) = explode(',', $form->category);
 | 
        
           |  |  | 1620 |             $sql = "SELECT i.*, d.*
 | 
        
           |  |  | 1621 |                 FROM {question_datasets} d,
 | 
        
           |  |  | 1622 |         {question_dataset_definitions} i
 | 
        
           |  |  | 1623 |         WHERE i.id = d.datasetdefinition
 | 
        
           |  |  | 1624 |         AND i.category = ?";
 | 
        
           |  |  | 1625 |             if ($records = $DB->get_records_sql($sql, [$category])) {
 | 
        
           |  |  | 1626 |                 foreach ($records as $r) {
 | 
        
           |  |  | 1627 |                     $sql1 = "SELECT q.*
 | 
        
           |  |  | 1628 |                                FROM {question} q
 | 
        
           |  |  | 1629 |                               WHERE q.id = ?";
 | 
        
           |  |  | 1630 |                     if (!isset ($datasetdefs["{$r->type}-{$r->category}-{$r->name}"])) {
 | 
        
           |  |  | 1631 |                         $datasetdefs["{$r->type}-{$r->category}-{$r->name}"] = $r;
 | 
        
           |  |  | 1632 |                     }
 | 
        
           |  |  | 1633 |                     if ($questionb = $DB->get_records_sql($sql1, [$r->question])) {
 | 
        
           |  |  | 1634 |                         if (!isset ($datasetdefs["{$r->type}-{$r->category}-{$r->name}"]->questions[$r->question])) {
 | 
        
           |  |  | 1635 |                             $datasetdefs["{$r->type}-{$r->category}-{$r->name}"]->questions[$r->question] = new stdClass();
 | 
        
           |  |  | 1636 |                         }
 | 
        
           |  |  | 1637 |                         $datasetdefs["{$r->type}-{$r->category}-{$r->name}"]->questions[$r->question]->name =
 | 
        
           |  |  | 1638 |                             $questionb[$r->question]->name;
 | 
        
           |  |  | 1639 |                     }
 | 
        
           |  |  | 1640 |                 }
 | 
        
           |  |  | 1641 |             }
 | 
        
           |  |  | 1642 |         }
 | 
        
           |  |  | 1643 |         if (!empty ($datasetdefs)) {
 | 
        
           |  |  | 1644 |   | 
        
           |  |  | 1645 |             $text = "<table width=\"100%\" border=\"1\"><tr>
 | 
        
           |  |  | 1646 |                     <th style=\"white-space:nowrap;\" class=\"header\"
 | 
        
           |  |  | 1647 |                             scope=\"col\">{$namestr}</th>
 | 
        
           |  |  | 1648 |                     <th style=\"white-space:nowrap;\" class=\"header\"
 | 
        
           |  |  | 1649 |                             scope=\"col\">{$rangeofvaluestr}</th>
 | 
        
           |  |  | 1650 |                     <th style=\"white-space:nowrap;\" class=\"header\"
 | 
        
           |  |  | 1651 |                             scope=\"col\">{$itemscountstr}</th>
 | 
        
           |  |  | 1652 |                     <th style=\"white-space:nowrap;\" class=\"header\"
 | 
        
           |  |  | 1653 |                             scope=\"col\">{$questionusingstr}</th>
 | 
        
           |  |  | 1654 |                     </tr>";
 | 
        
           |  |  | 1655 |             foreach ($datasetdefs as $datasetdef) {
 | 
        
           |  |  | 1656 |                 list($distribution, $min, $max, $dec) = explode(':', $datasetdef->options, 4);
 | 
        
           |  |  | 1657 |                 $text .= "<tr>
 | 
        
           |  |  | 1658 |                         <td valign=\"top\" align=\"center\">{$datasetdef->name}</td>
 | 
        
           |  |  | 1659 |                         <td align=\"center\" valign=\"top\">{$min} <strong>-</strong> $max</td>
 | 
        
           |  |  | 1660 |                         <td align=\"right\" valign=\"top\">{$datasetdef->itemcount}  </td>
 | 
        
           |  |  | 1661 |                         <td align=\"left\">";
 | 
        
           |  |  | 1662 |                 foreach ($datasetdef->questions as $qu) {
 | 
        
           |  |  | 1663 |                     // Limit the name length displayed.
 | 
        
           |  |  | 1664 |                     $questionname = $this->get_short_question_name($qu->name, $lnamemax);
 | 
        
           |  |  | 1665 |                     $text .= "    {$questionname} <br/>";
 | 
        
           |  |  | 1666 |                 }
 | 
        
           |  |  | 1667 |                 $text .= "</td></tr>";
 | 
        
           |  |  | 1668 |             }
 | 
        
           |  |  | 1669 |             $text .= "</table>";
 | 
        
           |  |  | 1670 |         } else {
 | 
        
           |  |  | 1671 |             $text .= get_string('nosharedwildcard', 'qtype_calculated');
 | 
        
           |  |  | 1672 |         }
 | 
        
           |  |  | 1673 |         return $text;
 | 
        
           |  |  | 1674 |     }
 | 
        
           |  |  | 1675 |   | 
        
           |  |  | 1676 |     /**
 | 
        
           |  |  | 1677 |      * This function shortens a question name if it exceeds the character limit.
 | 
        
           |  |  | 1678 |      *
 | 
        
           |  |  | 1679 |      * @param string $stringtoshorten the string to be shortened.
 | 
        
           |  |  | 1680 |      * @param int $characterlimit the character limit.
 | 
        
           |  |  | 1681 |      * @return string
 | 
        
           |  |  | 1682 |      */
 | 
        
           |  |  | 1683 |     public function get_short_question_name($stringtoshorten, $characterlimit) {
 | 
        
           |  |  | 1684 |         if (!empty($stringtoshorten)) {
 | 
        
           |  |  | 1685 |             $returnstring = format_string($stringtoshorten);
 | 
        
           |  |  | 1686 |             if (strlen($returnstring) > $characterlimit) {
 | 
        
           |  |  | 1687 |                 $returnstring = shorten_text($returnstring, $characterlimit, true);
 | 
        
           |  |  | 1688 |             }
 | 
        
           |  |  | 1689 |             return $returnstring;
 | 
        
           |  |  | 1690 |         } else {
 | 
        
           |  |  | 1691 |             return '';
 | 
        
           |  |  | 1692 |         }
 | 
        
           |  |  | 1693 |     }
 | 
        
           |  |  | 1694 |   | 
        
           |  |  | 1695 |     /**
 | 
        
           |  |  | 1696 |      * This function build a table showing the available category shareable
 | 
        
           |  |  | 1697 |      * wild cards, their name, their definition (Min, Max, Decimal) , the item count
 | 
        
           |  |  | 1698 |      * and the name of the question where they are used.
 | 
        
           |  |  | 1699 |      * This table is intended to be add before the question text to help the user use
 | 
        
           |  |  | 1700 |      * these wild cards
 | 
        
           |  |  | 1701 |      */
 | 
        
           |  |  | 1702 |   | 
        
           |  |  | 1703 |     public function print_dataset_definitions_category_shared($question, $datasetdefsq) {
 | 
        
           |  |  | 1704 |         global $CFG, $DB;
 | 
        
           |  |  | 1705 |         $datasetdefs = [];
 | 
        
           |  |  | 1706 |         $lnamemax = 22;
 | 
        
           |  |  | 1707 |         $namestr          = get_string('name', 'quiz');
 | 
        
           |  |  | 1708 |         $rangeofvaluestr  = get_string('minmax', 'qtype_calculated');
 | 
        
           |  |  | 1709 |         $questionusingstr = get_string('usedinquestion', 'qtype_calculated');
 | 
        
           |  |  | 1710 |         $itemscountstr    = get_string('itemscount', 'qtype_calculated');
 | 
        
           |  |  | 1711 |         $text = '';
 | 
        
           |  |  | 1712 |         if (!empty($question->category)) {
 | 
        
           |  |  | 1713 |             list($category) = explode(',', $question->category);
 | 
        
           |  |  | 1714 |             $sql = "SELECT i.*, d.*
 | 
        
           |  |  | 1715 |                       FROM {question_datasets} d, {question_dataset_definitions} i
 | 
        
           |  |  | 1716 |                      WHERE i.id = d.datasetdefinition AND i.category = ?";
 | 
        
           |  |  | 1717 |             if ($records = $DB->get_records_sql($sql, [$category])) {
 | 
        
           |  |  | 1718 |                 foreach ($records as $r) {
 | 
        
           |  |  | 1719 |                     $key = "{$r->type}-{$r->category}-{$r->name}";
 | 
        
           |  |  | 1720 |                     $sql1 = "SELECT q.*
 | 
        
           |  |  | 1721 |                                FROM {question} q
 | 
        
           |  |  | 1722 |                               WHERE q.id = ?";
 | 
        
           |  |  | 1723 |                     if (!isset($datasetdefs[$key])) {
 | 
        
           |  |  | 1724 |                         $datasetdefs[$key] = $r;
 | 
        
           |  |  | 1725 |                     }
 | 
        
           |  |  | 1726 |                     if ($questionb = $DB->get_records_sql($sql1, [$r->question])) {
 | 
        
           |  |  | 1727 |                         $datasetdefs[$key]->questions[$r->question] = new stdClass();
 | 
        
           |  |  | 1728 |                         $datasetdefs[$key]->questions[$r->question]->name =
 | 
        
           |  |  | 1729 |                                 $questionb[$r->question]->name;
 | 
        
           |  |  | 1730 |                         $datasetdefs[$key]->questions[$r->question]->id =
 | 
        
           |  |  | 1731 |                                 $questionb[$r->question]->id;
 | 
        
           |  |  | 1732 |                     }
 | 
        
           |  |  | 1733 |                 }
 | 
        
           |  |  | 1734 |             }
 | 
        
           |  |  | 1735 |         }
 | 
        
           |  |  | 1736 |         if (!empty ($datasetdefs)) {
 | 
        
           |  |  | 1737 |   | 
        
           |  |  | 1738 |             $text  = "<table width=\"100%\" border=\"1\"><tr>
 | 
        
           |  |  | 1739 |                     <th style=\"white-space:nowrap;\" class=\"header\"
 | 
        
           |  |  | 1740 |                             scope=\"col\">{$namestr}</th>";
 | 
        
           |  |  | 1741 |             $text .= "<th style=\"white-space:nowrap;\" class=\"header\"
 | 
        
           |  |  | 1742 |                     scope=\"col\">{$itemscountstr}</th>";
 | 
        
           |  |  | 1743 |             $text .= "<th style=\"white-space:nowrap;\" class=\"header\"
 | 
        
           |  |  | 1744 |                     scope=\"col\">  {$questionusingstr}   </th>";
 | 
        
           |  |  | 1745 |             $text .= "<th style=\"white-space:nowrap;\" class=\"header\"
 | 
        
           |  |  | 1746 |                     scope=\"col\">Quiz</th>";
 | 
        
           |  |  | 1747 |             $text .= "<th style=\"white-space:nowrap;\" class=\"header\"
 | 
        
           |  |  | 1748 |                     scope=\"col\">Attempts</th></tr>";
 | 
        
           |  |  | 1749 |             foreach ($datasetdefs as $datasetdef) {
 | 
        
           |  |  | 1750 |                 list($distribution, $min, $max, $dec) = explode(':', $datasetdef->options, 4);
 | 
        
           |  |  | 1751 |                 $count = count($datasetdef->questions);
 | 
        
           |  |  | 1752 |                 $text .= "<tr>
 | 
        
           |  |  | 1753 |                         <td style=\"white-space:nowrap;\" valign=\"top\"
 | 
        
           |  |  | 1754 |                                 align=\"center\" rowspan=\"{$count}\"> {$datasetdef->name} </td>
 | 
        
           |  |  | 1755 |                         <td align=\"right\" valign=\"top\"
 | 
        
           |  |  | 1756 |                                 rowspan=\"{$count}\">{$datasetdef->itemcount}</td>";
 | 
        
           |  |  | 1757 |                 $line = 0;
 | 
        
           |  |  | 1758 |                 foreach ($datasetdef->questions as $qu) {
 | 
        
           |  |  | 1759 |                     // Limit the name length displayed.
 | 
        
           |  |  | 1760 |                     $questionname = $this->get_short_question_name($qu->name, $lnamemax);
 | 
        
           |  |  | 1761 |                     if ($line) {
 | 
        
           |  |  | 1762 |                         $text .= "<tr>";
 | 
        
           |  |  | 1763 |                     }
 | 
        
           |  |  | 1764 |                     $line++;
 | 
        
           |  |  | 1765 |                     $text .= "<td align=\"left\" style=\"white-space:nowrap;\">{$questionname}</td>";
 | 
        
           |  |  | 1766 |                     // TODO MDL-43779 should not have quiz-specific code here.
 | 
        
           |  |  | 1767 |                     $sql = 'SELECT COUNT(*) FROM (' . qbank_usage\helper::get_question_bank_usage_sql() . ') questioncount';
 | 
        
           |  |  | 1768 |                     $nbofquiz = $DB->count_records_sql($sql, [$qu->id, 'mod_quiz', 'slot']);
 | 
        
           |  |  | 1769 |                     $sql = 'SELECT COUNT(*) FROM (' . qbank_usage\helper::get_question_attempt_usage_sql() . ') attemptcount';
 | 
        
           |  |  | 1770 |                     $nbofattempts = $DB->count_records_sql($sql, [$qu->id]);
 | 
        
           |  |  | 1771 |                     if ($nbofquiz > 0) {
 | 
        
           |  |  | 1772 |                         $text .= "<td align=\"center\">{$nbofquiz}</td>";
 | 
        
           |  |  | 1773 |                         $text .= "<td align=\"center\">{$nbofattempts}";
 | 
        
           |  |  | 1774 |                     } else {
 | 
        
           |  |  | 1775 |                         $text .= "<td align=\"center\">0</td>";
 | 
        
           |  |  | 1776 |                         $text .= "<td align=\"left\"><br/>";
 | 
        
           |  |  | 1777 |                     }
 | 
        
           |  |  | 1778 |   | 
        
           |  |  | 1779 |                     $text .= "</td></tr>";
 | 
        
           |  |  | 1780 |                 }
 | 
        
           |  |  | 1781 |             }
 | 
        
           |  |  | 1782 |             $text .= "</table>";
 | 
        
           |  |  | 1783 |         } else {
 | 
        
           |  |  | 1784 |             $text .= get_string('nosharedwildcard', 'qtype_calculated');
 | 
        
           |  |  | 1785 |         }
 | 
        
           |  |  | 1786 |         return $text;
 | 
        
           |  |  | 1787 |     }
 | 
        
           |  |  | 1788 |   | 
        
           |  |  | 1789 |     public function get_virtual_qtype() {
 | 
        
           |  |  | 1790 |         return question_bank::get_qtype('numerical');
 | 
        
           |  |  | 1791 |     }
 | 
        
           |  |  | 1792 |   | 
        
           |  |  | 1793 |     public function get_possible_responses($questiondata) {
 | 
        
           |  |  | 1794 |         $responses = [];
 | 
        
           |  |  | 1795 |   | 
        
           |  |  | 1796 |         $virtualqtype = $this->get_virtual_qtype();
 | 
        
           |  |  | 1797 |         $unit = $virtualqtype->get_default_numerical_unit($questiondata);
 | 
        
           |  |  | 1798 |   | 
        
           |  |  | 1799 |         $tolerancetypes = $this->tolerance_types();
 | 
        
           |  |  | 1800 |   | 
        
           |  |  | 1801 |         $starfound = false;
 | 
        
           |  |  | 1802 |         foreach ($questiondata->options->answers as $aid => $answer) {
 | 
        
           |  |  | 1803 |             $responseclass = $answer->answer;
 | 
        
           |  |  | 1804 |   | 
        
           |  |  | 1805 |             if ($responseclass === '*') {
 | 
        
           |  |  | 1806 |                 $starfound = true;
 | 
        
           |  |  | 1807 |             } else {
 | 
        
           |  |  | 1808 |                 $a = new stdClass();
 | 
        
           |  |  | 1809 |                 $a->answer = $virtualqtype->add_unit($questiondata, $responseclass, $unit);
 | 
        
           |  |  | 1810 |                 $a->tolerance = $answer->tolerance;
 | 
        
           |  |  | 1811 |                 $a->tolerancetype = $tolerancetypes[$answer->tolerancetype];
 | 
        
           |  |  | 1812 |   | 
        
           |  |  | 1813 |                 $responseclass = get_string('answerwithtolerance', 'qtype_calculated', $a);
 | 
        
           |  |  | 1814 |             }
 | 
        
           |  |  | 1815 |   | 
        
           |  |  | 1816 |             $responses[$aid] = new question_possible_response($responseclass,
 | 
        
           |  |  | 1817 |                     $answer->fraction);
 | 
        
           |  |  | 1818 |         }
 | 
        
           |  |  | 1819 |   | 
        
           |  |  | 1820 |         if (!$starfound) {
 | 
        
           |  |  | 1821 |             $responses[0] = new question_possible_response(
 | 
        
           |  |  | 1822 |             get_string('didnotmatchanyanswer', 'question'), 0);
 | 
        
           |  |  | 1823 |         }
 | 
        
           |  |  | 1824 |   | 
        
           |  |  | 1825 |         $responses[null] = question_possible_response::no_response();
 | 
        
           |  |  | 1826 |   | 
        
           |  |  | 1827 |         return [$questiondata->id => $responses];
 | 
        
           |  |  | 1828 |     }
 | 
        
           |  |  | 1829 |   | 
        
           |  |  | 1830 |     public function move_files($questionid, $oldcontextid, $newcontextid) {
 | 
        
           |  |  | 1831 |         $fs = get_file_storage();
 | 
        
           |  |  | 1832 |   | 
        
           |  |  | 1833 |         parent::move_files($questionid, $oldcontextid, $newcontextid);
 | 
        
           |  |  | 1834 |         $this->move_files_in_answers($questionid, $oldcontextid, $newcontextid);
 | 
        
           |  |  | 1835 |         $this->move_files_in_hints($questionid, $oldcontextid, $newcontextid);
 | 
        
           |  |  | 1836 |     }
 | 
        
           |  |  | 1837 |   | 
        
           |  |  | 1838 |     protected function delete_files($questionid, $contextid) {
 | 
        
           |  |  | 1839 |         $fs = get_file_storage();
 | 
        
           |  |  | 1840 |   | 
        
           |  |  | 1841 |         parent::delete_files($questionid, $contextid);
 | 
        
           |  |  | 1842 |         $this->delete_files_in_answers($questionid, $contextid);
 | 
        
           |  |  | 1843 |         $this->delete_files_in_hints($questionid, $contextid);
 | 
        
           |  |  | 1844 |     }
 | 
        
           |  |  | 1845 | }
 | 
        
           |  |  | 1846 |   | 
        
           |  |  | 1847 |   | 
        
           |  |  | 1848 | function qtype_calculated_calculate_answer($formula, $individualdata,
 | 
        
           |  |  | 1849 |     $tolerance, $tolerancetype, $answerlength, $answerformat = '1', $unit = '') {
 | 
        
           |  |  | 1850 |     // The return value has these properties: .
 | 
        
           |  |  | 1851 |     // ->answer    the correct answer
 | 
        
           |  |  | 1852 |     // ->min       the lower bound for an acceptable response
 | 
        
           |  |  | 1853 |     // ->max       the upper bound for an accetpable response.
 | 
        
           |  |  | 1854 |     $calculated = new stdClass();
 | 
        
           |  |  | 1855 |     // Exchange formula variables with the correct values...
 | 
        
           |  |  | 1856 |     $answer = question_bank::get_qtype('calculated')->substitute_variables_and_eval(
 | 
        
           |  |  | 1857 |             $formula, $individualdata);
 | 
        
           |  |  | 1858 |     if (!is_numeric($answer)) {
 | 
        
           |  |  | 1859 |         // Something went wrong, so just return NaN.
 | 
        
           |  |  | 1860 |         $calculated->answer = NAN;
 | 
        
           |  |  | 1861 |         return $calculated;
 | 
        
           |  |  | 1862 |     } else if (is_nan($answer) || is_infinite($answer)) {
 | 
        
           |  |  | 1863 |         $calculated->answer = $answer;
 | 
        
           |  |  | 1864 |         return $calculated;
 | 
        
           |  |  | 1865 |     }
 | 
        
           |  |  | 1866 |     if ('1' == $answerformat) { // Answer is to have $answerlength decimals.
 | 
        
           |  |  | 1867 |         // Decimal places.
 | 
        
           |  |  | 1868 |         $calculated->answer = sprintf('%.' . $answerlength . 'F', $answer);
 | 
        
           |  |  | 1869 |   | 
        
           |  |  | 1870 |     } else if ($answer) { // Significant figures does only apply if the result is non-zero.
 | 
        
           |  |  | 1871 |   | 
        
           |  |  | 1872 |         // Convert to positive answer...
 | 
        
           |  |  | 1873 |         if ($answer < 0) {
 | 
        
           |  |  | 1874 |             $answer = -$answer;
 | 
        
           |  |  | 1875 |             $sign = '-';
 | 
        
           |  |  | 1876 |         } else {
 | 
        
           |  |  | 1877 |             $sign = '';
 | 
        
           |  |  | 1878 |         }
 | 
        
           |  |  | 1879 |   | 
        
           |  |  | 1880 |         // Determine the format 0.[1-9][0-9]* for the answer...
 | 
        
           |  |  | 1881 |         $p10 = 0;
 | 
        
           |  |  | 1882 |         while ($answer < 1) {
 | 
        
           |  |  | 1883 |             --$p10;
 | 
        
           |  |  | 1884 |             $answer *= 10;
 | 
        
           |  |  | 1885 |         }
 | 
        
           |  |  | 1886 |         while ($answer >= 1) {
 | 
        
           |  |  | 1887 |             ++$p10;
 | 
        
           |  |  | 1888 |             $answer /= 10;
 | 
        
           |  |  | 1889 |         }
 | 
        
           |  |  | 1890 |         // ... and have the answer rounded of to the correct length.
 | 
        
           |  |  | 1891 |         $answer = round($answer, $answerlength);
 | 
        
           |  |  | 1892 |   | 
        
           |  |  | 1893 |         // If we rounded up to 1.0, place the answer back into 0.[1-9][0-9]* format.
 | 
        
           |  |  | 1894 |         if ($answer >= 1) {
 | 
        
           |  |  | 1895 |             ++$p10;
 | 
        
           |  |  | 1896 |             $answer /= 10;
 | 
        
           |  |  | 1897 |         }
 | 
        
           |  |  | 1898 |   | 
        
           |  |  | 1899 |         // Have the answer written on a suitable format:
 | 
        
           |  |  | 1900 |         // either scientific or plain numeric.
 | 
        
           |  |  | 1901 |         if (-2 > $p10 || 4 < $p10) {
 | 
        
           |  |  | 1902 |             // Use scientific format.
 | 
        
           |  |  | 1903 |             $exponent = 'e'.--$p10;
 | 
        
           |  |  | 1904 |             $answer *= 10;
 | 
        
           |  |  | 1905 |             if (1 == $answerlength) {
 | 
        
           |  |  | 1906 |                 $calculated->answer = $sign.$answer.$exponent;
 | 
        
           |  |  | 1907 |             } else {
 | 
        
           |  |  | 1908 |                 // Attach additional zeros at the end of $answer.
 | 
        
           |  |  | 1909 |                 $answer .= (1 == strlen($answer) ? '.' : '')
 | 
        
           |  |  | 1910 |                     . '00000000000000000000000000000000000000000x';
 | 
        
           |  |  | 1911 |                 $calculated->answer = $sign
 | 
        
           |  |  | 1912 |                     .substr($answer, 0, $answerlength + 1).$exponent;
 | 
        
           |  |  | 1913 |             }
 | 
        
           |  |  | 1914 |         } else {
 | 
        
           |  |  | 1915 |             // Stick to plain numeric format.
 | 
        
           |  |  | 1916 |             $answer *= "1e{$p10}";
 | 
        
           |  |  | 1917 |             if (0.1 <= $answer / "1e{$answerlength}") {
 | 
        
           |  |  | 1918 |                 $calculated->answer = $sign.$answer;
 | 
        
           |  |  | 1919 |             } else {
 | 
        
           |  |  | 1920 |                 // Could be an idea to add some zeros here.
 | 
        
           |  |  | 1921 |                 $answer .= (preg_match('~^[0-9]*$~', $answer) ? '.' : '')
 | 
        
           |  |  | 1922 |                     . '00000000000000000000000000000000000000000x';
 | 
        
           |  |  | 1923 |                 $oklen = $answerlength + ($p10 < 1 ? 2 - $p10 : 1);
 | 
        
           |  |  | 1924 |                 $calculated->answer = $sign.substr($answer, 0, $oklen);
 | 
        
           |  |  | 1925 |             }
 | 
        
           |  |  | 1926 |         }
 | 
        
           |  |  | 1927 |   | 
        
           |  |  | 1928 |     } else {
 | 
        
           |  |  | 1929 |         $calculated->answer = 0.0;
 | 
        
           |  |  | 1930 |     }
 | 
        
           |  |  | 1931 |     if ($unit != '') {
 | 
        
           |  |  | 1932 |             $calculated->answer = $calculated->answer . ' ' . $unit;
 | 
        
           |  |  | 1933 |     }
 | 
        
           |  |  | 1934 |   | 
        
           |  |  | 1935 |     // Return the result.
 | 
        
           |  |  | 1936 |     return $calculated;
 | 
        
           |  |  | 1937 | }
 | 
        
           |  |  | 1938 |   | 
        
           |  |  | 1939 |   | 
        
           |  |  | 1940 | /**
 | 
        
           |  |  | 1941 |  * Validate a forumula.
 | 
        
           |  |  | 1942 |  * @param string $formula the formula to validate.
 | 
        
           |  |  | 1943 |  * @return string|boolean false if there are no problems. Otherwise a string error message.
 | 
        
           |  |  | 1944 |  */
 | 
        
           |  |  | 1945 | function qtype_calculated_find_formula_errors($formula) {
 | 
        
           |  |  | 1946 |     foreach (['//', '/*', '#', '<?', '?>'] as $commentstart) {
 | 
        
           |  |  | 1947 |         if (strpos($formula, $commentstart) !== false) {
 | 
        
           |  |  | 1948 |             return get_string('illegalformulasyntax', 'qtype_calculated', $commentstart);
 | 
        
           |  |  | 1949 |         }
 | 
        
           |  |  | 1950 |     }
 | 
        
           |  |  | 1951 |   | 
        
           |  |  | 1952 |     // Validates the formula submitted from the question edit page.
 | 
        
           |  |  | 1953 |     // Returns false if everything is alright
 | 
        
           |  |  | 1954 |     // otherwise it constructs an error message.
 | 
        
           | 11 | efrain | 1955 |     // Strip away dataset names. Use 1.0 to remove valid names, so illegal names can be identified later.
 | 
        
           | 1 | efrain | 1956 |     $formula = preg_replace(qtype_calculated::PLACEHODLER_REGEX, '1.0', $formula);
 | 
        
           |  |  | 1957 |   | 
        
           |  |  | 1958 |     // Strip away empty space and lowercase it.
 | 
        
           |  |  | 1959 |     $formula = strtolower(str_replace(' ', '', $formula));
 | 
        
           |  |  | 1960 |   | 
        
           | 11 | efrain | 1961 |     // Only mathematical operators are supported. Bitwise operators are not safe.
 | 
        
           |  |  | 1962 |     // Note: In this context, ^ is a bitwise operator (exponents are represented by **).
 | 
        
           |  |  | 1963 |     $safeoperatorchar = '-+/*%>:\~<?=!';
 | 
        
           | 1 | efrain | 1964 |     $operatorornumber = "[{$safeoperatorchar}.0-9eE]";
 | 
        
           |  |  | 1965 |   | 
        
           | 11 | efrain | 1966 |     // Validate mathematical functions in formula.
 | 
        
           | 1 | efrain | 1967 |     while (preg_match("~(^|[{$safeoperatorchar},(])([a-z0-9_]*)" .
 | 
        
           |  |  | 1968 |             "\\(({$operatorornumber}+(,{$operatorornumber}+((,{$operatorornumber}+)+)?)?)?\\)~",
 | 
        
           |  |  | 1969 |             $formula, $regs)) {
 | 
        
           |  |  | 1970 |         switch ($regs[2]) {
 | 
        
           |  |  | 1971 |             // Simple parenthesis.
 | 
        
           |  |  | 1972 |             case '':
 | 
        
           |  |  | 1973 |                 if ((isset($regs[4]) && $regs[4]) || strlen($regs[3]) == 0) {
 | 
        
           |  |  | 1974 |                     return get_string('illegalformulasyntax', 'qtype_calculated', $regs[0]);
 | 
        
           |  |  | 1975 |                 }
 | 
        
           |  |  | 1976 |                 break;
 | 
        
           |  |  | 1977 |   | 
        
           |  |  | 1978 |                 // Zero argument functions.
 | 
        
           |  |  | 1979 |             case 'pi':
 | 
        
           |  |  | 1980 |                 if (array_key_exists(3, $regs)) {
 | 
        
           |  |  | 1981 |                     return get_string('functiontakesnoargs', 'qtype_calculated', $regs[2]);
 | 
        
           |  |  | 1982 |                 }
 | 
        
           |  |  | 1983 |                 break;
 | 
        
           |  |  | 1984 |   | 
        
           |  |  | 1985 |             // Single argument functions (the most common case).
 | 
        
           |  |  | 1986 |             case 'abs': case 'acos': case 'acosh': case 'asin': case 'asinh':
 | 
        
           |  |  | 1987 |             case 'atan': case 'atanh': case 'bindec': case 'ceil': case 'cos':
 | 
        
           |  |  | 1988 |             case 'cosh': case 'decbin': case 'decoct': case 'deg2rad':
 | 
        
           |  |  | 1989 |             case 'exp': case 'expm1': case 'floor': case 'is_finite':
 | 
        
           |  |  | 1990 |             case 'is_infinite': case 'is_nan': case 'log10': case 'log1p':
 | 
        
           |  |  | 1991 |             case 'octdec': case 'rad2deg': case 'sin': case 'sinh': case 'sqrt':
 | 
        
           |  |  | 1992 |             case 'tan': case 'tanh':
 | 
        
           |  |  | 1993 |                 if (!empty($regs[4]) || empty($regs[3])) {
 | 
        
           |  |  | 1994 |                     return get_string('functiontakesonearg', 'qtype_calculated', $regs[2]);
 | 
        
           |  |  | 1995 |                 }
 | 
        
           |  |  | 1996 |                 break;
 | 
        
           |  |  | 1997 |   | 
        
           |  |  | 1998 |                 // Functions that take one or two arguments.
 | 
        
           |  |  | 1999 |             case 'log': case 'round':
 | 
        
           |  |  | 2000 |                     if (!empty($regs[5]) || empty($regs[3])) {
 | 
        
           |  |  | 2001 |                         return get_string('functiontakesoneortwoargs', 'qtype_calculated', $regs[2]);
 | 
        
           |  |  | 2002 |                     }
 | 
        
           |  |  | 2003 |                 break;
 | 
        
           |  |  | 2004 |   | 
        
           |  |  | 2005 |                 // Functions that must have two arguments.
 | 
        
           |  |  | 2006 |             case 'atan2': case 'fmod': case 'pow':
 | 
        
           |  |  | 2007 |                         if (!empty($regs[5]) || empty($regs[4])) {
 | 
        
           |  |  | 2008 |                             return get_string('functiontakestwoargs', 'qtype_calculated', $regs[2]);
 | 
        
           |  |  | 2009 |                         }
 | 
        
           |  |  | 2010 |                 break;
 | 
        
           |  |  | 2011 |   | 
        
           |  |  | 2012 |                 // Functions that take two or more arguments.
 | 
        
           |  |  | 2013 |             case 'min': case 'max':
 | 
        
           |  |  | 2014 |                     if (empty($regs[4])) {
 | 
        
           |  |  | 2015 |                         return get_string('functiontakesatleasttwo', 'qtype_calculated', $regs[2]);
 | 
        
           |  |  | 2016 |                     }
 | 
        
           |  |  | 2017 |                 break;
 | 
        
           |  |  | 2018 |   | 
        
           |  |  | 2019 |             default:
 | 
        
           |  |  | 2020 |                 return get_string('unsupportedformulafunction', 'qtype_calculated', $regs[2]);
 | 
        
           |  |  | 2021 |         }
 | 
        
           |  |  | 2022 |   | 
        
           |  |  | 2023 |         // Exchange the function call with '1.0' and then check for
 | 
        
           |  |  | 2024 |         // another function call...
 | 
        
           |  |  | 2025 |         if ($regs[1]) {
 | 
        
           |  |  | 2026 |             // The function call is proceeded by an operator.
 | 
        
           |  |  | 2027 |             $formula = str_replace($regs[0], $regs[1] . '1.0', $formula);
 | 
        
           |  |  | 2028 |         } else {
 | 
        
           |  |  | 2029 |             // The function call starts the formula.
 | 
        
           |  |  | 2030 |             $formula = preg_replace('~^' . preg_quote($regs[2], '~') . '\([^)]*\)~', '1.0', $formula);
 | 
        
           |  |  | 2031 |         }
 | 
        
           |  |  | 2032 |     }
 | 
        
           |  |  | 2033 |   | 
        
           |  |  | 2034 |     if (preg_match("~[^{$safeoperatorchar}.0-9eE]+~", $formula, $regs)) {
 | 
        
           |  |  | 2035 |         return get_string('illegalformulasyntax', 'qtype_calculated', $regs[0]);
 | 
        
           |  |  | 2036 |     } else {
 | 
        
           |  |  | 2037 |         // Formula just might be valid.
 | 
        
           |  |  | 2038 |         return false;
 | 
        
           |  |  | 2039 |     }
 | 
        
           |  |  | 2040 | }
 | 
        
           |  |  | 2041 |   | 
        
           |  |  | 2042 | /**
 | 
        
           |  |  | 2043 |  * Validate all the forumulas in a bit of text.
 | 
        
           |  |  | 2044 |  * @param string $text the text in which to validate the formulas.
 | 
        
           |  |  | 2045 |  * @return string|boolean false if there are no problems. Otherwise a string error message.
 | 
        
           |  |  | 2046 |  */
 | 
        
           |  |  | 2047 | function qtype_calculated_find_formula_errors_in_text($text) {
 | 
        
           |  |  | 2048 |     $formulas = question_bank::get_qtype('calculated')->find_formulas($text);
 | 
        
           |  |  | 2049 |   | 
        
           |  |  | 2050 |     $errors = [];
 | 
        
           |  |  | 2051 |     foreach ($formulas as $match) {
 | 
        
           |  |  | 2052 |         $error = qtype_calculated_find_formula_errors($match);
 | 
        
           |  |  | 2053 |         if ($error) {
 | 
        
           |  |  | 2054 |             $errors[] = $error;
 | 
        
           |  |  | 2055 |         }
 | 
        
           |  |  | 2056 |     }
 | 
        
           |  |  | 2057 |   | 
        
           |  |  | 2058 |     if ($errors) {
 | 
        
           |  |  | 2059 |         return implode(' ', $errors);
 | 
        
           |  |  | 2060 |     }
 | 
        
           |  |  | 2061 |   | 
        
           |  |  | 2062 |     return false;
 | 
        
           |  |  | 2063 | }
 |