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 310 and 401] [Versions 39 and 401] [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 drag-and-drop images onto images question type.
  19   *
  20   * @package   qtype_ddmarker
  21   * @copyright 2012 The Open University
  22   * @author    Jamie Pratt <me@jamiep.org>
  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/ddimageortext/questiontypebase.php');
  30  
  31  /**
  32   * Question hint for ddmarker.
  33   *
  34   * An extension of {@link question_hint} for questions like match and multiple
  35   * choice with multile answers, where there are options for whether to show the
  36   * number of parts right at each stage, and to reset the wrong parts.
  37   *
  38   * @copyright  2010 The Open University
  39   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  40   */
  41  class question_hint_ddmarker extends question_hint_with_parts {
  42  
  43      public $statewhichincorrect;
  44  
  45      /**
  46       * Constructor.
  47       * @param int the hint id from the database.
  48       * @param string $hint The hint text
  49       * @param int the corresponding text FORMAT_... type.
  50       * @param bool $shownumcorrect whether the number of right parts should be shown
  51       * @param bool $clearwrong whether the wrong parts should be reset.
  52       */
  53      public function __construct($id, $hint, $hintformat, $shownumcorrect,
  54                                                              $clearwrong, $statewhichincorrect) {
  55          parent::__construct($id, $hint, $hintformat, $shownumcorrect, $clearwrong);
  56          $this->statewhichincorrect = $statewhichincorrect;
  57      }
  58  
  59      /**
  60       * Create a basic hint from a row loaded from the question_hints table in the database.
  61       * @param object $row with property options as well as hint, shownumcorrect and clearwrong set.
  62       * @return question_hint_ddmarker
  63       */
  64      public static function load_from_record($row) {
  65          return new question_hint_ddmarker($row->id, $row->hint, $row->hintformat,
  66                  $row->shownumcorrect, $row->clearwrong, $row->options);
  67      }
  68  
  69      public function adjust_display_options(question_display_options $options) {
  70          parent::adjust_display_options($options);
  71          $options->statewhichincorrect = $this->statewhichincorrect;
  72      }
  73  }
  74  
  75  
  76  
  77  /**
  78   * The drag-and-drop markers question type class.
  79   *
  80   * @copyright  2009 The Open University
  81   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  82   */
  83  class qtype_ddmarker extends qtype_ddtoimage_base {
  84  
  85      public function save_defaults_for_new_questions(stdClass $fromform): void {
  86          parent::save_defaults_for_new_questions($fromform);
  87          $this->set_default_value('showmisplaced', $fromform->showmisplaced);
  88          $this->set_default_value('shuffleanswers', $fromform->shuffleanswers);
  89      }
  90  
  91      public function save_question_options($formdata) {
  92          global $DB, $USER;
  93          $context = $formdata->context;
  94  
  95          $options = $DB->get_record('qtype_ddmarker', array('questionid' => $formdata->id));
  96          if (!$options) {
  97              $options = new stdClass();
  98              $options->questionid = $formdata->id;
  99              $options->correctfeedback = '';
 100              $options->partiallycorrectfeedback = '';
 101              $options->incorrectfeedback = '';
 102              $options->id = $DB->insert_record('qtype_ddmarker', $options);
 103          }
 104  
 105          $options->shuffleanswers = !empty($formdata->shuffleanswers);
 106          $options->showmisplaced = !empty($formdata->showmisplaced);
 107          $options = $this->save_combined_feedback_helper($options, $formdata, $context, true);
 108          $this->save_hints($formdata, true);
 109          $DB->update_record('qtype_ddmarker', $options);
 110          $DB->delete_records('qtype_ddmarker_drops', array('questionid' => $formdata->id));
 111          foreach (array_keys($formdata->drops) as $dropno) {
 112              if ($formdata->drops[$dropno]['choice'] == 0) {
 113                  continue;
 114              }
 115              $drop = new stdClass();
 116              $drop->questionid = $formdata->id;
 117              $drop->no = $dropno + 1;
 118              $drop->shape = $formdata->drops[$dropno]['shape'];
 119              $drop->coords = $formdata->drops[$dropno]['coords'];
 120              $drop->choice = $formdata->drops[$dropno]['choice'];
 121  
 122              $DB->insert_record('qtype_ddmarker_drops', $drop);
 123          }
 124  
 125          // An array of drag no -> drag id.
 126          $olddragids = $DB->get_records_menu('qtype_ddmarker_drags',
 127                                      array('questionid' => $formdata->id),
 128                                      '', 'no, id');
 129          foreach (array_keys($formdata->drags) as $dragno) {
 130              if ($formdata->drags[$dragno]['label'] !== '') {
 131                  $drag = new stdClass();
 132                  $drag->questionid = $formdata->id;
 133                  $drag->no = $dragno + 1;
 134                  if ($formdata->drags[$dragno]['noofdrags'] == 0) {
 135                      $drag->infinite = 1;
 136                      $drag->noofdrags = 1;
 137                  } else {
 138                      $drag->infinite = 0;
 139                      $drag->noofdrags = $formdata->drags[$dragno]['noofdrags'];
 140                  }
 141                  $drag->label = $formdata->drags[$dragno]['label'];
 142  
 143                  if (isset($olddragids[$dragno + 1])) {
 144                      $drag->id = $olddragids[$dragno + 1];
 145                      unset($olddragids[$dragno + 1]);
 146                      $DB->update_record('qtype_ddmarker_drags', $drag);
 147                  } else {
 148                      $drag->id = $DB->insert_record('qtype_ddmarker_drags', $drag);
 149                  }
 150              }
 151          }
 152  
 153          if (!empty($olddragids)) {
 154              list($sql, $params) = $DB->get_in_or_equal(array_values($olddragids));
 155              $DB->delete_records_select('qtype_ddmarker_drags', "id $sql", $params);
 156          }
 157          file_save_draft_area_files($formdata->bgimage, $formdata->context->id,
 158                                      'qtype_ddmarker', 'bgimage', $formdata->id,
 159                                      array('subdirs' => 0, 'maxbytes' => 0, 'maxfiles' => 1));
 160      }
 161  
 162      public function save_hints($formdata, $withparts = false) {
 163          global $DB;
 164          $context = $formdata->context;
 165  
 166          $oldhints = $DB->get_records('question_hints',
 167                  array('questionid' => $formdata->id), 'id ASC');
 168  
 169          if (!empty($formdata->hint)) {
 170              $numhints = max(array_keys($formdata->hint)) + 1;
 171          } else {
 172              $numhints = 0;
 173          }
 174  
 175          if ($withparts) {
 176              if (!empty($formdata->hintclearwrong)) {
 177                  $numclears = max(array_keys($formdata->hintclearwrong)) + 1;
 178              } else {
 179                  $numclears = 0;
 180              }
 181              if (!empty($formdata->hintshownumcorrect)) {
 182                  $numshows = max(array_keys($formdata->hintshownumcorrect)) + 1;
 183              } else {
 184                  $numshows = 0;
 185              }
 186              $numhints = max($numhints, $numclears, $numshows);
 187          }
 188  
 189          for ($i = 0; $i < $numhints; $i += 1) {
 190              if (html_is_blank($formdata->hint[$i]['text'])) {
 191                  $formdata->hint[$i]['text'] = '';
 192              }
 193  
 194              if ($withparts) {
 195                  $clearwrong = !empty($formdata->hintclearwrong[$i]);
 196                  $shownumcorrect = !empty($formdata->hintshownumcorrect[$i]);
 197                  $statewhichincorrect = !empty($formdata->hintoptions[$i]);
 198              }
 199  
 200              if (empty($formdata->hint[$i]['text']) && empty($clearwrong) &&
 201                      empty($shownumcorrect) && empty($statewhichincorrect)) {
 202                  continue;
 203              }
 204  
 205              // Update an existing hint if possible.
 206              $hint = array_shift($oldhints);
 207              if (!$hint) {
 208                  $hint = new stdClass();
 209                  $hint->questionid = $formdata->id;
 210                  $hint->hint = '';
 211                  $hint->id = $DB->insert_record('question_hints', $hint);
 212              }
 213  
 214              $hint->hint = $this->import_or_save_files($formdata->hint[$i],
 215                      $context, 'question', 'hint', $hint->id);
 216              $hint->hintformat = $formdata->hint[$i]['format'];
 217              if ($withparts) {
 218                  $hint->clearwrong = $clearwrong;
 219                  $hint->shownumcorrect = $shownumcorrect;
 220                  $hint->options = $statewhichincorrect;
 221              }
 222              $DB->update_record('question_hints', $hint);
 223          }
 224  
 225          // Delete any remaining old hints.
 226          $fs = get_file_storage();
 227          foreach ($oldhints as $oldhint) {
 228              $fs->delete_area_files($context->id, 'question', 'hint', $oldhint->id);
 229              $DB->delete_records('question_hints', array('id' => $oldhint->id));
 230          }
 231      }
 232  
 233      protected function make_hint($hint) {
 234          return question_hint_ddmarker::load_from_record($hint);
 235      }
 236      protected function make_choice($dragdata) {
 237          return new qtype_ddmarker_drag_item($dragdata->label, $dragdata->no, $dragdata->infinite, $dragdata->noofdrags);
 238      }
 239  
 240      protected function make_place($dropdata) {
 241          return new qtype_ddmarker_drop_zone($dropdata->no, $dropdata->shape, $dropdata->coords);
 242      }
 243  
 244      protected function initialise_combined_feedback(question_definition $question,
 245                                                                  $questiondata, $withparts = false) {
 246          parent::initialise_combined_feedback($question, $questiondata, $withparts);
 247          $question->showmisplaced = $questiondata->options->showmisplaced;
 248      }
 249  
 250      public function move_files($questionid, $oldcontextid, $newcontextid) {
 251          global $DB;
 252          $fs = get_file_storage();
 253  
 254          parent::move_files($questionid, $oldcontextid, $newcontextid);
 255          $fs->move_area_files_to_new_context($oldcontextid,
 256                                      $newcontextid, 'qtype_ddmarker', 'bgimage', $questionid);
 257  
 258          $this->move_files_in_combined_feedback($questionid, $oldcontextid, $newcontextid);
 259          $this->move_files_in_hints($questionid, $oldcontextid, $newcontextid);
 260      }
 261  
 262      /**
 263       * Delete all the files belonging to this question.
 264       * @param int $questionid the question being deleted.
 265       * @param int $contextid the context the question is in.
 266       */
 267  
 268      protected function delete_files($questionid, $contextid) {
 269          global $DB;
 270          $fs = get_file_storage();
 271  
 272          parent::delete_files($questionid, $contextid);
 273  
 274          $this->delete_files_in_combined_feedback($questionid, $contextid);
 275          $this->delete_files_in_hints($questionid, $contextid);
 276      }
 277  
 278      public function export_to_xml($question, qformat_xml $format, $extra = null) {
 279          $fs = get_file_storage();
 280          $contextid = $question->contextid;
 281          $output = '';
 282  
 283          if ($question->options->shuffleanswers) {
 284              $output .= "    <shuffleanswers/>\n";
 285          }
 286          if ($question->options->showmisplaced) {
 287              $output .= "    <showmisplaced/>\n";
 288          }
 289          $output .= $format->write_combined_feedback($question->options,
 290                                                      $question->id,
 291                                                      $question->contextid);
 292          $files = $fs->get_area_files($contextid, 'qtype_ddmarker', 'bgimage', $question->id);
 293          $output .= "    ".$this->write_files($files, 2)."\n";;
 294  
 295          foreach ($question->options->drags as $drag) {
 296              $files =
 297                      $fs->get_area_files($contextid, 'qtype_ddmarker', 'dragimage', $drag->id);
 298              $output .= "    <drag>\n";
 299              $output .= "      <no>{$drag->no}</no>\n";
 300              $output .= $format->writetext($drag->label, 3);
 301              if ($drag->infinite) {
 302                  $output .= "      <infinite/>\n";
 303              }
 304              $output .= "      <noofdrags>{$drag->noofdrags}</noofdrags>\n";
 305              $output .= "    </drag>\n";
 306          }
 307          foreach ($question->options->drops as $drop) {
 308              $output .= "    <drop>\n";
 309              $output .= "      <no>{$drop->no}</no>\n";
 310              $output .= "      <shape>{$drop->shape}</shape>\n";
 311              $output .= "      <coords>{$drop->coords}</coords>\n";
 312              $output .= "      <choice>{$drop->choice}</choice>\n";
 313              $output .= "    </drop>\n";
 314          }
 315  
 316          return $output;
 317      }
 318  
 319      public function import_from_xml($data, $question, qformat_xml $format, $extra=null) {
 320          if (!isset($data['@']['type']) || $data['@']['type'] != 'ddmarker') {
 321              return false;
 322          }
 323  
 324          $question = $format->import_headers($data);
 325          $question->qtype = 'ddmarker';
 326  
 327          $question->shuffleanswers = array_key_exists('shuffleanswers',
 328                                                      $format->getpath($data, array('#'), array()));
 329          $question->showmisplaced = array_key_exists('showmisplaced',
 330                                                      $format->getpath($data, array('#'), array()));
 331  
 332          $filexml = $format->getpath($data, array('#', 'file'), array());
 333          $question->bgimage = $format->import_files_as_draft($filexml);
 334          $drags = $data['#']['drag'];
 335          $question->drags = array();
 336  
 337          foreach ($drags as $dragxml) {
 338              $dragno = $format->getpath($dragxml, array('#', 'no', 0, '#'), 0);
 339              $dragindex = $dragno - 1;
 340              $question->drags[$dragindex] = array();
 341              $question->drags[$dragindex]['label'] =
 342                          $format->getpath($dragxml, array('#', 'text', 0, '#'), '', true);
 343              if (array_key_exists('infinite', $dragxml['#'])) {
 344                  $question->drags[$dragindex]['noofdrags'] = 0; // Means infinite in the form.
 345              } else {
 346                  // Defaults to 1 if 'noofdrags' not set.
 347                  $question->drags[$dragindex]['noofdrags'] = $format->getpath($dragxml, array('#', 'noofdrags', 0, '#'), 1);
 348              }
 349          }
 350  
 351          $drops = $data['#']['drop'];
 352          $question->drops = array();
 353          foreach ($drops as $dropxml) {
 354              $dropno = $format->getpath($dropxml, array('#', 'no', 0, '#'), 0);
 355              $dropindex = $dropno - 1;
 356              $question->drops[$dropindex] = array();
 357              $question->drops[$dropindex]['choice'] =
 358                          $format->getpath($dropxml, array('#', 'choice', 0, '#'), 0);
 359              $question->drops[$dropindex]['shape'] =
 360                          $format->getpath($dropxml, array('#', 'shape', 0, '#'), '');
 361              $question->drops[$dropindex]['coords'] =
 362                          $format->getpath($dropxml, array('#', 'coords', 0, '#'), '');
 363          }
 364  
 365          $format->import_combined_feedback($question, $data, true);
 366          $format->import_hints($question, $data, true, true,
 367                  $format->get_format($question->questiontextformat));
 368  
 369          return $question;
 370      }
 371  
 372      public function get_random_guess_score($questiondata) {
 373          return null;
 374      }
 375  
 376  }