Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.2.x will end 22 April 2024 (12 months).
  • Bug fixes for security issues in 4.2.x will end 7 October 2024 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.1.x is supported too.

Differences Between: [Versions 310 and 402] [Versions 311 and 402] [Versions 39 and 402] [Versions 400 and 402] [Versions 401 and 402]

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