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