[ Index ]

PHP Cross Reference of Moodle 1.9.3 [Build 15-Oct-2008]

title

Body

[close]

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

   1  <?php  // $Id: questiontype.php,v 1.41.2.11 2008/09/03 15:26:04 pichetp Exp $
   2  
   3  ///////////////////
   4  /// MULTIANSWER /// (Embedded - cloze)
   5  ///////////////////
   6  
   7  ///
   8  /// The multianswer question type is special in that it
   9  /// depends on a few other question types, i.e.
  10  /// 'multichoice', 'shortanswer' and 'numerical'.
  11  /// These question types have got a few special features that
  12  /// makes them useable by the 'multianswer' question type
  13  ///
  14  
  15  /// QUESTION TYPE CLASS //////////////////
  16  /**
  17   * @package questionbank
  18   * @subpackage questiontypes
  19   */
  20  class embedded_cloze_qtype extends default_questiontype {
  21  
  22      function name() {
  23          return 'multianswer';
  24      }
  25  
  26      function get_question_options(&$question) {
  27          global $QTYPES;
  28  
  29          // Get relevant data indexed by positionkey from the multianswers table
  30          if (!$sequence = get_field('question_multianswer', 'sequence', 'question', $question->id)) {
  31              notify(get_string('noquestions','qtype_multianswer',$question->name));
  32              $question->options->questions['1']= '';
  33              return true ;
  34          }
  35  
  36          $wrappedquestions = get_records_list('question', 'id', $sequence, 'id ASC');
  37  
  38          // We want an array with question ids as index and the positions as values
  39          $sequence = array_flip(explode(',', $sequence));
  40          array_walk($sequence, create_function('&$val', '$val++;'));
  41          //If a question is lost, the corresponding index is null
  42          // so this null convention is used to test $question->options->questions 
  43          // before using the values.
  44          // first all possible questions from sequence are nulled 
  45          // then filled with the data if available in  $wrappedquestions
  46          $nbvaliquestion = 0 ;
  47          foreach($sequence as $seq){
  48              $question->options->questions[$seq]= '';
  49          }
  50          if (isset($wrappedquestions) && is_array($wrappedquestions)){
  51              foreach ($wrappedquestions as $wrapped) {
  52                  if (!$QTYPES[$wrapped->qtype]->get_question_options($wrapped)) {
  53                      notify("Unable to get options for questiontype {$wrapped->qtype} (id={$wrapped->id})");                
  54                  }else {
  55                      // for wrapped questions the maxgrade is always equal to the defaultgrade,
  56                      // there is no entry in the question_instances table for them
  57                      $wrapped->maxgrade = $wrapped->defaultgrade;
  58                      $nbvaliquestion++ ;
  59                      $question->options->questions[$sequence[$wrapped->id]] = clone($wrapped); // ??? Why do we need a clone here?
  60                  }
  61              }
  62          }
  63          if ($nbvaliquestion == 0 ) {
  64              notify(get_string('noquestions','qtype_multianswer',$question->name));
  65          }
  66          return true;
  67      }
  68  
  69      function save_question_options($question) {
  70          global $QTYPES;
  71          $result = new stdClass;
  72  
  73          // This function needs to be able to handle the case where the existing set of wrapped
  74          // questions does not match the new set of wrapped questions so that some need to be
  75          // created, some modified and some deleted
  76          // Unfortunately the code currently simply overwrites existing ones in sequence. This
  77          // will make re-marking after a re-ordering of wrapped questions impossible and
  78          // will also create difficulties if questiontype specific tables reference the id.
  79  
  80          // First we get all the existing wrapped questions
  81          if (!$oldwrappedids = get_field('question_multianswer', 'sequence', 'question', $question->id)) {
  82              $oldwrappedquestions = array();
  83          } else {
  84              $oldwrappedquestions = get_records_list('question', 'id', $oldwrappedids, 'id ASC');
  85          }
  86          $sequence = array();
  87          foreach($question->options->questions as $wrapped) {
  88              if ($wrapped != ''){
  89              // if we still have some old wrapped question ids, reuse the next of them
  90  
  91                  if (is_array($oldwrappedquestions) && $oldwrappedquestion = array_shift($oldwrappedquestions)) {
  92                      $wrapped->id = $oldwrappedquestion->id;
  93                      if($oldwrappedquestion->qtype != $wrapped->qtype ) {
  94                          switch ($oldwrappedquestion->qtype) {
  95                          case 'multichoice':
  96                                   delete_records('question_multichoice', 'question' , $oldwrappedquestion->id );
  97                              break;
  98                          case 'shortanswer':
  99                                   delete_records('question_shortanswer', 'question' , $oldwrappedquestion->id );
 100                              break;
 101                          case 'numerical':
 102                                   delete_records('question_numerical', 'question' , $oldwrappedquestion->id );
 103                              break;
 104                          default:
 105                                  print_error('qtypenotrecognized', 'qtype_multianswer','',$oldwrappedquestion->qtype);
 106                                          $wrapped->id = 0 ;
 107                              }
 108                      }
 109                  }else {
 110                      $wrapped->id = 0 ;
 111                  }
 112              }
 113              $wrapped->name = $question->name;
 114              $wrapped->parent = $question->id;
 115              $wrapped->category = $question->category . ',1'; // save_question strips this extra bit off again.
 116              $wrapped = $QTYPES[$wrapped->qtype]->save_question($wrapped,
 117                      $wrapped, $question->course);
 118              $sequence[] = $wrapped->id;
 119          }
 120  
 121          // Delete redundant wrapped questions
 122          if(is_array($oldwrappedids) && count($oldwrappedids)){
 123              foreach ($oldwrappedids as $id) {
 124                  delete_question($id) ;
 125              }
 126          }
 127  
 128          if (!empty($sequence)) {
 129              $multianswer = new stdClass;
 130              $multianswer->question = $question->id;
 131              $multianswer->sequence = implode(',', $sequence);
 132              if ($oldid = get_field('question_multianswer', 'id', 'question', $question->id)) {
 133                  $multianswer->id = $oldid;
 134                  if (!update_record("question_multianswer", $multianswer)) {
 135                      $result->error = "Could not update cloze question options! " .
 136                              "(id=$multianswer->id)";
 137                      return $result;
 138                  }
 139              } else {
 140                  if (!insert_record("question_multianswer", $multianswer)) {
 141                      $result->error = "Could not insert cloze question options!";
 142                      return $result;
 143                  }
 144              }
 145          }
 146      }
 147  
 148      function save_question($authorizedquestion, $form, $course) {
 149          $question = qtype_multianswer_extract_question($form->questiontext);
 150          if (isset($authorizedquestion->id)) {
 151              $question->id = $authorizedquestion->id;
 152          }
 153  
 154  
 155          $question->category = $authorizedquestion->category;
 156          $form->course = $course; // To pass the course object to
 157                                   // save_question_options, where it is
 158                                   // needed to call type specific
 159                                   // save_question methods.
 160          $form->defaultgrade = $question->defaultgrade;
 161          $form->questiontext = $question->questiontext;
 162          $form->questiontextformat = 0;
 163          $form->options = clone($question->options);
 164          unset($question->options);
 165          return parent::save_question($question, $form, $course);
 166      }
 167  
 168      function create_session_and_responses(&$question, &$state, $cmoptions, $attempt) {
 169          $state->responses = array();
 170          foreach ($question->options->questions as $key => $wrapped) {
 171              $state->responses[$key] = '';
 172          }
 173          return true;
 174      }
 175  
 176      function restore_session_and_responses(&$question, &$state) {
 177          $responses = explode(',', $state->responses['']);
 178          $state->responses = array();
 179          foreach ($responses as $response) {
 180              $tmp = explode("-", $response);
 181              // restore encoded characters
 182              $state->responses[$tmp[0]] = str_replace(array("&#0044;", "&#0045;"),
 183                      array(",", "-"), $tmp[1]);
 184          }
 185          return true;
 186      }
 187  
 188      function save_session_and_responses(&$question, &$state) {
 189          $responses = $state->responses;
 190          // encode - (hyphen) and , (comma) to &#0045; because they are used as
 191          // delimiters
 192          array_walk($responses, create_function('&$val, $key',
 193                  '$val = str_replace(array(",", "-"), array("&#0044;", "&#0045;"), $val);
 194                  $val = "$key-$val";'));
 195          $responses = implode(',', $responses);
 196  
 197          // Set the legacy answer field
 198          if (!set_field('question_states', 'answer', $responses, 'id', $state->id)) {
 199              return false;
 200          }
 201          return true;
 202      }
 203  
 204      /**
 205      * Deletes question from the question-type specific tables
 206      *
 207      * @return boolean Success/Failure
 208      * @param object $question  The question being deleted
 209      */
 210      function delete_question($questionid) {
 211          delete_records("question_multianswer", "question", $questionid);
 212          return true;
 213      }
 214  
 215      function get_correct_responses(&$question, &$state) {
 216          global $QTYPES;
 217          $responses = array();
 218          foreach($question->options->questions as $key => $wrapped) {
 219              if ($wrapped != ''){
 220              if ($correct = $QTYPES[$wrapped->qtype]->get_correct_responses($wrapped, $state)) {
 221                  $responses[$key] = $correct[''];
 222              } else {
 223                  // if there is no correct answer to this subquestion then there
 224                  // can not be a correct answer to the whole question either, so
 225                  // we have to return null.
 226                  return null;
 227              }
 228          }
 229          }
 230          return $responses;
 231      }
 232  
 233      function print_question_formulation_and_controls(&$question, &$state, $cmoptions, $options) {
 234  
 235          global $QTYPES, $CFG, $USER;
 236          $readonly = empty($options->readonly) ? '' : 'readonly="readonly"';
 237          $disabled = empty($options->readonly) ? '' : 'disabled="disabled"';
 238          $formatoptions = new stdClass;
 239          $formatoptions->noclean = true;
 240          $formatoptions->para = false;
 241          $nameprefix = $question->name_prefix;
 242  
 243          // adding an icon with alt to warn user this is a fill in the gap question
 244          // MDL-7497
 245          if (!empty($USER->screenreader)) {
 246              echo "<img src=\"$CFG->wwwroot/question/type/$question->qtype/icon.gif\" ".
 247                  "class=\"icon\" alt=\"".get_string('clozeaid','qtype_multichoice')."\" />  ";
 248          }
 249  
 250          echo '<div class="ablock clearfix">';
 251          // For this question type, we better print the image on top:
 252          if ($image = get_question_image($question)) {
 253              echo('<img class="qimage" src="' . $image . '" alt="" /><br />');
 254          }
 255  
 256          $qtextremaining = format_text($question->questiontext,
 257                  $question->questiontextformat, $formatoptions, $cmoptions->course);
 258  
 259          $strfeedback = get_string('feedback', 'quiz');
 260  
 261          // The regex will recognize text snippets of type {#X}
 262          // where the X can be any text not containg } or white-space characters.
 263  
 264          while (ereg('\{#([^[:space:]}]*)}', $qtextremaining, $regs)) {
 265              $qtextsplits = explode($regs[0], $qtextremaining, 2);
 266              echo "<label>"; // MDL-7497
 267              echo $qtextsplits[0];
 268              $qtextremaining = $qtextsplits[1];
 269  
 270              $positionkey = $regs[1];
 271              if (isset($question->options->questions[$positionkey]) && $question->options->questions[$positionkey] != ''){
 272              $wrapped = &$question->options->questions[$positionkey];
 273              $answers = &$wrapped->options->answers;
 274             // $correctanswers = $QTYPES[$wrapped->qtype]->get_correct_responses($wrapped, $state);
 275  
 276              $inputname = $nameprefix.$positionkey;
 277              if (isset($state->responses[$positionkey])) {
 278                  $response = $state->responses[$positionkey];
 279              } else {
 280                  $response = null;
 281              }
 282  
 283              // Determine feedback popup if any
 284              $popup = '';
 285              $style = '';
 286              $feedbackimg = '';
 287              $feedback = '' ;
 288              $correctanswer = '';
 289              $strfeedbackwrapped  = $strfeedback;
 290             // if($wrapped->qtype == 'numerical' ||$wrapped->qtype == 'shortanswer'){
 291                  $testedstate = clone($state);
 292                  if ($correctanswers =  $QTYPES[$wrapped->qtype]->get_correct_responses($wrapped, $testedstate)) {
 293                      if ($options->readonly && $options->correct_responses) {
 294                          $delimiter = '';
 295                          if ($correctanswers) {
 296                              foreach ($correctanswers as $ca) {
 297                                  switch($wrapped->qtype){
 298                                      case 'numerical':
 299                                      case 'shortanswer':
 300                                          $correctanswer .= $delimiter.$ca;
 301                                          break ;
 302                                      case 'multichoice':
 303                                          if (isset($answers[$ca])){
 304                                              $correctanswer .= $delimiter.$answers[$ca]->answer;
 305                                          }
 306                                          break ;
 307                                  }
 308                                  $delimiter = ', ';
 309                              }
 310                          }
 311                      }
 312                      if ($correctanswer) {
 313                          $feedback = '<div class="correctness">';
 314                          $feedback .= get_string('correctansweris', 'quiz', s($correctanswer, true));
 315                          $feedback .= '</div>';
 316                         // $strfeedbackwrapped = get_string('correctanswer and', 'quiz').get_string('feedback', 'quiz');
 317                      }
 318                  }
 319              if ($options->feedback) {
 320                  $chosenanswer = null;
 321                  switch ($wrapped->qtype) {
 322                      case 'numerical':
 323                      case 'shortanswer':
 324                          $testedstate = clone($state);
 325                          $testedstate->responses[''] = $response;
 326                          foreach ($answers as $answer) {
 327                              if($QTYPES[$wrapped->qtype]
 328                                      ->test_response($wrapped, $testedstate, $answer)) {
 329                                  $chosenanswer = clone($answer);
 330                                  break;
 331                              }
 332                          }
 333                          break;
 334                      case 'multichoice':
 335                          if (isset($answers[$response])) {
 336                              $chosenanswer = clone($answers[$response]);
 337                          }
 338                          break;
 339                      default:
 340                          break;
 341                  }
 342  
 343                  // Set up a default chosenanswer so that all non-empty wrong
 344                  // answers are highlighted red
 345                  if (empty($chosenanswer) && !empty($response)) {
 346                      $chosenanswer = new stdClass;
 347                      $chosenanswer->fraction = 0.0;
 348                  }
 349  
 350                  if (!empty($chosenanswer->feedback)) {
 351                      $feedback = s(str_replace(array("\\", "'"), array("\\\\", "\\'"), $feedback.$chosenanswer->feedback));
 352                      if  ($options->readonly && $options->correct_responses) {
 353                          $strfeedbackwrapped = get_string('correctanswerandfeedback', 'qtype_multianswer');
 354                      }else {
 355                          $strfeedbackwrapped = get_string('feedback', 'quiz');
 356                      }
 357                      $popup = " onmouseover=\"return overlib('$feedback', STICKY, MOUSEOFF, CAPTION, '$strfeedbackwrapped', FGCOLOR, '#FFFFFF');\" ".
 358                               " onmouseout=\"return nd();\" ";
 359                  }
 360  
 361                  /// Determine style
 362                  if ($options->feedback && $response != '') {
 363                      $style = 'class = "'.question_get_feedback_class($chosenanswer->fraction).'"';
 364                      $feedbackimg = question_get_feedback_image($chosenanswer->fraction);
 365                  } else {
 366                      $style = '';
 367                      $feedbackimg = '';
 368                  }
 369              }
 370              if ($feedback !='' && $popup == ''){
 371                  $strfeedbackwrapped = get_string('correctanswer', 'qtype_multianswer');
 372                      $feedback = s(str_replace(array("\\", "'"), array("\\\\", "\\'"), $feedback));
 373                      $popup = " onmouseover=\"return overlib('$feedback', STICKY, MOUSEOFF, CAPTION, '$strfeedbackwrapped', FGCOLOR, '#FFFFFF');\" ".
 374                               " onmouseout=\"return nd();\" ";
 375              }
 376  
 377              // Print the input control
 378              switch ($wrapped->qtype) {
 379                  case 'shortanswer':
 380                  case 'numerical':
 381                      $size = 1 ;
 382                      foreach ($answers as $answer) {
 383                          if (strlen(trim($answer->answer)) > $size ){
 384                              $size = strlen(trim($answer->answer));
 385                          }
 386                      }
 387                      if (strlen(trim($response))> $size ){
 388                              $size = strlen(trim($response))+1;
 389                      }
 390                      $size = $size + rand(0,$size*0.15);
 391                      $size > 60 ? $size = 60 : $size = $size;
 392                      $styleinfo = "size=\"$size\"";
 393                      /**
 394                      * Uncomment the following lines if you want to limit for small sizes.
 395                      * Results may vary with browsers see MDL-3274
 396                      */
 397                      /*
 398                      if ($size < 2) {
 399                          $styleinfo = 'style="width: 1.1em;"';
 400                      }
 401                      if ($size == 2) {
 402                          $styleinfo = 'style="width: 1.9em;"';
 403                      }
 404                      if ($size == 3) {
 405                          $styleinfo = 'style="width: 2.3em;"';
 406                      }
 407                      if ($size == 4) {
 408                          $styleinfo = 'style="width: 2.8em;"';
 409                      }
 410                      */
 411  
 412                      echo "<input $style $readonly $popup name=\"$inputname\"";
 413                      echo "  type=\"text\" value=\"".s($response, true)."\" ".$styleinfo." /> ";
 414                      if (!empty($feedback) && !empty($USER->screenreader)) {
 415                          echo "<img src=\"$CFG->pixpath/i/feedback.gif\" alt=\"$feedback\" />";
 416                      }
 417                      echo $feedbackimg;
 418                      break;
 419                  case 'multichoice':
 420                   if ($wrapped->options->layout == 0 ){
 421                      $outputoptions = '<option></option>'; // Default empty option
 422                      foreach ($answers as $mcanswer) {
 423                          $selected = '';
 424                          if ($response == $mcanswer->id) {
 425                              $selected = ' selected="selected"';
 426                          }
 427                          $outputoptions .= "<option value=\"$mcanswer->id\"$selected>" .
 428                                  s($mcanswer->answer, true) . '</option>';
 429                      }
 430                      // In the next line, $readonly is invalid HTML, but it works in
 431                      // all browsers. $disabled would be valid, but then the JS for
 432                      // displaying the feedback does not work. Of course, we should
 433                      // not be relying on JS (for accessibility reasons), but that is
 434                      // a bigger problem.
 435                      //
 436                      // The span is used for safari, which does not allow styling of
 437                      // selects.
 438                      echo "<span $style><select $popup $readonly $style name=\"$inputname\">";
 439                      echo $outputoptions;
 440                      echo '</select></span>';
 441                      if (!empty($feedback) && !empty($USER->screenreader)) {
 442                          echo "<img src=\"$CFG->pixpath/i/feedback.gif\" alt=\"$feedback\" />";
 443                      }
 444                      echo $feedbackimg;
 445                      }else if ($wrapped->options->layout == 1 || $wrapped->options->layout == 2){
 446                          $ordernumber=0;
 447                          $anss =  Array();
 448                          foreach ($answers as $mcanswer) {
 449                              $ordernumber++;
 450                              $checked = '';
 451                              $chosen = false;
 452                              $type = 'type="radio"';
 453                              $name   = "name=\"{$inputname}\"";
 454                              if ($response == $mcanswer->id) {
 455                                  $checked = 'checked="checked"';
 456                                  $chosen = true;
 457                              }
 458                              $a = new stdClass;
 459                              $a->id   = $question->name_prefix . $mcanswer->id;
 460                              $a->class = '';
 461                              $a->feedbackimg = '';
 462          
 463                      // Print the control
 464                      $a->control = "<input $readonly id=\"$a->id\" $name $checked $type value=\"$mcanswer->id\" />";
 465                  if ($options->correct_responses && $mcanswer->fraction > 0) {
 466                      $a->class = question_get_feedback_class(1);
 467                  }
 468                  if (($options->feedback && $chosen) || $options->correct_responses) {
 469                      if ($type == ' type="checkbox" ') {
 470                          $a->feedbackimg = question_get_feedback_image($mcanswer->fraction > 0 ? 1 : 0, $chosen && $options->feedback);
 471                      } else {
 472                          $a->feedbackimg = question_get_feedback_image($mcanswer->fraction, $chosen && $options->feedback);
 473                      }
 474                  }
 475      
 476                  // Print the answer text
 477                  $a->text = '<span class="anun">' . $ordernumber . '<span class="anumsep">.</span></span> ' .
 478                          format_text($mcanswer->answer, FORMAT_MOODLE, $formatoptions, $cmoptions->course);
 479      
 480                  // Print feedback if feedback is on
 481                  if (($options->feedback || $options->correct_responses) && ($checked )) { //|| $options->readonly
 482                      $a->feedback = format_text($mcanswer->feedback, true, $formatoptions, $cmoptions->course);
 483                  } else {
 484                      $a->feedback = '';
 485                  }
 486      
 487                      $anss[] = clone($a);
 488                  }
 489                  ?>
 490              <?php    if ($wrapped->options->layout == 1 ){
 491              ?>
 492                    <table class="answer">
 493                      <?php $row = 1; foreach ($anss as $answer) { ?>
 494                        <tr class="<?php echo 'r'.$row = $row ? 0 : 1; ?>">
 495                          <td class="c0 control">
 496                            <?php echo $answer->control; ?>
 497                          </td>
 498                          <td class="c1 text <?php echo $answer->class ?>">
 499                            <label for="<?php echo $answer->id ?>">
 500                              <?php echo $answer->text; ?>
 501                              <?php echo $answer->feedbackimg; ?>
 502                            </label>
 503                          </td>
 504                          <td class="c0 feedback">
 505                            <?php echo $answer->feedback; ?>
 506                          </td>
 507                        </tr>
 508                      <?php } ?>
 509                    </table>
 510                    <?php }else  if ($wrapped->options->layout == 2 ){
 511                      ?>
 512             
 513                    <table class="answer">
 514                        <tr class="<?php echo 'r'.$row = $row ? 0 : 1; ?>">
 515                      <?php $row = 1; foreach ($anss as $answer) { ?>
 516                          <td class="c0 control">
 517                            <?php echo $answer->control; ?>
 518                          </td>
 519                          <td class="c1 text <?php echo $answer->class ?>">
 520                            <label for="<?php echo $answer->id ?>">
 521                              <?php echo $answer->text; ?>
 522                              <?php echo $answer->feedbackimg; ?>
 523                            </label>
 524                          </td>
 525                          <td class="c0 feedback">
 526                            <?php echo $answer->feedback; ?>
 527                          </td>
 528                      <?php } ?>
 529                        </tr>
 530                    </table>
 531                    <?php }  
 532                          
 533                      }else {
 534                          echo "no valid layout";
 535                      }
 536                      
 537                      break;
 538                  default:
 539                      $a = new stdClass;
 540                      $a->type = $wrapped->qtype ; 
 541                      $a->sub = $positionkey;
 542                      print_error('unknownquestiontypeofsubquestion', 'qtype_multianswer','',$a);
 543                      break;
 544             }
 545             echo "</label>"; // MDL-7497
 546          }
 547          else {
 548              if(!  isset($question->options->questions[$positionkey])){
 549                  echo $regs[0]."</label>";
 550              }else {
 551                  echo '</label><div class="error" >'.get_string('questionnotfound','qtype_multianswer',$positionkey).'</div>';
 552              }
 553         }
 554      }
 555  
 556          // Print the final piece of question text:
 557          echo $qtextremaining;
 558          $this->print_question_submit_buttons($question, $state, $cmoptions, $options);
 559          echo '</div>';
 560      }
 561  
 562      function grade_responses(&$question, &$state, $cmoptions) {
 563          global $QTYPES;
 564          $teststate = clone($state);
 565          $state->raw_grade = 0;
 566          foreach($question->options->questions as $key => $wrapped) {
 567              if ($wrapped != ''){
 568              $state->responses[$key] = $state->responses[$key];
 569              $teststate->responses = array('' => $state->responses[$key]);
 570              $teststate->raw_grade = 0;
 571              if (false === $QTYPES[$wrapped->qtype]
 572               ->grade_responses($wrapped, $teststate, $cmoptions)) {
 573                  return false;
 574              }
 575              $state->raw_grade += $teststate->raw_grade;
 576          }
 577          }
 578          $state->raw_grade /= $question->defaultgrade;
 579          $state->raw_grade = min(max((float) $state->raw_grade, 0.0), 1.0)
 580           * $question->maxgrade;
 581  
 582          if (empty($state->raw_grade)) {
 583              $state->raw_grade = 0.0;
 584          }
 585          $state->penalty = $question->penalty * $question->maxgrade;
 586  
 587          // mark the state as graded
 588          $state->event = ($state->event ==  QUESTION_EVENTCLOSE) ? QUESTION_EVENTCLOSEANDGRADE : QUESTION_EVENTGRADE;
 589  
 590          return true;
 591      }
 592  
 593      function get_actual_response($question, $state) {
 594          global $QTYPES;
 595          $teststate = clone($state);
 596          foreach($question->options->questions as $key => $wrapped) {
 597              $state->responses[$key] = html_entity_decode($state->responses[$key]);
 598              $teststate->responses = array('' => $state->responses[$key]);
 599              $correct = $QTYPES[$wrapped->qtype]
 600               ->get_actual_response($wrapped, $teststate);
 601              // change separator here if you want
 602              $responsesseparator = ',';
 603              $responses[$key] = implode($responsesseparator, $correct);
 604          }
 605          return $responses;
 606      }
 607  
 608  /// BACKUP FUNCTIONS ////////////////////////////
 609  
 610      /*
 611       * Backup the data in the question
 612       *
 613       * This is used in question/backuplib.php
 614       */
 615      function backup($bf,$preferences,$question,$level=6) {
 616  
 617          $status = true;
 618  
 619          $multianswers = get_records("question_multianswer","question",$question,"id");
 620          //If there are multianswers
 621          if ($multianswers) {
 622              //Print multianswers header
 623              $status = fwrite ($bf,start_tag("MULTIANSWERS",$level,true));
 624              //Iterate over each multianswer
 625              foreach ($multianswers as $multianswer) {
 626                  $status = fwrite ($bf,start_tag("MULTIANSWER",$level+1,true));
 627                  //Print multianswer contents
 628                  fwrite ($bf,full_tag("ID",$level+2,false,$multianswer->id));
 629                  fwrite ($bf,full_tag("QUESTION",$level+2,false,$multianswer->question));
 630                  fwrite ($bf,full_tag("SEQUENCE",$level+2,false,$multianswer->sequence));
 631                  $status = fwrite ($bf,end_tag("MULTIANSWER",$level+1,true));
 632              }
 633              //Print multianswers footer
 634              $status = fwrite ($bf,end_tag("MULTIANSWERS",$level,true));
 635              //Now print question_answers
 636              $status = question_backup_answers($bf,$preferences,$question);
 637          }
 638          return $status;
 639      }
 640  
 641  /// RESTORE FUNCTIONS /////////////////
 642  
 643      /*
 644       * Restores the data in the question
 645       *
 646       * This is used in question/restorelib.php
 647       */
 648      function restore($old_question_id,$new_question_id,$info,$restore) {
 649  
 650          $status = true;
 651  
 652          //Get the multianswers array
 653          $multianswers = $info['#']['MULTIANSWERS']['0']['#']['MULTIANSWER'];
 654          //Iterate over multianswers
 655          for($i = 0; $i < sizeof($multianswers); $i++) {
 656              $mul_info = $multianswers[$i];
 657  
 658              //We need this later
 659              $oldid = backup_todb($mul_info['#']['ID']['0']['#']);
 660  
 661              //Now, build the question_multianswer record structure
 662              $multianswer = new stdClass;
 663              $multianswer->question = $new_question_id;
 664              $multianswer->sequence = backup_todb($mul_info['#']['SEQUENCE']['0']['#']);
 665  
 666              //We have to recode the sequence field (a list of question ids)
 667              //Extracts question id from sequence
 668              $sequence_field = "";
 669              $in_first = true;
 670              $tok = strtok($multianswer->sequence,",");
 671              while ($tok) {
 672                  //Get the answer from backup_ids
 673                  $question = backup_getid($restore->backup_unique_code,"question",$tok);
 674                  if ($question) {
 675                      if ($in_first) {
 676                          $sequence_field .= $question->new_id;
 677                          $in_first = false;
 678                      } else {
 679                          $sequence_field .= ",".$question->new_id;
 680                      }
 681                  }
 682                  //check for next
 683                  $tok = strtok(",");
 684              }
 685              //We have the answers field recoded to its new ids
 686              $multianswer->sequence = $sequence_field;
 687              //The structure is equal to the db, so insert the question_multianswer
 688              $newid = insert_record("question_multianswer", $multianswer);
 689  
 690              //Save ids in backup_ids
 691              if ($newid) {
 692                  backup_putid($restore->backup_unique_code,"question_multianswer",
 693                               $oldid, $newid);
 694              }
 695  
 696              //Do some output
 697              if (($i+1) % 50 == 0) {
 698                  if (!defined('RESTORE_SILENTLY')) {
 699                      echo ".";
 700                      if (($i+1) % 1000 == 0) {
 701                          echo "<br />";
 702                      }
 703                  }
 704                  backup_flush(300);
 705              }
 706          }
 707  
 708          return $status;
 709      }
 710  
 711      function restore_map($old_question_id,$new_question_id,$info,$restore) {
 712  
 713          $status = true;
 714  
 715          //Get the multianswers array
 716          $multianswers = $info['#']['MULTIANSWERS']['0']['#']['MULTIANSWER'];
 717          //Iterate over multianswers
 718          for($i = 0; $i < sizeof($multianswers); $i++) {
 719              $mul_info = $multianswers[$i];
 720  
 721              //We need this later
 722              $oldid = backup_todb($mul_info['#']['ID']['0']['#']);
 723  
 724              //Now, build the question_multianswer record structure
 725              $multianswer->question = $new_question_id;
 726              $multianswer->answers = backup_todb($mul_info['#']['ANSWERS']['0']['#']);
 727              $multianswer->positionkey = backup_todb($mul_info['#']['POSITIONKEY']['0']['#']);
 728              $multianswer->answertype = backup_todb($mul_info['#']['ANSWERTYPE']['0']['#']);
 729              $multianswer->norm = backup_todb($mul_info['#']['NORM']['0']['#']);
 730  
 731              //If we are in this method is because the question exists in DB, so its
 732              //multianswer must exist too.
 733              //Now, we are going to look for that multianswer in DB and to create the
 734              //mappings in backup_ids to use them later where restoring states (user level).
 735  
 736              //Get the multianswer from DB (by question and positionkey)
 737              $db_multianswer = get_record ("question_multianswer","question",$new_question_id,
 738                                                        "positionkey",$multianswer->positionkey);
 739              //Do some output
 740              if (($i+1) % 50 == 0) {
 741                  if (!defined('RESTORE_SILENTLY')) {
 742                      echo ".";
 743                      if (($i+1) % 1000 == 0) {
 744                          echo "<br />";
 745                      }
 746                  }
 747                  backup_flush(300);
 748              }
 749  
 750              //We have the database multianswer, so update backup_ids
 751              if ($db_multianswer) {
 752                  //We have the newid, update backup_ids
 753                  backup_putid($restore->backup_unique_code,"question_multianswer",$oldid,
 754                               $db_multianswer->id);
 755              } else {
 756                  $status = false;
 757              }
 758          }
 759  
 760          return $status;
 761      }
 762  
 763      function restore_recode_answer($state, $restore) {
 764          //The answer is a comma separated list of hypen separated sequence number and answers. We may have to recode the answers
 765          $answer_field = "";
 766          $in_first = true;
 767          $tok = strtok($state->answer,",");
 768          while ($tok) {
 769              //Extract the multianswer_id and the answer
 770              $exploded = explode("-",$tok);
 771              $seqnum = $exploded[0];
 772              $answer = $exploded[1];
 773              // $sequence is an ordered array of the question ids.
 774              if (!$sequence = get_field('question_multianswer', 'sequence', 'question', $state->question)) {
 775                  print_error('missingoption', 'question', '', $state->question);
 776              }
 777              $sequence = explode(',', $sequence);
 778              // The id of the current question.
 779              $wrappedquestionid = $sequence[$seqnum-1];
 780              // now we can find the question
 781              if (!$wrappedquestion = get_record('question', 'id', $wrappedquestionid)) {
 782                  notify("Can't find the subquestion $wrappedquestionid that is used as part $seqnum in cloze question $state->question");
 783              }
 784              // For multichoice question we need to recode the answer
 785              if ($answer and $wrappedquestion->qtype == 'multichoice') {
 786                  //The answer is an answer_id, look for it in backup_ids
 787                  if (!$ans = backup_getid($restore->backup_unique_code,"question_answers",$answer)) {
 788                      echo 'Could not recode cloze multichoice answer '.$answer.'<br />';
 789                  }
 790                  $answer = $ans->new_id;
 791              }
 792              //build the new answer field for each pair
 793              if ($in_first) {
 794                  $answer_field .= $seqnum."-".$answer;
 795                  $in_first = false;
 796              } else {
 797                  $answer_field .= ",".$seqnum."-".$answer;
 798              }
 799              //check for next
 800              $tok = strtok(",");
 801          }
 802          return $answer_field;
 803      }
 804  
 805      /**
 806       * Runs all the code required to set up and save an essay question for testing purposes.
 807       * Alternate DB table prefix may be used to facilitate data deletion.
 808       */
 809      function generate_test($name, $courseid = null) {
 810          list($form, $question) = parent::generate_test($name, $courseid);
 811          $question->category = $form->category;
 812          $form->questiontext = "This question consists of some text with an answer embedded right here {1:MULTICHOICE:Wrong answer#Feedback for this wrong answer~Another wrong answer#Feedback for the other wrong answer~=Correct answer#Feedback for correct answer~%50%Answer that gives half the credit#Feedback for half credit answer} and right after that you will have to deal with this short answer {1:SHORTANSWER:Wrong answer#Feedback for this wrong answer~=Correct answer#Feedback for correct answer~%50%Answer that gives half the credit#Feedback for half credit answer} and finally we have a floating point number {2:NUMERICAL:=23.8:0.1#Feedback for correct answer 23.8~%50%23.8:2#Feedback for half credit answer in the nearby region of the correct answer}.
 813  
 814  Note that addresses like www.moodle.org and smileys :-) all work as normal:
 815   a) How good is this? {:MULTICHOICE:=Yes#Correct~No#We have a different opinion}
 816   b) What grade would you give it? {3:NUMERICAL:=3:2}
 817  
 818  Good luck!
 819  ";
 820          $form->feedback = "feedback";
 821          $form->generalfeedback = "General feedback";
 822          $form->fraction = 0;
 823          $form->penalty = 0.1;
 824          $form->versioning = 0;
 825  
 826          if ($courseid) {
 827              $course = get_record('course', 'id', $courseid);
 828          }
 829  
 830          return $this->save_question($question, $form, $course);
 831      }
 832  
 833  }
 834  //// END OF CLASS ////
 835  
 836  
 837  //////////////////////////////////////////////////////////////////////////
 838  //// INITIATION - Without this line the question type is not in use... ///
 839  //////////////////////////////////////////////////////////////////////////
 840  question_register_questiontype(new embedded_cloze_qtype());
 841  
 842  /////////////////////////////////////////////////////////////
 843  //// ADDITIONAL FUNCTIONS
 844  //// The functions below deal exclusivly with editing
 845  //// of questions with question type 'multianswer'.
 846  //// Therefore they are kept in this file.
 847  //// They are not in the class as they are not
 848  //// likely to be subject for overriding.
 849  /////////////////////////////////////////////////////////////
 850  
 851  // ANSWER_ALTERNATIVE regexes
 852  define("ANSWER_ALTERNATIVE_FRACTION_REGEX",
 853         '=|%(-?[0-9]+)%');
 854  // for the syntax '(?<!' see http://www.perl.com/doc/manual/html/pod/perlre.html#item_C
 855  define("ANSWER_ALTERNATIVE_ANSWER_REGEX",
 856          '.+?(?<!\\\\|&|&amp;)(?=[~#}]|$)');
 857  define("ANSWER_ALTERNATIVE_FEEDBACK_REGEX",
 858          '.*?(?<!\\\\)(?=[~}]|$)');
 859  define("ANSWER_ALTERNATIVE_REGEX",
 860         '(' . ANSWER_ALTERNATIVE_FRACTION_REGEX .')?' .
 861         '(' . ANSWER_ALTERNATIVE_ANSWER_REGEX . ')' .
 862         '(#(' . ANSWER_ALTERNATIVE_FEEDBACK_REGEX .'))?');
 863  
 864  // Parenthesis positions for ANSWER_ALTERNATIVE_REGEX
 865  define("ANSWER_ALTERNATIVE_REGEX_PERCENTILE_FRACTION", 2);
 866  define("ANSWER_ALTERNATIVE_REGEX_FRACTION", 1);
 867  define("ANSWER_ALTERNATIVE_REGEX_ANSWER", 3);
 868  define("ANSWER_ALTERNATIVE_REGEX_FEEDBACK", 5);
 869  
 870  // NUMBER_FORMATED_ALTERNATIVE_ANSWER_REGEX is used
 871  // for identifying numerical answers in ANSWER_ALTERNATIVE_REGEX_ANSWER
 872  define("NUMBER_REGEX",
 873          '-?(([0-9]+[.,]?[0-9]*|[.,][0-9]+)([eE][-+]?[0-9]+)?)');
 874  define("NUMERICAL_ALTERNATIVE_REGEX",
 875          '^(' . NUMBER_REGEX . ')(:' . NUMBER_REGEX . ')?$');
 876  
 877  // Parenthesis positions for NUMERICAL_FORMATED_ALTERNATIVE_ANSWER_REGEX
 878  define("NUMERICAL_CORRECT_ANSWER", 1);
 879  define("NUMERICAL_ABS_ERROR_MARGIN", 6);
 880  
 881  // Remaining ANSWER regexes
 882  define("ANSWER_TYPE_DEF_REGEX",
 883         '(NUMERICAL|NM)|(MULTICHOICE|MC)|(MULTICHOICE_V|MCV)|(MULTICHOICE_H|MCH)|(SHORTANSWER|SA|MW)');
 884  define("ANSWER_START_REGEX",
 885         '\{([0-9]*):(' . ANSWER_TYPE_DEF_REGEX . '):');
 886  
 887  define("ANSWER_REGEX",
 888          ANSWER_START_REGEX
 889          . '(' . ANSWER_ALTERNATIVE_REGEX
 890          . '(~'
 891          . ANSWER_ALTERNATIVE_REGEX
 892          . ')*)\}' );
 893  
 894  // Parenthesis positions for singulars in ANSWER_REGEX
 895  define("ANSWER_REGEX_NORM", 1);
 896  define("ANSWER_REGEX_ANSWER_TYPE_NUMERICAL", 3);
 897  define("ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE", 4);
 898  define("ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_REGULAR", 5);
 899  define("ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_HORIZONTAL", 6);
 900  define("ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER", 7);
 901  define("ANSWER_REGEX_ALTERNATIVES", 8);
 902  
 903  function qtype_multianswer_extract_question($text) {
 904      $question = new stdClass;
 905      $question->qtype = 'multianswer';
 906      $question->questiontext = $text;
 907      $question->options->questions = array();
 908      $question->defaultgrade = 0; // Will be increased for each answer norm
 909  
 910      for ($positionkey=1
 911          ; preg_match('/'.ANSWER_REGEX.'/', $question->questiontext, $answerregs)
 912          ; ++$positionkey ) {
 913          $wrapped = new stdClass;
 914          $wrapped->defaultgrade = $answerregs[ANSWER_REGEX_NORM]
 915              or $wrapped->defaultgrade = '1';
 916          if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_NUMERICAL])) {
 917              $wrapped->qtype = 'numerical';
 918              $wrapped->multiplier = array();
 919              $wrapped->units      = array();
 920          } else if(!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER])) {
 921              $wrapped->qtype = 'shortanswer';
 922              $wrapped->usecase = 0;
 923          } else if(!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE])) {
 924              $wrapped->qtype = 'multichoice';
 925              $wrapped->single = 1;
 926              $wrapped->answernumbering = 0;
 927              $wrapped->correctfeedback = '';
 928              $wrapped->partiallycorrectfeedback = '';
 929              $wrapped->incorrectfeedback = '';
 930              $wrapped->layout = 0;
 931          } else if(!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_REGULAR])) {
 932              $wrapped->qtype = 'multichoice';
 933              $wrapped->single = 1;
 934              $wrapped->answernumbering = 0;
 935              $wrapped->correctfeedback = '';
 936              $wrapped->partiallycorrectfeedback = '';
 937              $wrapped->incorrectfeedback = '';
 938              $wrapped->layout = 1;
 939          } else if(!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_HORIZONTAL])) {
 940              $wrapped->qtype = 'multichoice';
 941              $wrapped->single = 1;
 942              $wrapped->answernumbering = 0;
 943              $wrapped->correctfeedback = '';
 944              $wrapped->partiallycorrectfeedback = '';
 945              $wrapped->incorrectfeedback = '';
 946              $wrapped->layout = 2;
 947          } else {
 948              print_error('unknownquestiontype', 'question', '', $answerregs[2]);
 949              return false;
 950          }
 951  
 952          // Each $wrapped simulates a $form that can be processed by the
 953          // respective save_question and save_question_options methods of the
 954          // wrapped questiontypes
 955          $wrapped->answer   = array();
 956          $wrapped->fraction = array();
 957          $wrapped->feedback = array();
 958          $wrapped->shuffleanswers = 1;
 959          $wrapped->questiontext = $answerregs[0];
 960          $wrapped->questiontextformat = 0;
 961  
 962          $remainingalts = $answerregs[ANSWER_REGEX_ALTERNATIVES];
 963          while (preg_match('/~?'.ANSWER_ALTERNATIVE_REGEX.'/', $remainingalts, $altregs)) {
 964              if ('=' == $altregs[ANSWER_ALTERNATIVE_REGEX_FRACTION]) {
 965                  $wrapped->fraction[] = '1';
 966              } else if ($percentile = $altregs[ANSWER_ALTERNATIVE_REGEX_PERCENTILE_FRACTION]){
 967                  $wrapped->fraction[] = .01 * $percentile;
 968              } else {
 969                  $wrapped->fraction[] = '0';
 970              }
 971              if (isset($altregs[ANSWER_ALTERNATIVE_REGEX_FEEDBACK])) {
 972                  $feedback = html_entity_decode($altregs[ANSWER_ALTERNATIVE_REGEX_FEEDBACK], ENT_QUOTES, 'UTF-8');
 973                  $feedback = str_replace('\}', '}', $feedback);
 974                  $wrapped->feedback[] = str_replace('\#', '#', $feedback);
 975              } else {
 976                  $wrapped->feedback[] = '';
 977              }
 978              if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_NUMERICAL])
 979                      && ereg(NUMERICAL_ALTERNATIVE_REGEX, $altregs[ANSWER_ALTERNATIVE_REGEX_ANSWER], $numregs)) {
 980                  $wrapped->answer[] = $numregs[NUMERICAL_CORRECT_ANSWER];
 981                  if ($numregs[NUMERICAL_ABS_ERROR_MARGIN]) {
 982                      $wrapped->tolerance[] =
 983                      $numregs[NUMERICAL_ABS_ERROR_MARGIN];
 984                  } else {
 985                      $wrapped->tolerance[] = 0;
 986                  }
 987              } else { // Tolerance can stay undefined for non numerical questions
 988                  // Undo quoting done by the HTML editor.
 989                  $answer = html_entity_decode($altregs[ANSWER_ALTERNATIVE_REGEX_ANSWER], ENT_QUOTES, 'UTF-8');
 990                  $answer = str_replace('\}', '}', $answer);
 991                  $wrapped->answer[] = str_replace('\#', '#', $answer);
 992              }
 993              $tmp = explode($altregs[0], $remainingalts, 2);
 994              $remainingalts = $tmp[1];
 995          }
 996  
 997          $question->defaultgrade += $wrapped->defaultgrade;
 998          $question->options->questions[$positionkey] = clone($wrapped);
 999          $question->questiontext = implode("{#$positionkey}",
1000                      explode($answerregs[0], $question->questiontext, 2));
1001      }
1002      $question->questiontext = $question->questiontext;
1003      return $question;
1004  }
1005  ?>


Generated: Wed Jan 14 11:33:29 2009 Cross-referenced by PHPXref 0.7