Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 401 and 402] [Versions 401 and 403]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * Question type class for the calculated multiple-choice question type.
  19   *
  20   * @package    qtype
  21   * @subpackage calculatedmulti
  22   * @copyright  2009 Pierre Pichet
  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/multichoice/questiontype.php');
  30  require_once($CFG->dirroot . '/question/type/calculated/questiontype.php');
  31  
  32  
  33  /**
  34   * The calculated multiple-choice question type.
  35   *
  36   * @copyright  2009 Pierre Pichet
  37   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  38   */
  39  class qtype_calculatedmulti extends qtype_calculated {
  40  
  41      public function save_question_options($question) {
  42          global $CFG, $DB;
  43          $context = $question->context;
  44  
  45          // Make it impossible to save bad formulas anywhere.
  46          $this->validate_question_data($question);
  47  
  48          // Calculated options.
  49          $update = true;
  50          $options = $DB->get_record('question_calculated_options',
  51                  array('question' => $question->id));
  52          if (!$options) {
  53              $options = new stdClass();
  54              $options->question = $question->id;
  55              $options->correctfeedback = '';
  56              $options->partiallycorrectfeedback = '';
  57              $options->incorrectfeedback = '';
  58              $options->id = $DB->insert_record('question_calculated_options', $options);
  59          }
  60          $options->synchronize = $question->synchronize;
  61          $options->single = $question->single;
  62          $options->answernumbering = $question->answernumbering;
  63          $options->shuffleanswers = $question->shuffleanswers;
  64          $options = $this->save_combined_feedback_helper($options, $question, $context, true);
  65          $DB->update_record('question_calculated_options', $options);
  66  
  67          // Get old versions of the objects.
  68          if (!$oldanswers = $DB->get_records('question_answers',
  69                  array('question' => $question->id), 'id ASC')) {
  70              $oldanswers = array();
  71          }
  72          if (!$oldoptions = $DB->get_records('question_calculated',
  73                  array('question' => $question->id), 'answer ASC')) {
  74              $oldoptions = array();
  75          }
  76  
  77          // Insert all the new answers.
  78          foreach ($question->answer as $key => $answerdata) {
  79              if (is_array($answerdata)) {
  80                  $answerdata = $answerdata['text'];
  81              }
  82              if (trim($answerdata) == '') {
  83                  continue;
  84              }
  85  
  86              // Update an existing answer if possible.
  87              $answer = array_shift($oldanswers);
  88              if (!$answer) {
  89                  $answer = new stdClass();
  90                  $answer->question = $question->id;
  91                  $answer->answer   = '';
  92                  $answer->feedback = '';
  93                  $answer->id       = $DB->insert_record('question_answers', $answer);
  94              }
  95  
  96              if (is_array($answerdata)) {
  97                  // Doing an import.
  98                  $answer->answer = $this->import_or_save_files($answerdata,
  99                          $context, 'question', 'answer', $answer->id);
 100                  $answer->answerformat = $answerdata['format'];
 101              } else {
 102                  // Saving the form.
 103                  $answer->answer = $answerdata;
 104                  $answer->answerformat = FORMAT_HTML;
 105              }
 106              $answer->fraction = $question->fraction[$key];
 107              $answer->feedback = $this->import_or_save_files($question->feedback[$key],
 108                      $context, 'question', 'answerfeedback', $answer->id);
 109              $answer->feedbackformat = $question->feedback[$key]['format'];
 110  
 111              $DB->update_record("question_answers", $answer);
 112  
 113              // Set up the options object.
 114              if (!$options = array_shift($oldoptions)) {
 115                  $options = new stdClass();
 116              }
 117              $options->question            = $question->id;
 118              $options->answer              = $answer->id;
 119              $options->tolerance           = trim($question->tolerance[$key]);
 120              $options->tolerancetype       = trim($question->tolerancetype[$key]);
 121              $options->correctanswerlength = trim($question->correctanswerlength[$key]);
 122              $options->correctanswerformat = trim($question->correctanswerformat[$key]);
 123  
 124              // Save options.
 125              if (isset($options->id)) {
 126                  // Reusing existing record.
 127                  $DB->update_record('question_calculated', $options);
 128              } else {
 129                  // New options.
 130                  $DB->insert_record('question_calculated', $options);
 131              }
 132          }
 133  
 134          // Delete old answer records.
 135          if (!empty($oldanswers)) {
 136              foreach ($oldanswers as $oa) {
 137                  $DB->delete_records('question_answers', array('id' => $oa->id));
 138              }
 139          }
 140          if (!empty($oldoptions)) {
 141              foreach ($oldoptions as $oo) {
 142                  $DB->delete_records('question_calculated', array('id' => $oo->id));
 143              }
 144          }
 145  
 146          $this->save_hints($question, true);
 147  
 148          if (isset($question->import_process) && $question->import_process) {
 149              $this->import_datasets($question);
 150          }
 151          // Report any problems.
 152          if (!empty($result->notice)) {
 153              return $result;
 154          }
 155  
 156          return true;
 157      }
 158  
 159      protected function validate_answer($answer) {
 160          $error = qtype_calculated_find_formula_errors_in_text($answer);
 161          if ($error) {
 162              throw new coding_exception($error);
 163          }
 164      }
 165  
 166      protected function validate_question_data($question) {
 167          parent::validate_question_data($question);
 168          $this->validate_text($question->correctfeedback['text']);
 169          $this->validate_text($question->partiallycorrectfeedback['text']);
 170          $this->validate_text($question->incorrectfeedback['text']);
 171      }
 172  
 173      protected function make_question_instance($questiondata) {
 174          question_bank::load_question_definition_classes($this->name());
 175          if ($questiondata->options->single) {
 176              $class = 'qtype_calculatedmulti_single_question';
 177          } else {
 178              $class = 'qtype_calculatedmulti_multi_question';
 179          }
 180          return new $class();
 181      }
 182  
 183      protected function initialise_question_instance(question_definition $question, $questiondata) {
 184          question_type::initialise_question_instance($question, $questiondata);
 185  
 186          $question->shuffleanswers = $questiondata->options->shuffleanswers;
 187          $question->answernumbering = $questiondata->options->answernumbering;
 188          if (!empty($questiondata->options->layout)) {
 189              $question->layout = $questiondata->options->layout;
 190          } else {
 191              $question->layout = qtype_multichoice_single_question::LAYOUT_VERTICAL;
 192          }
 193  
 194          $question->synchronised = $questiondata->options->synchronize;
 195  
 196          $this->initialise_combined_feedback($question, $questiondata, true);
 197          $this->initialise_question_answers($question, $questiondata);
 198  
 199          foreach ($questiondata->options->answers as $a) {
 200              $question->answers[$a->id]->correctanswerlength = $a->correctanswerlength;
 201              $question->answers[$a->id]->correctanswerformat = $a->correctanswerformat;
 202          }
 203  
 204          $question->datasetloader = new qtype_calculated_dataset_loader($questiondata->id);
 205      }
 206  
 207      /**
 208       * Public override method, created so that make_answer will be available
 209       * for use by classes which extend qtype_multichoice_base.
 210       *
 211       * @param stdClass $answer  Moodle DB question_answers object.
 212       * @return question_answer
 213       */
 214      public function make_answer($answer) {
 215          return parent::make_answer($answer);
 216      }
 217  
 218      protected function make_hint($hint) {
 219          return question_hint_with_parts::load_from_record($hint);
 220      }
 221  
 222      public function comment_header($question) {
 223          $strheader = '';
 224          $delimiter = '';
 225  
 226          $answers = $question->options->answers;
 227  
 228          foreach ($answers as $key => $answer) {
 229              $ans = shorten_text($answer->answer, 17, true);
 230              $strheader .= $delimiter.$ans;
 231              $delimiter = '<br/><br/>';
 232          }
 233          return $strheader;
 234      }
 235  
 236      public function comment_on_datasetitems($qtypeobj, $questionid, $questiontext,
 237              $answers, $data, $number) {
 238  
 239          $comment = new stdClass();
 240          $comment->stranswers = array();
 241          $comment->outsidelimit = false;
 242          $comment->answers = array();
 243  
 244          $answers = fullclone($answers);
 245          foreach ($answers as $key => $answer) {
 246              // Evaluate the equations i.e {=5+4).
 247              $anssubstituted = $this->substitute_variables($answer->answer, $data);
 248              $formulas = $this->find_formulas($anssubstituted);
 249              $replaces = [];
 250              foreach ($formulas as $formula) {
 251                  if ($formulaerrors = qtype_calculated_find_formula_errors($formula)) {
 252                      $str = $formulaerrors;
 253                  } else {
 254                      eval('$str = ' . $formula . ';');
 255                  }
 256                  $replaces[$formula] = $str;
 257              }
 258              $anstext = str_replace(array_keys($replaces), array_values($replaces), $anssubstituted);
 259              $comment->stranswers[$key] = $anssubstituted.'<br/>'.$anstext;
 260          }
 261          return fullclone($comment);
 262      }
 263  
 264      public function get_virtual_qtype() {
 265          return question_bank::get_qtype('multichoice');
 266      }
 267  
 268      public function get_possible_responses($questiondata) {
 269          if ($questiondata->options->single) {
 270              $responses = array();
 271  
 272              foreach ($questiondata->options->answers as $aid => $answer) {
 273                  $responses[$aid] = new question_possible_response($answer->answer,
 274                          $answer->fraction);
 275              }
 276  
 277              $responses[null] = question_possible_response::no_response();
 278              return array($questiondata->id => $responses);
 279          } else {
 280              $parts = array();
 281  
 282              foreach ($questiondata->options->answers as $aid => $answer) {
 283                  $parts[$aid] = array($aid =>
 284                          new question_possible_response($answer->answer, $answer->fraction));
 285              }
 286  
 287              return $parts;
 288          }
 289      }
 290  
 291      public function move_files($questionid, $oldcontextid, $newcontextid) {
 292          $fs = get_file_storage();
 293  
 294          parent::move_files($questionid, $oldcontextid, $newcontextid);
 295          $this->move_files_in_answers($questionid, $oldcontextid, $newcontextid, true);
 296          $this->move_files_in_hints($questionid, $oldcontextid, $newcontextid);
 297  
 298          $fs->move_area_files_to_new_context($oldcontextid,
 299                  $newcontextid, 'qtype_calculatedmulti', 'correctfeedback', $questionid);
 300          $fs->move_area_files_to_new_context($oldcontextid,
 301                  $newcontextid, 'qtype_calculatedmulti', 'partiallycorrectfeedback', $questionid);
 302          $fs->move_area_files_to_new_context($oldcontextid,
 303                  $newcontextid, 'qtype_calculatedmulti', 'incorrectfeedback', $questionid);
 304      }
 305  
 306      protected function delete_files($questionid, $contextid) {
 307          $fs = get_file_storage();
 308  
 309          parent::delete_files($questionid, $contextid);
 310          $this->delete_files_in_answers($questionid, $contextid, true);
 311          $this->delete_files_in_hints($questionid, $contextid);
 312  
 313          $fs->delete_area_files($contextid, 'qtype_calculatedmulti',
 314                  'correctfeedback', $questionid);
 315          $fs->delete_area_files($contextid, 'qtype_calculatedmulti',
 316                  'partiallycorrectfeedback', $questionid);
 317          $fs->delete_area_files($contextid, 'qtype_calculatedmulti',
 318                  'incorrectfeedback', $questionid);
 319      }
 320  }