| [ Index ] |
PHP Cross Reference of Moodle 1.9.3 [Build 15-Oct-2008] |
[Summary view] [Print] [Text view]
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(",", "-"), 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 - because they are used as 191 // delimiters 192 array_walk($responses, create_function('&$val, $key', 193 '$val = str_replace(array(",", "-"), array(",", "-"), $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 '.+?(?<!\\\\|&|&)(?=[~#}]|$)'); 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 ?>
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated: Wed Jan 14 11:33:29 2009 | Cross-referenced by PHPXref 0.7 |