[ Index ]

PHP Cross Reference of Unnamed Project

title

Body

[close]

/question/type/multianswer/ -> questiontype.php (source)

   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 multi-answer question type.
  19   *
  20   * @package    qtype
  21   * @subpackage multianswer
  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/multichoice/question.php');
  31  
  32  /**
  33   * The multi-answer question type class.
  34   *
  35   * @copyright  1999 onwards Martin Dougiamas  {@link http://moodle.com}
  36   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  37   */
  38  class qtype_multianswer extends question_type {
  39  
  40      public function can_analyse_responses() {
  41          return false;
  42      }
  43  
  44      public function get_question_options($question) {
  45          global $DB, $OUTPUT;
  46  
  47          // Get relevant data indexed by positionkey from the multianswers table.
  48          $sequence = $DB->get_field('question_multianswer', 'sequence',
  49                  array('question' => $question->id), '*', MUST_EXIST);
  50  
  51          $wrappedquestions = $DB->get_records_list('question', 'id',
  52                  explode(',', $sequence), 'id ASC');
  53  
  54          // We want an array with question ids as index and the positions as values.
  55          $sequence = array_flip(explode(',', $sequence));
  56          array_walk($sequence, create_function('&$val', '$val++;'));
  57  
  58          // If a question is lost, the corresponding index is null
  59          // so this null convention is used to test $question->options->questions
  60          // before using the values.
  61          // First all possible questions from sequence are nulled
  62          // then filled with the data if available in  $wrappedquestions.
  63          foreach ($sequence as $seq) {
  64              $question->options->questions[$seq] = '';
  65          }
  66  
  67          foreach ($wrappedquestions as $wrapped) {
  68              question_bank::get_qtype($wrapped->qtype)->get_question_options($wrapped);
  69              // For wrapped questions the maxgrade is always equal to the defaultmark,
  70              // there is no entry in the question_instances table for them.
  71              $wrapped->maxmark = $wrapped->defaultmark;
  72              $question->options->questions[$sequence[$wrapped->id]] = $wrapped;
  73          }
  74          $question->hints = $DB->get_records('question_hints',
  75                  array('questionid' => $question->id), 'id ASC');
  76  
  77          return true;
  78      }
  79  
  80      public function save_question_options($question) {
  81          global $DB;
  82          $result = new stdClass();
  83  
  84          // This function needs to be able to handle the case where the existing set of wrapped
  85          // questions does not match the new set of wrapped questions so that some need to be
  86          // created, some modified and some deleted.
  87          // Unfortunately the code currently simply overwrites existing ones in sequence. This
  88          // will make re-marking after a re-ordering of wrapped questions impossible and
  89          // will also create difficulties if questiontype specific tables reference the id.
  90  
  91          // First we get all the existing wrapped questions.
  92          if (!$oldwrappedids = $DB->get_field('question_multianswer', 'sequence',
  93                  array('question' => $question->id))) {
  94              $oldwrappedquestions = array();
  95          } else {
  96              $oldwrappedquestions = $DB->get_records_list('question', 'id',
  97                      explode(',', $oldwrappedids), 'id ASC');
  98          }
  99  
 100          $sequence = array();
 101          foreach ($question->options->questions as $wrapped) {
 102              if (!empty($wrapped)) {
 103                  // If we still have some old wrapped question ids, reuse the next of them.
 104  
 105                  if (is_array($oldwrappedquestions) &&
 106                          $oldwrappedquestion = array_shift($oldwrappedquestions)) {
 107                      $wrapped->id = $oldwrappedquestion->id;
 108                      if ($oldwrappedquestion->qtype != $wrapped->qtype) {
 109                          switch ($oldwrappedquestion->qtype) {
 110                              case 'multichoice':
 111                                  $DB->delete_records('qtype_multichoice_options',
 112                                          array('questionid' => $oldwrappedquestion->id));
 113                                  break;
 114                              case 'shortanswer':
 115                                  $DB->delete_records('qtype_shortanswer_options',
 116                                          array('questionid' => $oldwrappedquestion->id));
 117                                  break;
 118                              case 'numerical':
 119                                  $DB->delete_records('question_numerical',
 120                                          array('question' => $oldwrappedquestion->id));
 121                                  break;
 122                              default:
 123                                  throw new moodle_exception('qtypenotrecognized',
 124                                          'qtype_multianswer', '', $oldwrappedquestion->qtype);
 125                                  $wrapped->id = 0;
 126                          }
 127                      }
 128                  } else {
 129                      $wrapped->id = 0;
 130                  }
 131              }
 132              $wrapped->name = $question->name;
 133              $wrapped->parent = $question->id;
 134              $previousid = $wrapped->id;
 135              // Save_question strips this extra bit off the category again.
 136              $wrapped->category = $question->category . ',1';
 137              $wrapped = question_bank::get_qtype($wrapped->qtype)->save_question(
 138                      $wrapped, clone($wrapped));
 139              $sequence[] = $wrapped->id;
 140              if ($previousid != 0 && $previousid != $wrapped->id) {
 141                  // For some reasons a new question has been created
 142                  // so delete the old one.
 143                  question_delete_question($previousid);
 144              }
 145          }
 146  
 147          // Delete redundant wrapped questions.
 148          if (is_array($oldwrappedquestions) && count($oldwrappedquestions)) {
 149              foreach ($oldwrappedquestions as $oldwrappedquestion) {
 150                  question_delete_question($oldwrappedquestion->id);
 151              }
 152          }
 153  
 154          if (!empty($sequence)) {
 155              $multianswer = new stdClass();
 156              $multianswer->question = $question->id;
 157              $multianswer->sequence = implode(',', $sequence);
 158              if ($oldid = $DB->get_field('question_multianswer', 'id',
 159                      array('question' => $question->id))) {
 160                  $multianswer->id = $oldid;
 161                  $DB->update_record('question_multianswer', $multianswer);
 162              } else {
 163                  $DB->insert_record('question_multianswer', $multianswer);
 164              }
 165          }
 166  
 167          $this->save_hints($question, true);
 168      }
 169  
 170      public function save_question($authorizedquestion, $form) {
 171          $question = qtype_multianswer_extract_question($form->questiontext);
 172          if (isset($authorizedquestion->id)) {
 173              $question->id = $authorizedquestion->id;
 174          }
 175  
 176          $question->category = $authorizedquestion->category;
 177          $form->defaultmark = $question->defaultmark;
 178          $form->questiontext = $question->questiontext;
 179          $form->questiontextformat = 0;
 180          $form->options = clone($question->options);
 181          unset($question->options);
 182          return parent::save_question($question, $form);
 183      }
 184  
 185      protected function make_hint($hint) {
 186          return question_hint_with_parts::load_from_record($hint);
 187      }
 188  
 189      public function delete_question($questionid, $contextid) {
 190          global $DB;
 191          $DB->delete_records('question_multianswer', array('question' => $questionid));
 192  
 193          parent::delete_question($questionid, $contextid);
 194      }
 195  
 196      protected function initialise_question_instance(question_definition $question, $questiondata) {
 197          parent::initialise_question_instance($question, $questiondata);
 198  
 199          $bits = preg_split('/\{#(\d+)\}/', $question->questiontext,
 200                  null, PREG_SPLIT_DELIM_CAPTURE);
 201          $question->textfragments[0] = array_shift($bits);
 202          $i = 1;
 203          while (!empty($bits)) {
 204              $question->places[$i] = array_shift($bits);
 205              $question->textfragments[$i] = array_shift($bits);
 206              $i += 1;
 207          }
 208          foreach ($questiondata->options->questions as $key => $subqdata) {
 209              $subqdata->contextid = $questiondata->contextid;
 210              if ($subqdata->qtype == 'multichoice') {
 211                  $answerregs = array();
 212                  if ($subqdata->options->shuffleanswers == 1 &&  isset($questiondata->options->shuffleanswers)
 213                      && $questiondata->options->shuffleanswers == 0 ) {
 214                      $subqdata->options->shuffleanswers = 0;
 215                  }
 216              }
 217              $question->subquestions[$key] = question_bank::make_question($subqdata);
 218              $question->subquestions[$key]->maxmark = $subqdata->defaultmark;
 219              if (isset($subqdata->options->layout)) {
 220                  $question->subquestions[$key]->layout = $subqdata->options->layout;
 221              }
 222          }
 223      }
 224  
 225      public function get_random_guess_score($questiondata) {
 226          $fractionsum = 0;
 227          $fractionmax = 0;
 228          foreach ($questiondata->options->questions as $key => $subqdata) {
 229              $fractionmax += $subqdata->defaultmark;
 230              $fractionsum += question_bank::get_qtype(
 231                      $subqdata->qtype)->get_random_guess_score($subqdata);
 232          }
 233          return $fractionsum / $fractionmax;
 234      }
 235  
 236      public function move_files($questionid, $oldcontextid, $newcontextid) {
 237          parent::move_files($questionid, $oldcontextid, $newcontextid);
 238          $this->move_files_in_hints($questionid, $oldcontextid, $newcontextid);
 239      }
 240  
 241      protected function delete_files($questionid, $contextid) {
 242          parent::delete_files($questionid, $contextid);
 243          $this->delete_files_in_hints($questionid, $contextid);
 244      }
 245  }
 246  
 247  
 248  // ANSWER_ALTERNATIVE regexes.
 249  define('ANSWER_ALTERNATIVE_FRACTION_REGEX',
 250         '=|%(-?[0-9]+)%');
 251  // For the syntax '(?<!' see http://www.perl.com/doc/manual/html/pod/perlre.html#item_C.
 252  define('ANSWER_ALTERNATIVE_ANSWER_REGEX',
 253          '.+?(?<!\\\\|&|&amp;)(?=[~#}]|$)');
 254  define('ANSWER_ALTERNATIVE_FEEDBACK_REGEX',
 255          '.*?(?<!\\\\)(?=[~}]|$)');
 256  define('ANSWER_ALTERNATIVE_REGEX',
 257         '(' . ANSWER_ALTERNATIVE_FRACTION_REGEX .')?' .
 258         '(' . ANSWER_ALTERNATIVE_ANSWER_REGEX . ')' .
 259         '(#(' . ANSWER_ALTERNATIVE_FEEDBACK_REGEX .'))?');
 260  
 261  // Parenthesis positions for ANSWER_ALTERNATIVE_REGEX.
 262  define('ANSWER_ALTERNATIVE_REGEX_PERCENTILE_FRACTION', 2);
 263  define('ANSWER_ALTERNATIVE_REGEX_FRACTION', 1);
 264  define('ANSWER_ALTERNATIVE_REGEX_ANSWER', 3);
 265  define('ANSWER_ALTERNATIVE_REGEX_FEEDBACK', 5);
 266  
 267  // NUMBER_FORMATED_ALTERNATIVE_ANSWER_REGEX is used
 268  // for identifying numerical answers in ANSWER_ALTERNATIVE_REGEX_ANSWER.
 269  define('NUMBER_REGEX',
 270          '-?(([0-9]+[.,]?[0-9]*|[.,][0-9]+)([eE][-+]?[0-9]+)?)');
 271  define('NUMERICAL_ALTERNATIVE_REGEX',
 272          '^(' . NUMBER_REGEX . ')(:' . NUMBER_REGEX . ')?$');
 273  
 274  // Parenthesis positions for NUMERICAL_FORMATED_ALTERNATIVE_ANSWER_REGEX.
 275  define('NUMERICAL_CORRECT_ANSWER', 1);
 276  define('NUMERICAL_ABS_ERROR_MARGIN', 6);
 277  
 278  // Remaining ANSWER regexes.
 279  define('ANSWER_TYPE_DEF_REGEX',
 280          '(NUMERICAL|NM)|(MULTICHOICE|MC)|(MULTICHOICE_V|MCV)|(MULTICHOICE_H|MCH)|' .
 281          '(SHORTANSWER|SA|MW)|(SHORTANSWER_C|SAC|MWC)|' .
 282          '(MULTICHOICE_S|MCS)|(MULTICHOICE_VS|MCVS)|(MULTICHOICE_HS|MCHS)');
 283  define('ANSWER_START_REGEX',
 284         '\{([0-9]*):(' . ANSWER_TYPE_DEF_REGEX . '):');
 285  
 286  define('ANSWER_REGEX',
 287          ANSWER_START_REGEX
 288          . '(' . ANSWER_ALTERNATIVE_REGEX
 289          . '(~'
 290          . ANSWER_ALTERNATIVE_REGEX
 291          . ')*)\}');
 292  
 293  // Parenthesis positions for singulars in ANSWER_REGEX.
 294  define('ANSWER_REGEX_NORM', 1);
 295  define('ANSWER_REGEX_ANSWER_TYPE_NUMERICAL', 3);
 296  define('ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE', 4);
 297  define('ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_REGULAR', 5);
 298  define('ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_HORIZONTAL', 6);
 299  define('ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER', 7);
 300  define('ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER_C', 8);
 301  define('ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_SHUFFLED', 9);
 302  define('ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_REGULAR_SHUFFLED', 10);
 303  define('ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_HORIZONTAL_SHUFFLED', 11);
 304  define('ANSWER_REGEX_ALTERNATIVES', 12);
 305  
 306  /**
 307   * Initialise subquestion fields that are constant across all MULTICHOICE
 308   * types.
 309   *
 310   * @param objet $wrapped  The subquestion to initialise
 311   *
 312   */
 313  function qtype_multianswer_initialise_multichoice_subquestion($wrapped) {
 314      $wrapped->qtype = 'multichoice';
 315      $wrapped->single = 1;
 316      $wrapped->answernumbering = 0;
 317      $wrapped->correctfeedback['text'] = '';
 318      $wrapped->correctfeedback['format'] = FORMAT_HTML;
 319      $wrapped->correctfeedback['itemid'] = '';
 320      $wrapped->partiallycorrectfeedback['text'] = '';
 321      $wrapped->partiallycorrectfeedback['format'] = FORMAT_HTML;
 322      $wrapped->partiallycorrectfeedback['itemid'] = '';
 323      $wrapped->incorrectfeedback['text'] = '';
 324      $wrapped->incorrectfeedback['format'] = FORMAT_HTML;
 325      $wrapped->incorrectfeedback['itemid'] = '';
 326  }
 327  
 328  function qtype_multianswer_extract_question($text) {
 329      // Variable $text is an array [text][format][itemid].
 330      $question = new stdClass();
 331      $question->qtype = 'multianswer';
 332      $question->questiontext = $text;
 333      $question->generalfeedback['text'] = '';
 334      $question->generalfeedback['format'] = FORMAT_HTML;
 335      $question->generalfeedback['itemid'] = '';
 336  
 337      $question->options = new stdClass();
 338      $question->options->questions = array();
 339      $question->defaultmark = 0; // Will be increased for each answer norm.
 340  
 341      for ($positionkey = 1;
 342              preg_match('/'.ANSWER_REGEX.'/s', $question->questiontext['text'], $answerregs);
 343              ++$positionkey) {
 344          $wrapped = new stdClass();
 345          $wrapped->generalfeedback['text'] = '';
 346          $wrapped->generalfeedback['format'] = FORMAT_HTML;
 347          $wrapped->generalfeedback['itemid'] = '';
 348          if (isset($answerregs[ANSWER_REGEX_NORM]) && $answerregs[ANSWER_REGEX_NORM] !== '') {
 349              $wrapped->defaultmark = $answerregs[ANSWER_REGEX_NORM];
 350          } else {
 351              $wrapped->defaultmark = '1';
 352          }
 353          if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_NUMERICAL])) {
 354              $wrapped->qtype = 'numerical';
 355              $wrapped->multiplier = array();
 356              $wrapped->units      = array();
 357              $wrapped->instructions['text'] = '';
 358              $wrapped->instructions['format'] = FORMAT_HTML;
 359              $wrapped->instructions['itemid'] = '';
 360          } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER])) {
 361              $wrapped->qtype = 'shortanswer';
 362              $wrapped->usecase = 0;
 363          } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER_C])) {
 364              $wrapped->qtype = 'shortanswer';
 365              $wrapped->usecase = 1;
 366          } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE])) {
 367              qtype_multianswer_initialise_multichoice_subquestion($wrapped);
 368              $wrapped->shuffleanswers = 0;
 369              $wrapped->layout = qtype_multichoice_base::LAYOUT_DROPDOWN;
 370          } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_SHUFFLED])) {
 371              qtype_multianswer_initialise_multichoice_subquestion($wrapped);
 372              $wrapped->shuffleanswers = 1;
 373              $wrapped->layout = qtype_multichoice_base::LAYOUT_DROPDOWN;
 374          } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_REGULAR])) {
 375              qtype_multianswer_initialise_multichoice_subquestion($wrapped);
 376              $wrapped->shuffleanswers = 0;
 377              $wrapped->layout = qtype_multichoice_base::LAYOUT_VERTICAL;
 378          } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_REGULAR_SHUFFLED])) {
 379              qtype_multianswer_initialise_multichoice_subquestion($wrapped);
 380              $wrapped->shuffleanswers = 1;
 381              $wrapped->layout = qtype_multichoice_base::LAYOUT_VERTICAL;
 382          } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_HORIZONTAL])) {
 383              qtype_multianswer_initialise_multichoice_subquestion($wrapped);
 384              $wrapped->shuffleanswers = 0;
 385              $wrapped->layout = qtype_multichoice_base::LAYOUT_HORIZONTAL;
 386          } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_HORIZONTAL_SHUFFLED])) {
 387              qtype_multianswer_initialise_multichoice_subquestion($wrapped);
 388              $wrapped->shuffleanswers = 1;
 389              $wrapped->layout = qtype_multichoice_base::LAYOUT_HORIZONTAL;
 390          } else {
 391              print_error('unknownquestiontype', 'question', '', $answerregs[2]);
 392              return false;
 393          }
 394  
 395          // Each $wrapped simulates a $form that can be processed by the
 396          // respective save_question and save_question_options methods of the
 397          // wrapped questiontypes.
 398          $wrapped->answer   = array();
 399          $wrapped->fraction = array();
 400          $wrapped->feedback = array();
 401          $wrapped->questiontext['text'] = $answerregs[0];
 402          $wrapped->questiontext['format'] = FORMAT_HTML;
 403          $wrapped->questiontext['itemid'] = '';
 404          $answerindex = 0;
 405  
 406          $remainingalts = $answerregs[ANSWER_REGEX_ALTERNATIVES];
 407          while (preg_match('/~?'.ANSWER_ALTERNATIVE_REGEX.'/s', $remainingalts, $altregs)) {
 408              if ('=' == $altregs[ANSWER_ALTERNATIVE_REGEX_FRACTION]) {
 409                  $wrapped->fraction["{$answerindex}"] = '1';
 410              } else if ($percentile = $altregs[ANSWER_ALTERNATIVE_REGEX_PERCENTILE_FRACTION]) {
 411                  $wrapped->fraction["{$answerindex}"] = .01 * $percentile;
 412              } else {
 413                  $wrapped->fraction["{$answerindex}"] = '0';
 414              }
 415              if (isset($altregs[ANSWER_ALTERNATIVE_REGEX_FEEDBACK])) {
 416                  $feedback = html_entity_decode(
 417                          $altregs[ANSWER_ALTERNATIVE_REGEX_FEEDBACK], ENT_QUOTES, 'UTF-8');
 418                  $feedback = str_replace('\}', '}', $feedback);
 419                  $wrapped->feedback["{$answerindex}"]['text'] = str_replace('\#', '#', $feedback);
 420                  $wrapped->feedback["{$answerindex}"]['format'] = FORMAT_HTML;
 421                  $wrapped->feedback["{$answerindex}"]['itemid'] = '';
 422              } else {
 423                  $wrapped->feedback["{$answerindex}"]['text'] = '';
 424                  $wrapped->feedback["{$answerindex}"]['format'] = FORMAT_HTML;
 425                  $wrapped->feedback["{$answerindex}"]['itemid'] = '';
 426  
 427              }
 428              if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_NUMERICAL])
 429                      && preg_match('~'.NUMERICAL_ALTERNATIVE_REGEX.'~s',
 430                              $altregs[ANSWER_ALTERNATIVE_REGEX_ANSWER], $numregs)) {
 431                  $wrapped->answer[] = $numregs[NUMERICAL_CORRECT_ANSWER];
 432                  if (array_key_exists(NUMERICAL_ABS_ERROR_MARGIN, $numregs)) {
 433                      $wrapped->tolerance["{$answerindex}"] =
 434                      $numregs[NUMERICAL_ABS_ERROR_MARGIN];
 435                  } else {
 436                      $wrapped->tolerance["{$answerindex}"] = 0;
 437                  }
 438              } else { // Tolerance can stay undefined for non numerical questions.
 439                  // Undo quoting done by the HTML editor.
 440                  $answer = html_entity_decode(
 441                          $altregs[ANSWER_ALTERNATIVE_REGEX_ANSWER], ENT_QUOTES, 'UTF-8');
 442                  $answer = str_replace('\}', '}', $answer);
 443                  $wrapped->answer["{$answerindex}"] = str_replace('\#', '#', $answer);
 444                  if ($wrapped->qtype == 'multichoice') {
 445                      $wrapped->answer["{$answerindex}"] = array(
 446                              'text' => $wrapped->answer["{$answerindex}"],
 447                              'format' => FORMAT_HTML,
 448                              'itemid' => '');
 449                  }
 450              }
 451              $tmp = explode($altregs[0], $remainingalts, 2);
 452              $remainingalts = $tmp[1];
 453              $answerindex++;
 454          }
 455  
 456          $question->defaultmark += $wrapped->defaultmark;
 457          $question->options->questions[$positionkey] = clone($wrapped);
 458          $question->questiontext['text'] = implode("{#$positionkey}",
 459                      explode($answerregs[0], $question->questiontext['text'], 2));
 460      }
 461      return $question;
 462  }


Generated: Thu Aug 11 10:00:09 2016 Cross-referenced by PHPXref 0.7.1