checking that there remain $oldwrappedids before deleting them lines 87-..
[moodle.git] / question / type / multianswer / questiontype.php
CommitLineData
516cf3eb 1<?php // $Id$
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.
dfa47f96 10/// 'multichoice', 'shortanswer' and 'numerical'.
516cf3eb 11/// These question types have got a few special features that
dfa47f96 12/// makes them useable by the 'multianswer' question type
516cf3eb 13///
14
15/// QUESTION TYPE CLASS //////////////////
1976496e 16/**
17 * @package questionbank
18 * @subpackage questiontypes
7375c542 19 */
7518b645 20class embedded_cloze_qtype extends default_questiontype {
516cf3eb 21
22 function name() {
23 return 'multianswer';
24 }
25
26 function get_question_options(&$question) {
f02c6f01 27 global $QTYPES;
516cf3eb 28
29 // Get relevant data indexed by positionkey from the multianswers table
32a189d6 30 if (!$sequence = get_field('question_multianswer', 'sequence', 'question', $question->id)) {
ccccf04f 31 notify('Error: Cloze question '.$question->id.' is missing question options!');
516cf3eb 32 return false;
33 }
34
f07d1d31 35 $wrappedquestions = get_records_list('question', 'id', $sequence, 'id ASC');
516cf3eb 36
37 // We want an array with question ids as index and the positions as values
38 $sequence = array_flip(explode(',', $sequence));
39 array_walk($sequence, create_function('&$val', '$val++;'));
40
41 foreach ($wrappedquestions as $wrapped) {
f02c6f01 42 if (!$QTYPES[$wrapped->qtype]->get_question_options($wrapped)) {
516cf3eb 43 notify("Unable to get options for questiontype {$wrapped->qtype} (id={$wrapped->id})");
44 }
45 // for wrapped questions the maxgrade is always equal to the defaultgrade,
7518b645 46 // there is no entry in the question_instances table for them
516cf3eb 47 $wrapped->maxgrade = $wrapped->defaultgrade;
9fc3100f 48
516cf3eb 49 $question->options->questions[$sequence[$wrapped->id]] = clone($wrapped); // ??? Why do we need a clone here?
50 }
51
52 return true;
53 }
54
55 function save_question_options($question) {
f02c6f01 56 global $QTYPES;
da298d82 57 $result = new stdClass;
9fc3100f 58
516cf3eb 59 // This function needs to be able to handle the case where the existing set of wrapped
60 // questions does not match the new set of wrapped questions so that some need to be
61 // created, some modified and some deleted
62 // Unfortunately the code currently simply overwrites existing ones in sequence. This
9fc3100f 63 // will make re-marking after a re-ordering of wrapped questions impossible and
516cf3eb 64 // will also create difficulties if questiontype specific tables reference the id.
9fc3100f 65
516cf3eb 66 // First we get all the existing wrapped questions
424dba12 67 if (!$oldwrappedids = get_field('question_multianswer', 'sequence', 'question', $question->id)) {
516cf3eb 68 $oldwrappedids = array();
0a5b58af 69 } else {
70 $oldwrappedids = explode(',', $oldwrappedids);
516cf3eb 71 }
516cf3eb 72 $sequence = array();
73 foreach($question->options->questions as $wrapped) {
74 // if we still have some old wrapped question ids, reuse the next of them
75 if ($oldwrappedid = array_shift($oldwrappedids)) {
76 $wrapped->id = $oldwrappedid;
77 }
77fa3a0d 78 $wrapped->name = $question->name;
79 $wrapped->parent = $question->id;
80fdc53e 80 $wrapped->category = $question->category . ',1'; // save_question strips this extra bit off again.
f02c6f01 81 $wrapped = $QTYPES[$wrapped->qtype]->save_question($wrapped,
77fa3a0d 82 $wrapped, $question->course);
516cf3eb 83 $sequence[] = $wrapped->id;
84 }
85
86 // Delete redundant wrapped questions
4bc4ca50 87 if(is_array($oldwrappedids) && count($oldwrappedids)){
88 $oldwrappedids = implode(',', $oldwrappedids);
89 delete_records_select('question', "id IN ($oldwrappedids)");
90 }
516cf3eb 91
92 if (!empty($sequence)) {
93 $multianswer = new stdClass;
94 $multianswer->question = $question->id;
95 $multianswer->sequence = implode(',', $sequence);
77fa3a0d 96 if ($oldid = get_field('question_multianswer', 'id', 'question', $question->id)) {
516cf3eb 97 $multianswer->id = $oldid;
32a189d6 98 if (!update_record("question_multianswer", $multianswer)) {
ccccf04f 99 $result->error = "Could not update cloze question options! " .
77fa3a0d 100 "(id=$multianswer->id)";
516cf3eb 101 return $result;
102 }
103 } else {
32a189d6 104 if (!insert_record("question_multianswer", $multianswer)) {
ccccf04f 105 $result->error = "Could not insert cloze question options!";
516cf3eb 106 return $result;
107 }
108 }
109 }
110 }
111
112 function save_question($authorizedquestion, $form, $course) {
e51efd7e 113 $question = qtype_multianswer_extract_question($form->questiontext);
516cf3eb 114 if (isset($authorizedquestion->id)) {
115 $question->id = $authorizedquestion->id;
516cf3eb 116 }
117
118
119 $question->category = $authorizedquestion->category;
120 $form->course = $course; // To pass the course object to
121 // save_question_options, where it is
122 // needed to call type specific
123 // save_question methods.
124 $form->defaultgrade = $question->defaultgrade;
125 $form->questiontext = $question->questiontext;
126 $form->questiontextformat = 0;
77fa3a0d 127 $form->options = clone($question->options);
516cf3eb 128 unset($question->options);
129 return parent::save_question($question, $form, $course);
130 }
131
132 function create_session_and_responses(&$question, &$state, $cmoptions, $attempt) {
133 $state->responses = array();
134 foreach ($question->options->questions as $key => $wrapped) {
135 $state->responses[$key] = '';
136 }
137 return true;
138 }
139
140 function restore_session_and_responses(&$question, &$state) {
141 $responses = explode(',', $state->responses['']);
142 $state->responses = array();
143 foreach ($responses as $response) {
144 $tmp = explode("-", $response);
145 // restore encoded characters
77fa3a0d 146 $state->responses[$tmp[0]] = str_replace(array("&#0044;", "&#0045;"),
147 array(",", "-"), $tmp[1]);
516cf3eb 148 }
149 return true;
150 }
151
152 function save_session_and_responses(&$question, &$state) {
153 $responses = $state->responses;
77fa3a0d 154 // encode - (hyphen) and , (comma) to &#0045; because they are used as
155 // delimiters
516cf3eb 156 array_walk($responses, create_function('&$val, $key',
77fa3a0d 157 '$val = str_replace(array(",", "-"), array("&#0044;", "&#0045;"), $val);
158 $val = "$key-$val";'));
516cf3eb 159 $responses = implode(',', $responses);
160
161 // Set the legacy answer field
77fa3a0d 162 if (!set_field('question_states', 'answer', $responses, 'id', $state->id)) {
516cf3eb 163 return false;
164 }
165 return true;
166 }
167
168 /**
169 * Deletes question from the question-type specific tables
170 *
171 * @return boolean Success/Failure
172 * @param object $question The question being deleted
173 */
90c3f310 174 function delete_question($questionid) {
175 delete_records("question_multianswer", "question", $questionid);
516cf3eb 176 return true;
177 }
178
179 function get_correct_responses(&$question, &$state) {
f02c6f01 180 global $QTYPES;
516cf3eb 181 $responses = array();
182 foreach($question->options->questions as $key => $wrapped) {
f02c6f01 183 if ($correct = $QTYPES[$wrapped->qtype]->get_correct_responses($wrapped, $state)) {
516cf3eb 184 $responses[$key] = $correct[''];
185 } else {
186 // if there is no correct answer to this subquestion then there
187 // can not be a correct answer to the whole question either, so
188 // we have to return null.
189 return null;
190 }
191 }
192 return $responses;
193 }
194
195 function print_question_formulation_and_controls(&$question, &$state, $cmoptions, $options) {
9fc3100f 196
04d6ac46 197 global $QTYPES, $CFG, $USER;
516cf3eb 198 $readonly = empty($options->readonly) ? '' : 'readonly="readonly"';
6463e8a6 199 $disabled = empty($options->readonly) ? '' : 'disabled="disabled"';
da298d82 200 $formatoptions = new stdClass;
7347c60b 201 $formatoptions->noclean = true;
202 $formatoptions->para = false;
516cf3eb 203 $nameprefix = $question->name_prefix;
9fc3100f 204
73ca1421 205 // adding an icon with alt to warn user this is a fill in the gap question
206 // MDL-7497
ccffd412 207 if (!empty($USER->screenreader)) {
73ca1421 208 echo "<img src=\"$CFG->wwwroot/question/type/$question->qtype/icon.gif\" ".
0d905d9f 209 "class=\"icon\" alt=\"".get_string('clozeaid','qtype_multichoice')."\" /> ";
73ca1421 210 }
516cf3eb 211 // For this question type, we better print the image on top:
9fc3100f 212 if ($image = get_question_image($question)) {
da298d82 213 echo('<img class="qimage" src="' . $image . '" alt="" /><br />');
214 }
9fc3100f 215
516cf3eb 216 $qtextremaining = format_text($question->questiontext,
77fa3a0d 217 $question->questiontextformat, $formatoptions, $cmoptions->course);
516cf3eb 218
219 $strfeedback = get_string('feedback', 'quiz');
220
221 // The regex will recognize text snippets of type {#X}
222 // where the X can be any text not containg } or white-space characters.
223
224 while (ereg('\{#([^[:space:]}]*)}', $qtextremaining, $regs)) {
225 $qtextsplits = explode($regs[0], $qtextremaining, 2);
73ca1421 226 echo "<label>"; // MDL-7497
516cf3eb 227 echo $qtextsplits[0];
228 $qtextremaining = $qtextsplits[1];
229
230 $positionkey = $regs[1];
231 $wrapped = &$question->options->questions[$positionkey];
232 $answers = &$wrapped->options->answers;
77fa3a0d 233 $correctanswers = $QTYPES[$wrapped->qtype]->get_correct_responses($wrapped, $state);
516cf3eb 234
235 $inputname = $nameprefix.$positionkey;
e51efd7e 236 if (isset($state->responses[$positionkey])) {
1f8db780 237 $response = $state->responses[$positionkey];
e51efd7e 238 } else {
239 $response = null;
240 }
516cf3eb 241
242 // Determine feedback popup if any
243 $popup = '';
244 $style = '';
2b087056 245 $feedbackimg = '';
516cf3eb 246 if ($options->feedback) {
247 $chosenanswer = null;
248 switch ($wrapped->qtype) {
dfa47f96 249 case 'numerical':
dfa47f96 250 case 'shortanswer':
516cf3eb 251 $testedstate = clone($state);
252 $testedstate->responses[''] = $response;
516cf3eb 253 foreach ($answers as $answer) {
f02c6f01 254 if($QTYPES[$wrapped->qtype]
bc2defd5 255 ->test_response($wrapped, $testedstate, $answer)) {
256 $chosenanswer = clone($answer);
257 break;
516cf3eb 258 }
259 }
260 break;
dfa47f96 261 case 'multichoice':
516cf3eb 262 if (isset($answers[$response])) {
263 $chosenanswer = clone($answers[$response]);
264 }
265 break;
266 default:
267 break;
268 }
269
270 // Set up a default chosenanswer so that all non-empty wrong
271 // answers are highlighted red
272 if (empty($chosenanswer) && !empty($response)) {
273 $chosenanswer = new stdClass;
274 $chosenanswer->fraction = 0.0;
275 }
276
277 if (!empty($chosenanswer->feedback)) {
1f8db780 278 $feedback = s(str_replace(array("\\", "'"), array("\\\\", "\\'"), $chosenanswer->feedback));
516cf3eb 279 $popup = " onmouseover=\"return overlib('$feedback', STICKY, MOUSEOFF, CAPTION, '$strfeedback', FGCOLOR, '#FFFFFF');\" ".
280 " onmouseout=\"return nd();\" ";
281 }
282
283 /// Determine style
d9935f29 284 if ($options->feedback && $response != '') {
2b087056 285 $style = 'class = "'.question_get_feedback_class($chosenanswer->fraction).'"';
286 $feedbackimg = question_get_feedback_image($chosenanswer->fraction);
516cf3eb 287 } else {
288 $style = '';
2b087056 289 $feedbackimg = '';
516cf3eb 290 }
291 }
292
293 // Print the input control
294 switch ($wrapped->qtype) {
dfa47f96 295 case 'shortanswer':
296 case 'numerical':
516cf3eb 297 echo " <input $style $readonly $popup name=\"$inputname\"
1f8db780 298 type=\"text\" value=\"".s($response, true)."\" size=\"12\" /> ";
ccffd412 299 if (!empty($feedback) && !empty($USER->screenreader)) {
04d6ac46 300 echo "<img src=\"$CFG->pixpath/i/feedback.gif\" alt=\"$feedback\" />";
301 }
2b087056 302 echo $feedbackimg;
516cf3eb 303 break;
dfa47f96 304 case 'multichoice':
516cf3eb 305 $outputoptions = '<option></option>'; // Default empty option
306 foreach ($answers as $mcanswer) {
77fa3a0d 307 $selected = '';
308 if ($response == $mcanswer->id) {
309 $selected = ' selected="selected"';
310 }
311 $outputoptions .= "<option value=\"$mcanswer->id\"$selected>" .
312 s($mcanswer->answer, true) . '</option>';
516cf3eb 313 }
e0c25647 314 // In the next line, $readonly is invalid HTML, but it works in
315 // all browsers. $disabled would be valid, but then the JS for
316 // displaying the feedback does not work. Of course, we should
317 // not be relying on JS (for accessibility reasons), but that is
318 // a bigger problem.
2b087056 319 //
320 // The span is used for safari, which does not allow styling of
321 // selects.
e0c25647 322 echo "<span $style><select $popup $readonly $style name=\"$inputname\">";
323 echo $outputoptions;
324 echo '</select></span>';
ccffd412 325 if (!empty($feedback) && !empty($USER->screenreader)) {
04d6ac46 326 echo "<img src=\"$CFG->pixpath/i/feedback.gif\" alt=\"$feedback\" />";
e0c25647 327 }
2b087056 328 echo $feedbackimg;
e0c25647 329 break;
330 default:
331 error("Unable to recognize questiontype ($wrapped->qtype) of
332 question part $positionkey.");
333 break;
516cf3eb 334 }
73ca1421 335 echo "</label>"; // MDL-7497
516cf3eb 336 }
337
338 // Print the final piece of question text:
339 echo $qtextremaining;
f4b72cdb 340 $this->print_question_submit_buttons($question, $state, $cmoptions, $options);
516cf3eb 341 }
342
343 function grade_responses(&$question, &$state, $cmoptions) {
f02c6f01 344 global $QTYPES;
516cf3eb 345 $teststate = clone($state);
346 $state->raw_grade = 0;
347 foreach($question->options->questions as $key => $wrapped) {
1f8db780 348 $state->responses[$key] = $state->responses[$key];
516cf3eb 349 $teststate->responses = array('' => $state->responses[$key]);
350 $teststate->raw_grade = 0;
f02c6f01 351 if (false === $QTYPES[$wrapped->qtype]
516cf3eb 352 ->grade_responses($wrapped, $teststate, $cmoptions)) {
353 return false;
354 }
355 $state->raw_grade += $teststate->raw_grade;
356 }
357 $state->raw_grade /= $question->defaultgrade;
358 $state->raw_grade = min(max((float) $state->raw_grade, 0.0), 1.0)
359 * $question->maxgrade;
360
361 if (empty($state->raw_grade)) {
362 $state->raw_grade = 0.0;
363 }
364 $state->penalty = $question->penalty * $question->maxgrade;
365
f30bbcaf 366 // mark the state as graded
367 $state->event = ($state->event == QUESTION_EVENTCLOSE) ? QUESTION_EVENTCLOSEANDGRADE : QUESTION_EVENTGRADE;
368
516cf3eb 369 return true;
370 }
371
372 function get_actual_response($question, $state) {
f02c6f01 373 global $QTYPES;
516cf3eb 374 $teststate = clone($state);
375 foreach($question->options->questions as $key => $wrapped) {
376 $state->responses[$key] = html_entity_decode($state->responses[$key]);
377 $teststate->responses = array('' => $state->responses[$key]);
f02c6f01 378 $correct = $QTYPES[$wrapped->qtype]
516cf3eb 379 ->get_actual_response($wrapped, $teststate);
380 // change separator here if you want
381 $responsesseparator = ',';
382 $responses[$key] = implode($responsesseparator, $correct);
383 }
384 return $responses;
385 }
9fc3100f 386
c5d94c41 387/// BACKUP FUNCTIONS ////////////////////////////
388
389 /*
390 * Backup the data in the question
391 *
392 * This is used in question/backuplib.php
393 */
394 function backup($bf,$preferences,$question,$level=6) {
395
396 $status = true;
397
398 $multianswers = get_records("question_multianswer","question",$question,"id");
399 //If there are multianswers
400 if ($multianswers) {
401 //Print multianswers header
402 $status = fwrite ($bf,start_tag("MULTIANSWERS",$level,true));
403 //Iterate over each multianswer
404 foreach ($multianswers as $multianswer) {
405 $status = fwrite ($bf,start_tag("MULTIANSWER",$level+1,true));
406 //Print multianswer contents
407 fwrite ($bf,full_tag("ID",$level+2,false,$multianswer->id));
408 fwrite ($bf,full_tag("QUESTION",$level+2,false,$multianswer->question));
409 fwrite ($bf,full_tag("SEQUENCE",$level+2,false,$multianswer->sequence));
410 $status = fwrite ($bf,end_tag("MULTIANSWER",$level+1,true));
411 }
412 //Print multianswers footer
413 $status = fwrite ($bf,end_tag("MULTIANSWERS",$level,true));
414 //Now print question_answers
415 $status = question_backup_answers($bf,$preferences,$question);
416 }
417 return $status;
418 }
419
315559d3 420/// RESTORE FUNCTIONS /////////////////
421
422 /*
423 * Restores the data in the question
424 *
425 * This is used in question/restorelib.php
426 */
427 function restore($old_question_id,$new_question_id,$info,$restore) {
428
429 $status = true;
430
431 //Get the multianswers array
432 $multianswers = $info['#']['MULTIANSWERS']['0']['#']['MULTIANSWER'];
433 //Iterate over multianswers
434 for($i = 0; $i < sizeof($multianswers); $i++) {
435 $mul_info = $multianswers[$i];
436
437 //We need this later
438 $oldid = backup_todb($mul_info['#']['ID']['0']['#']);
439
440 //Now, build the question_multianswer record structure
da298d82 441 $multianswer = new stdClass;
315559d3 442 $multianswer->question = $new_question_id;
443 $multianswer->sequence = backup_todb($mul_info['#']['SEQUENCE']['0']['#']);
444
445 //We have to recode the sequence field (a list of question ids)
446 //Extracts question id from sequence
447 $sequence_field = "";
448 $in_first = true;
449 $tok = strtok($multianswer->sequence,",");
450 while ($tok) {
451 //Get the answer from backup_ids
452 $question = backup_getid($restore->backup_unique_code,"question",$tok);
453 if ($question) {
454 if ($in_first) {
455 $sequence_field .= $question->new_id;
456 $in_first = false;
457 } else {
458 $sequence_field .= ",".$question->new_id;
459 }
460 }
461 //check for next
462 $tok = strtok(",");
463 }
464 //We have the answers field recoded to its new ids
465 $multianswer->sequence = $sequence_field;
466 //The structure is equal to the db, so insert the question_multianswer
da298d82 467 $newid = insert_record("question_multianswer", $multianswer);
315559d3 468
469 //Save ids in backup_ids
470 if ($newid) {
471 backup_putid($restore->backup_unique_code,"question_multianswer",
472 $oldid, $newid);
473 }
474
475 //Do some output
476 if (($i+1) % 50 == 0) {
477 if (!defined('RESTORE_SILENTLY')) {
478 echo ".";
479 if (($i+1) % 1000 == 0) {
480 echo "<br />";
481 }
482 }
483 backup_flush(300);
484 }
315559d3 485 }
486
487 return $status;
488 }
489
490 function restore_map($old_question_id,$new_question_id,$info,$restore) {
491
492 $status = true;
493
494 //Get the multianswers array
495 $multianswers = $info['#']['MULTIANSWERS']['0']['#']['MULTIANSWER'];
496 //Iterate over multianswers
497 for($i = 0; $i < sizeof($multianswers); $i++) {
498 $mul_info = $multianswers[$i];
499
500 //We need this later
501 $oldid = backup_todb($mul_info['#']['ID']['0']['#']);
502
503 //Now, build the question_multianswer record structure
504 $multianswer->question = $new_question_id;
505 $multianswer->answers = backup_todb($mul_info['#']['ANSWERS']['0']['#']);
506 $multianswer->positionkey = backup_todb($mul_info['#']['POSITIONKEY']['0']['#']);
507 $multianswer->answertype = backup_todb($mul_info['#']['ANSWERTYPE']['0']['#']);
508 $multianswer->norm = backup_todb($mul_info['#']['NORM']['0']['#']);
509
510 //If we are in this method is because the question exists in DB, so its
511 //multianswer must exist too.
512 //Now, we are going to look for that multianswer in DB and to create the
513 //mappings in backup_ids to use them later where restoring states (user level).
514
515 //Get the multianswer from DB (by question and positionkey)
516 $db_multianswer = get_record ("question_multianswer","question",$new_question_id,
517 "positionkey",$multianswer->positionkey);
518 //Do some output
519 if (($i+1) % 50 == 0) {
520 if (!defined('RESTORE_SILENTLY')) {
521 echo ".";
522 if (($i+1) % 1000 == 0) {
523 echo "<br />";
524 }
525 }
526 backup_flush(300);
527 }
528
529 //We have the database multianswer, so update backup_ids
530 if ($db_multianswer) {
531 //We have the newid, update backup_ids
532 backup_putid($restore->backup_unique_code,"question_multianswer",$oldid,
533 $db_multianswer->id);
534 } else {
535 $status = false;
536 }
537 }
538
539 return $status;
540 }
541
542 function restore_recode_answer($state, $restore) {
543 //The answer is a comma separated list of hypen separated sequence number and answers. We may have to recode the answers
544 $answer_field = "";
545 $in_first = true;
546 $tok = strtok($state->answer,",");
547 while ($tok) {
548 //Extract the multianswer_id and the answer
549 $exploded = explode("-",$tok);
550 $seqnum = $exploded[0];
551 $answer = $exploded[1];
552 // $sequence is an ordered array of the question ids.
553 if (!$sequence = get_field('question_multianswer', 'sequence', 'question', $state->question)) {
554 error("The cloze question $state->question is missing its options");
555 }
556 $sequence = explode(',', $sequence);
557 // The id of the current question.
558 $wrappedquestionid = $sequence[$seqnum-1];
559 // now we can find the question
560 if (!$wrappedquestion = get_record('question', 'id', $wrappedquestionid)) {
158e5459 561 notify("Can't find the subquestion $wrappedquestionid that is used as part $seqnum in cloze question $state->question");
315559d3 562 }
563 // For multichoice question we need to recode the answer
dfa47f96 564 if ($answer and $wrappedquestion->qtype == 'multichoice') {
315559d3 565 //The answer is an answer_id, look for it in backup_ids
566 if (!$ans = backup_getid($restore->backup_unique_code,"question_answers",$answer)) {
567 echo 'Could not recode cloze multichoice answer '.$answer.'<br />';
568 }
569 $answer = $ans->new_id;
570 }
571 //build the new answer field for each pair
572 if ($in_first) {
573 $answer_field .= $seqnum."-".$answer;
574 $in_first = false;
575 } else {
576 $answer_field .= ",".$seqnum."-".$answer;
577 }
578 //check for next
579 $tok = strtok(",");
580 }
581 return $answer_field;
582 }
583
584
516cf3eb 585}
586//// END OF CLASS ////
587
588
589//////////////////////////////////////////////////////////////////////////
590//// INITIATION - Without this line the question type is not in use... ///
591//////////////////////////////////////////////////////////////////////////
a2156789 592question_register_questiontype(new embedded_cloze_qtype());
516cf3eb 593
594/////////////////////////////////////////////////////////////
595//// ADDITIONAL FUNCTIONS
596//// The functions below deal exclusivly with editing
dfa47f96 597//// of questions with question type 'multianswer'.
516cf3eb 598//// Therefore they are kept in this file.
599//// They are not in the class as they are not
600//// likely to be subject for overriding.
601/////////////////////////////////////////////////////////////
602
0b346164 603// ANSWER_ALTERNATIVE regexes
604define("ANSWER_ALTERNATIVE_FRACTION_REGEX",
605 '=|%(-?[0-9]+)%');
606// for the syntax '(?<!' see http://www.perl.com/doc/manual/html/pod/perlre.html#item_C
607define("ANSWER_ALTERNATIVE_ANSWER_REGEX",
e51efd7e 608 '.+?(?<!\\\\|&|&amp;)(?=[~#}]|$)');
0b346164 609define("ANSWER_ALTERNATIVE_FEEDBACK_REGEX",
610 '.*?(?<!\\\\)(?=[~}]|$)');
0b346164 611define("ANSWER_ALTERNATIVE_REGEX",
e51efd7e 612 '(' . ANSWER_ALTERNATIVE_FRACTION_REGEX .')?' .
613 '(' . ANSWER_ALTERNATIVE_ANSWER_REGEX . ')' .
614 '(#(' . ANSWER_ALTERNATIVE_FEEDBACK_REGEX .'))?');
0b346164 615
616// Parenthesis positions for ANSWER_ALTERNATIVE_REGEX
617define("ANSWER_ALTERNATIVE_REGEX_PERCENTILE_FRACTION", 2);
618define("ANSWER_ALTERNATIVE_REGEX_FRACTION", 1);
619define("ANSWER_ALTERNATIVE_REGEX_ANSWER", 3);
620define("ANSWER_ALTERNATIVE_REGEX_FEEDBACK", 5);
621
622// NUMBER_FORMATED_ALTERNATIVE_ANSWER_REGEX is used
623// for identifying numerical answers in ANSWER_ALTERNATIVE_REGEX_ANSWER
624define("NUMBER_REGEX",
625 '-?(([0-9]+[.,]?[0-9]*|[.,][0-9]+)([eE][-+]?[0-9]+)?)');
626define("NUMERICAL_ALTERNATIVE_REGEX",
627 '^(' . NUMBER_REGEX . ')(:' . NUMBER_REGEX . ')?$');
628
629// Parenthesis positions for NUMERICAL_FORMATED_ALTERNATIVE_ANSWER_REGEX
630define("NUMERICAL_CORRECT_ANSWER", 1);
631define("NUMERICAL_ABS_ERROR_MARGIN", 6);
632
633// Remaining ANSWER regexes
634define("ANSWER_TYPE_DEF_REGEX",
635 '(NUMERICAL|NM)|(MULTICHOICE|MC)|(SHORTANSWER|SA|MW)');
636define("ANSWER_START_REGEX",
637 '\{([0-9]*):(' . ANSWER_TYPE_DEF_REGEX . '):');
638
639define("ANSWER_REGEX",
640 ANSWER_START_REGEX
641 . '(' . ANSWER_ALTERNATIVE_REGEX
642 . '(~'
643 . ANSWER_ALTERNATIVE_REGEX
644 . ')*)\}' );
645
646// Parenthesis positions for singulars in ANSWER_REGEX
647define("ANSWER_REGEX_NORM", 1);
648define("ANSWER_REGEX_ANSWER_TYPE_NUMERICAL", 3);
649define("ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE", 4);
650define("ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER", 5);
651define("ANSWER_REGEX_ALTERNATIVES", 6);
516cf3eb 652
7518b645 653function qtype_multianswer_extract_question($text) {
516cf3eb 654 $question = new stdClass;
dfa47f96 655 $question->qtype = 'multianswer';
516cf3eb 656 $question->questiontext = $text;
657 $question->options->questions = array();
658 $question->defaultgrade = 0; // Will be increased for each answer norm
659
660 for ($positionkey=1
661 ; preg_match('/'.ANSWER_REGEX.'/', $question->questiontext, $answerregs)
662 ; ++$positionkey ) {
663 $wrapped = new stdClass;
664 $wrapped->defaultgrade = $answerregs[ANSWER_REGEX_NORM]
665 or $wrapped->defaultgrade = '1';
666 if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_NUMERICAL])) {
dfa47f96 667 $wrapped->qtype = 'numerical';
516cf3eb 668 $wrapped->multiplier = array();
669 $wrapped->units = array();
670 } else if(!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER])) {
dfa47f96 671 $wrapped->qtype = 'shortanswer';
516cf3eb 672 $wrapped->usecase = 0;
673 } else if(!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE])) {
dfa47f96 674 $wrapped->qtype = 'multichoice';
516cf3eb 675 $wrapped->single = 1;
271e6dec 676 $wrapped->answernumbering = 0;
e51efd7e 677 $wrapped->correctfeedback = '';
678 $wrapped->partiallycorrectfeedback = '';
679 $wrapped->incorrectfeedback = '';
516cf3eb 680 } else {
681 error("Cannot identify qtype $answerregs[2]");
682 return false;
683 }
684
685 // Each $wrapped simulates a $form that can be processed by the
686 // respective save_question and save_question_options methods of the
687 // wrapped questiontypes
688 $wrapped->answer = array();
689 $wrapped->fraction = array();
690 $wrapped->feedback = array();
691 $wrapped->shuffleanswers = 1;
e51efd7e 692 $wrapped->questiontext = $answerregs[0];
516cf3eb 693 $wrapped->questiontextformat = 0;
694
695 $remainingalts = $answerregs[ANSWER_REGEX_ALTERNATIVES];
696 while (preg_match('/~?'.ANSWER_ALTERNATIVE_REGEX.'/', $remainingalts, $altregs)) {
697 if ('=' == $altregs[ANSWER_ALTERNATIVE_REGEX_FRACTION]) {
698 $wrapped->fraction[] = '1';
e51efd7e 699 } else if ($percentile = $altregs[ANSWER_ALTERNATIVE_REGEX_PERCENTILE_FRACTION]){
516cf3eb 700 $wrapped->fraction[] = .01 * $percentile;
701 } else {
702 $wrapped->fraction[] = '0';
703 }
e51efd7e 704 if (isset($altregs[ANSWER_ALTERNATIVE_REGEX_FEEDBACK])) {
83d22f70 705 $feedback = html_entity_decode($altregs[ANSWER_ALTERNATIVE_REGEX_FEEDBACK], ENT_QUOTES, 'UTF-8');
095b599a 706 $feedback = str_replace('\}', '}', $feedback);
707 $wrapped->feedback[] = str_replace('\#', '#', $feedback);
e51efd7e 708 } else {
709 $wrapped->feedback[] = '';
710 }
516cf3eb 711 if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_NUMERICAL])
e51efd7e 712 && ereg(NUMERICAL_ALTERNATIVE_REGEX, $altregs[ANSWER_ALTERNATIVE_REGEX_ANSWER], $numregs)) {
713 $wrapped->answer[] = $numregs[NUMERICAL_CORRECT_ANSWER];
516cf3eb 714 if ($numregs[NUMERICAL_ABS_ERROR_MARGIN]) {
715 $wrapped->tolerance[] =
e51efd7e 716 $numregs[NUMERICAL_ABS_ERROR_MARGIN];
516cf3eb 717 } else {
718 $wrapped->tolerance[] = 0;
719 }
720 } else { // Tolerance can stay undefined for non numerical questions
1f8db780 721 // Undo quoting done by the HTML editor.
9c61c44f 722 $answer = html_entity_decode($altregs[ANSWER_ALTERNATIVE_REGEX_ANSWER], ENT_QUOTES, 'UTF-8');
095b599a 723 $answer = str_replace('\}', '}', $answer);
724 $wrapped->answer[] = str_replace('\#', '#', $answer);
516cf3eb 725 }
726 $tmp = explode($altregs[0], $remainingalts, 2);
727 $remainingalts = $tmp[1];
728 }
729
730 $question->defaultgrade += $wrapped->defaultgrade;
731 $question->options->questions[$positionkey] = clone($wrapped);
732 $question->questiontext = implode("{#$positionkey}",
733 explode($answerregs[0], $question->questiontext, 2));
734 }
e51efd7e 735 $question->questiontext = $question->questiontext;
516cf3eb 736 return $question;
737}
516cf3eb 738?>