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