Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

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

   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   * Defines the editing form for the multi-answer question type.
  19   *
  20   * @package    qtype
  21   * @subpackage multianswer
  22   * @copyright  2007 Jamie Pratt me@jamiep.org
  23   * @license    http://www.gnu.org/copyleft/gpl.html GNU Public License
  24   */
  25  
  26  
  27  defined('MOODLE_INTERNAL') || die();
  28  
  29  require_once($CFG->dirroot . '/question/type/numerical/questiontype.php');
  30  
  31  
  32  /**
  33   * Form for editing multi-answer questions.
  34   *
  35   * @copyright  2007 Jamie Pratt me@jamiep.org
  36   * @license    http://www.gnu.org/copyleft/gpl.html GNU Public License
  37   */
  38  class qtype_multianswer_edit_form extends question_edit_form {
  39  
  40      // The variable $questiondisplay will contain the qtype_multianswer_extract_question from
  41      // the questiontext.
  42      public $questiondisplay;
  43      // The variable $savedquestiondisplay will contain the qtype_multianswer_extract_question
  44      // from the questiontext in database.
  45      public $savedquestion;
  46      public $savedquestiondisplay;
  47      /** @var bool this question is used in quiz */
  48      public $usedinquiz = false;
  49      /** @var bool the qtype has been changed */
  50      public $qtypechange = false;
  51      /** @var integer number of questions that have been deleted   */
  52      public $negativediff = 0;
  53      /** @var integer number of quiz that used this question   */
  54      public $nbofquiz = 0;
  55      /** @var integer number of attempts that used this question   */
  56      public $nbofattempts = 0;
  57      public $confirm = 0;
  58      public $reload = false;
  59      /** @var qtype_numerical_answer_processor used when validating numerical answers. */
  60      protected $ap = null;
  61  
  62  
  63      public function __construct($submiturl, $question, $category, $contexts, $formeditable = true) {
  64          global $SESSION, $CFG, $DB;
  65          $this->regenerate = true;
  66          $this->reload = optional_param('reload', false, PARAM_BOOL);
  67  
  68          $this->usedinquiz = false;
  69  
  70          if (isset($question->id) && $question->id != 0) {
  71              // TODO MDL-43779 should not have quiz-specific code here.
  72              $this->savedquestiondisplay = fullclone($question);
  73              $this->nbofquiz = $DB->count_records('quiz_slots', array('questionid' => $question->id));
  74              $this->usedinquiz = $this->nbofquiz > 0;
  75              $this->nbofattempts = $DB->count_records_sql("
  76                      SELECT count(1)
  77                        FROM {quiz_slots} slot
  78                        JOIN {quiz_attempts} quiza ON quiza.quiz = slot.quizid
  79                       WHERE slot.questionid = ?
  80                         AND quiza.preview = 0", array($question->id));
  81          }
  82  
  83          parent::__construct($submiturl, $question, $category, $contexts, $formeditable);
  84      }
  85  
  86      protected function definition_inner($mform) {
  87          $mform->addElement('hidden', 'reload', 1);
  88          $mform->setType('reload', PARAM_INT);
  89  
  90          // Remove meaningless defaultmark field.
  91          $mform->removeElement('defaultmark');
  92          $this->confirm = optional_param('confirm', false, PARAM_BOOL);
  93  
  94          // Display the questions from questiontext.
  95          if ($questiontext = optional_param_array('questiontext', false, PARAM_RAW)) {
  96              $this->questiondisplay = fullclone(qtype_multianswer_extract_question($questiontext));
  97  
  98          } else {
  99              if (!$this->reload && !empty($this->savedquestiondisplay->id)) {
 100                  // Use database data as this is first pass
 101                  // question->id == 0 so no stored datasets.
 102                  $this->questiondisplay = fullclone($this->savedquestiondisplay);
 103                  foreach ($this->questiondisplay->options->questions as $subquestion) {
 104                      if (!empty($subquestion)) {
 105                          $subquestion->answer = array('');
 106                          foreach ($subquestion->options->answers as $ans) {
 107                              $subquestion->answer[] = $ans->answer;
 108                          }
 109                      }
 110                  }
 111              } else {
 112                  $this->questiondisplay = "";
 113              }
 114          }
 115  
 116          if (isset($this->savedquestiondisplay->options->questions) &&
 117                  is_array($this->savedquestiondisplay->options->questions)) {
 118              $countsavedsubquestions = 0;
 119              foreach ($this->savedquestiondisplay->options->questions as $subquestion) {
 120                  if (!empty($subquestion)) {
 121                      $countsavedsubquestions++;
 122                  }
 123              }
 124          } else {
 125              $countsavedsubquestions = 0;
 126          }
 127          if ($this->reload) {
 128              if (isset($this->questiondisplay->options->questions) &&
 129                      is_array($this->questiondisplay->options->questions)) {
 130                  $countsubquestions = 0;
 131                  foreach ($this->questiondisplay->options->questions as $subquestion) {
 132                      if (!empty($subquestion)) {
 133                          $countsubquestions++;
 134                      }
 135                  }
 136              } else {
 137                  $countsubquestions = 0;
 138              }
 139          } else {
 140              $countsubquestions = $countsavedsubquestions;
 141          }
 142  
 143          $mform->addElement('submit', 'analyzequestion',
 144                  get_string('decodeverifyquestiontext', 'qtype_multianswer'));
 145          $mform->registerNoSubmitButton('analyzequestion');
 146          if ($this->reload) {
 147              for ($sub = 1; $sub <= $countsubquestions; $sub++) {
 148  
 149                  if (isset($this->questiondisplay->options->questions[$sub]->qtype)) {
 150                      $this->editas[$sub] = $this->questiondisplay->options->questions[$sub]->qtype;
 151                  } else {
 152                      $this->editas[$sub] = optional_param('sub_'.$sub.'_qtype', 'unknown type', PARAM_PLUGIN);
 153                  }
 154  
 155                  $storemess = '';
 156                  if (isset($this->savedquestiondisplay->options->questions[$sub]->qtype) &&
 157                          $this->savedquestiondisplay->options->questions[$sub]->qtype !=
 158                                  $this->questiondisplay->options->questions[$sub]->qtype &&
 159                          $this->savedquestiondisplay->options->questions[$sub]->qtype != 'subquestion_replacement') {
 160                      $this->qtypechange = true;
 161                      $storemess = ' ' . html_writer::tag('span', get_string(
 162                              'storedqtype', 'qtype_multianswer', question_bank::get_qtype_name(
 163                                      $this->savedquestiondisplay->options->questions[$sub]->qtype)),
 164                              array('class' => 'error'));
 165                  }
 166                              $mform->addElement('header', 'subhdr'.$sub, get_string('questionno', 'question',
 167                         '{#'.$sub.'}').'&nbsp;'.question_bank::get_qtype_name(
 168                          $this->questiondisplay->options->questions[$sub]->qtype).$storemess);
 169  
 170                  $mform->addElement('static', 'sub_'.$sub.'_questiontext',
 171                          get_string('questiondefinition', 'qtype_multianswer'));
 172  
 173                  if (isset ($this->questiondisplay->options->questions[$sub]->questiontext)) {
 174                      $mform->setDefault('sub_'.$sub.'_questiontext',
 175                              $this->questiondisplay->options->questions[$sub]->questiontext['text']);
 176                  }
 177  
 178                  $mform->addElement('static', 'sub_'.$sub.'_defaultmark',
 179                          get_string('defaultmark', 'question'));
 180                  $mform->setDefault('sub_'.$sub.'_defaultmark',
 181                          $this->questiondisplay->options->questions[$sub]->defaultmark);
 182  
 183                  if ($this->questiondisplay->options->questions[$sub]->qtype == 'shortanswer') {
 184                      $mform->addElement('static', 'sub_'.$sub.'_usecase',
 185                              get_string('casesensitive', 'qtype_shortanswer'));
 186                  }
 187  
 188                  if ($this->questiondisplay->options->questions[$sub]->qtype == 'multichoice') {
 189                      $mform->addElement('static', 'sub_'.$sub.'_layout',
 190                              get_string('layout', 'qtype_multianswer'));
 191                      $mform->addElement('static', 'sub_'.$sub.'_shuffleanswers',
 192                              get_string('shuffleanswers', 'qtype_multichoice'));
 193                  }
 194  
 195                  foreach ($this->questiondisplay->options->questions[$sub]->answer as $key => $ans) {
 196                      $mform->addElement('static', 'sub_'.$sub.'_answer['.$key.']',
 197                              get_string('answer', 'question'));
 198  
 199                      if ($this->questiondisplay->options->questions[$sub]->qtype == 'numerical' &&
 200                              $key == 0) {
 201                          $mform->addElement('static', 'sub_'.$sub.'_tolerance['.$key.']',
 202                                  get_string('acceptederror', 'qtype_numerical'));
 203                      }
 204  
 205                      $mform->addElement('static', 'sub_'.$sub.'_fraction['.$key.']',
 206                              get_string('gradenoun'));
 207  
 208                      $mform->addElement('static', 'sub_'.$sub.'_feedback['.$key.']',
 209                              get_string('feedback', 'question'));
 210                  }
 211              }
 212  
 213              $this->negativediff = $countsavedsubquestions - $countsubquestions;
 214              if (($this->negativediff > 0) ||$this->qtypechange ||
 215                      ($this->usedinquiz && $this->negativediff != 0)) {
 216                  $mform->addElement('header', 'additemhdr',
 217                          get_string('warningquestionmodified', 'qtype_multianswer'));
 218              }
 219              if ($this->negativediff > 0) {
 220                  $mform->addElement('static', 'alert1', "<strong>".
 221                          get_string('questiondeleted', 'qtype_multianswer')."</strong>",
 222                          get_string('questionsless', 'qtype_multianswer', $this->negativediff));
 223              }
 224              if ($this->qtypechange) {
 225                  $mform->addElement('static', 'alert1', "<strong>".
 226                          get_string('questiontypechanged', 'qtype_multianswer')."</strong>",
 227                          get_string('questiontypechangedcomment', 'qtype_multianswer'));
 228              }
 229          }
 230          if ($this->usedinquiz) {
 231              if ($this->negativediff < 0) {
 232                  $diff = $countsubquestions - $countsavedsubquestions;
 233                  $mform->addElement('static', 'alert1', "<strong>".
 234                          get_string('questionsadded', 'qtype_multianswer')."</strong>",
 235                          "<strong>".get_string('questionsmore', 'qtype_multianswer', $diff).
 236                          "</strong>");
 237              }
 238              $a = new stdClass();
 239              $a->nb_of_quiz = $this->nbofquiz;
 240              $a->nb_of_attempts = $this->nbofattempts;
 241              $mform->addElement('header', 'additemhdr2',
 242                      get_string('questionusedinquiz', 'qtype_multianswer', $a));
 243              $mform->addElement('static', 'alertas',
 244                      get_string('youshouldnot', 'qtype_multianswer'));
 245          }
 246          if (($this->negativediff > 0 || $this->usedinquiz &&
 247                  ($this->negativediff > 0 || $this->negativediff < 0 || $this->qtypechange)) &&
 248                          $this->reload) {
 249              $mform->addElement('header', 'additemhdr',
 250                      get_string('questionsaveasedited', 'qtype_multianswer'));
 251              $mform->addElement('checkbox', 'confirm', '',
 252                      get_string('confirmquestionsaveasedited', 'qtype_multianswer'));
 253              $mform->setDefault('confirm', 0);
 254          } else {
 255              $mform->addElement('hidden', 'confirm', 0);
 256              $mform->setType('confirm', PARAM_BOOL);
 257          }
 258  
 259          $this->add_interactive_settings(true, true);
 260      }
 261  
 262  
 263      public function set_data($question) {
 264          global $DB;
 265          $defaultvalues = array();
 266          if (isset($question->id) and $question->id and $question->qtype &&
 267                  $question->questiontext) {
 268  
 269              foreach ($question->options->questions as $key => $wrapped) {
 270                  if (!empty($wrapped)) {
 271                      // The old way of restoring the definitions is kept to gradually
 272                      // update all multianswer questions.
 273                      if (empty($wrapped->questiontext)) {
 274                          $parsableanswerdef = '{' . $wrapped->defaultmark . ':';
 275                          switch ($wrapped->qtype) {
 276                              case 'multichoice':
 277                                  $parsableanswerdef .= 'MULTICHOICE:';
 278                                  break;
 279                              case 'shortanswer':
 280                                  $parsableanswerdef .= 'SHORTANSWER:';
 281                                  break;
 282                              case 'numerical':
 283                                  $parsableanswerdef .= 'NUMERICAL:';
 284                                  break;
 285                              case 'subquestion_replacement':
 286                                  continue 2;
 287                              default:
 288                                  print_error('unknownquestiontype', 'question', '',
 289                                          $wrapped->qtype);
 290                          }
 291                          $separator = '';
 292                          foreach ($wrapped->options->answers as $subanswer) {
 293                              $parsableanswerdef .= $separator
 294                                  . '%' . round(100 * $subanswer->fraction) . '%';
 295                              if (is_array($subanswer->answer)) {
 296                                  $parsableanswerdef .= $subanswer->answer['text'];
 297                              } else {
 298                                  $parsableanswerdef .= $subanswer->answer;
 299                              }
 300                              if (!empty($wrapped->options->tolerance)) {
 301                                  // Special for numerical answers.
 302                                  $parsableanswerdef .= ":{$wrapped->options->tolerance}";
 303                                  // We only want tolerance for the first alternative, it will
 304                                  // be applied to all of the alternatives.
 305                                  unset($wrapped->options->tolerance);
 306                              }
 307                              if ($subanswer->feedback) {
 308                                  $parsableanswerdef .= "#{$subanswer->feedback}";
 309                              }
 310                              $separator = '~';
 311                          }
 312                          $parsableanswerdef .= '}';
 313                          // Fix the questiontext fields of old questions.
 314                          $DB->set_field('question', 'questiontext', $parsableanswerdef,
 315                                  array('id' => $wrapped->id));
 316                      } else {
 317                          $parsableanswerdef = str_replace('&#', '&\#', $wrapped->questiontext);
 318                      }
 319                      $question->questiontext = str_replace("{#$key}", $parsableanswerdef,
 320                              $question->questiontext);
 321                  }
 322              }
 323          }
 324  
 325          // Set default to $questiondisplay questions elements.
 326          if ($this->reload) {
 327              if (isset($this->questiondisplay->options->questions)) {
 328                  $subquestions = fullclone($this->questiondisplay->options->questions);
 329                  if (count($subquestions)) {
 330                      $sub = 1;
 331                      foreach ($subquestions as $subquestion) {
 332                          $prefix = 'sub_'.$sub.'_';
 333  
 334                          // Validate parameters.
 335                          $answercount = 0;
 336                          $maxgrade = false;
 337                          $maxfraction = -1;
 338                          if ($subquestion->qtype == 'shortanswer') {
 339                              switch ($subquestion->usecase) {
 340                                  case '1':
 341                                      $defaultvalues[$prefix.'usecase'] =
 342                                              get_string('caseyes', 'qtype_shortanswer');
 343                                      break;
 344                                  case '0':
 345                                  default :
 346                                      $defaultvalues[$prefix.'usecase'] =
 347                                              get_string('caseno', 'qtype_shortanswer');
 348                              }
 349                          }
 350  
 351                          if ($subquestion->qtype == 'multichoice') {
 352                              $defaultvalues[$prefix.'layout'] = $subquestion->layout;
 353                              if ($subquestion->single == 1) {
 354                                  switch ($subquestion->layout) {
 355                                      case '0':
 356                                          $defaultvalues[$prefix.'layout'] =
 357                                              get_string('layoutselectinline', 'qtype_multianswer');
 358                                          break;
 359                                      case '1':
 360                                          $defaultvalues[$prefix.'layout'] =
 361                                              get_string('layoutvertical', 'qtype_multianswer');
 362                                          break;
 363                                      case '2':
 364                                          $defaultvalues[$prefix.'layout'] =
 365                                              get_string('layouthorizontal', 'qtype_multianswer');
 366                                          break;
 367                                      default:
 368                                          $defaultvalues[$prefix.'layout'] =
 369                                              get_string('layoutundefined', 'qtype_multianswer');
 370                                  }
 371                              } else {
 372                                  switch ($subquestion->layout) {
 373                                      case '1':
 374                                          $defaultvalues[$prefix.'layout'] =
 375                                              get_string('layoutmultiple_vertical', 'qtype_multianswer');
 376                                          break;
 377                                      case '2':
 378                                          $defaultvalues[$prefix.'layout'] =
 379                                              get_string('layoutmultiple_horizontal', 'qtype_multianswer');
 380                                          break;
 381                                      default:
 382                                          $defaultvalues[$prefix.'layout'] =
 383                                              get_string('layoutundefined', 'qtype_multianswer');
 384                                  }
 385                              }
 386                              if ($subquestion->shuffleanswers ) {
 387                                  $defaultvalues[$prefix.'shuffleanswers'] = get_string('yes', 'moodle');
 388                              } else {
 389                                  $defaultvalues[$prefix.'shuffleanswers'] = get_string('no', 'moodle');
 390                              }
 391                          }
 392                          foreach ($subquestion->answer as $key => $answer) {
 393                              if ($subquestion->qtype == 'numerical' && $key == 0) {
 394                                  $defaultvalues[$prefix.'tolerance['.$key.']'] =
 395                                          $subquestion->tolerance[0];
 396                              }
 397                              if (is_array($answer)) {
 398                                  $answer = $answer['text'];
 399                              }
 400                              $trimmedanswer = trim($answer);
 401                              if ($trimmedanswer !== '') {
 402                                  $answercount++;
 403                                  if ($subquestion->qtype == 'numerical' &&
 404                                          !(qtype_numerical::is_valid_number($trimmedanswer) || $trimmedanswer == '*')) {
 405                                      $this->_form->setElementError($prefix.'answer['.$key.']',
 406                                              get_string('answermustbenumberorstar',
 407                                                      'qtype_numerical'));
 408                                  }
 409                                  if ($subquestion->fraction[$key] == 1) {
 410                                      $maxgrade = true;
 411                                  }
 412                                  if ($subquestion->fraction[$key] > $maxfraction) {
 413                                      $maxfraction = $subquestion->fraction[$key];
 414                                  }
 415                                  // For 'multiresponse' we are OK if there is at least one fraction > 0.
 416                                  if ($subquestion->qtype == 'multichoice' && $subquestion->single == 0 &&
 417                                      $subquestion->fraction[$key] > 0) {
 418                                      $maxgrade = true;
 419                                  }
 420                              }
 421  
 422                              $defaultvalues[$prefix.'answer['.$key.']'] =
 423                                      htmlspecialchars($answer);
 424                          }
 425                          if ($answercount == 0) {
 426                              if ($subquestion->qtype == 'multichoice') {
 427                                  $this->_form->setElementError($prefix.'answer[0]',
 428                                          get_string('notenoughanswers', 'qtype_multichoice', 2));
 429                              } else {
 430                                  $this->_form->setElementError($prefix.'answer[0]',
 431                                          get_string('notenoughanswers', 'question', 1));
 432                              }
 433                          }
 434                          if ($maxgrade == false) {
 435                              $this->_form->setElementError($prefix.'fraction[0]',
 436                                      get_string('fractionsnomax', 'question'));
 437                          }
 438                          foreach ($subquestion->feedback as $key => $answer) {
 439  
 440                              $defaultvalues[$prefix.'feedback['.$key.']'] =
 441                                      htmlspecialchars ($answer['text']);
 442                          }
 443                          foreach ($subquestion->fraction as $key => $answer) {
 444                              $defaultvalues[$prefix.'fraction['.$key.']'] = $answer;
 445                          }
 446  
 447                          $sub++;
 448                      }
 449                  }
 450              }
 451          }
 452          $defaultvalues['alertas'] = "<strong>".get_string('questioninquiz', 'qtype_multianswer').
 453                  "</strong>";
 454  
 455          if ($defaultvalues != "") {
 456              $question = (object)((array)$question + $defaultvalues);
 457          }
 458          $question = $this->data_preprocessing_hints($question, true, true);
 459          parent::set_data($question);
 460      }
 461  
 462      public function validation($data, $files) {
 463          $errors = parent::validation($data, $files);
 464  
 465          $questiondisplay = qtype_multianswer_extract_question($data['questiontext']);
 466  
 467          $errors = array_merge($errors, qtype_multianswer_validate_question($questiondisplay));
 468  
 469          if (($this->negativediff > 0 || $this->usedinquiz &&
 470                  ($this->negativediff > 0 || $this->negativediff < 0 ||
 471                          $this->qtypechange)) && !$this->confirm) {
 472              $errors['confirm'] =
 473                      get_string('confirmsave', 'qtype_multianswer', $this->negativediff);
 474          }
 475  
 476          return $errors;
 477      }
 478  
 479      public function qtype() {
 480          return 'multianswer';
 481      }
 482  }