4 /// MULTIANSWER /// (Embedded - cloze)
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
15 /// QUESTION TYPE CLASS //////////////////
17 * @package questionbank
18 * @subpackage questiontypes
20 class embedded_cloze_qtype
extends default_questiontype
{
26 function get_question_options(&$question) {
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']= '';
36 $wrappedquestions = get_records_list('question', 'id', $sequence, 'id ASC');
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
47 foreach($sequence as $seq){
48 $question->options
->questions
[$seq]= '';
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})");
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
;
59 $question->options
->questions
[$sequence[$wrapped->id
]] = clone($wrapped); // ??? Why do we need a clone here?
63 if ($nbvaliquestion == 0 ) {
64 notify(get_string('noquestions','qtype_multianswer',$question->name
));
69 function save_question_options($question) {
71 $result = new stdClass
;
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.
80 // First we get all the existing wrapped questions
81 if (!$oldwrappedids = get_field('question_multianswer', 'sequence', 'question', $question->id
)) {
82 $oldwrappedquestions = array();
84 $oldwrappedquestions = get_records_list('question', 'id', $oldwrappedids, 'id ASC');
87 foreach($question->options
->questions
as $wrapped) {
89 // if we still have some old wrapped question ids, reuse the next of them
91 if (is_array($oldwrappedquestions) && $oldwrappedquestion = array_shift($oldwrappedquestions)) {
92 $wrapped->id
= $oldwrappedquestion->id
;
93 if($oldwrappedquestion->qtype
!= $wrapped->qtype
) {
94 switch ($oldwrappedquestion->qtype
) {
96 delete_records('question_multichoice', 'question' , $oldwrappedquestion->id
);
99 delete_records('question_shortanswer', 'question' , $oldwrappedquestion->id
);
102 delete_records('question_numerical', 'question' , $oldwrappedquestion->id
);
105 print_error('qtypenotrecognized', 'qtype_multianswer','',$oldwrappedquestion->qtype
);
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
;
121 // Delete redundant wrapped questions
122 if(is_array($oldwrappedids) && count($oldwrappedids)){
123 foreach ($oldwrappedids as $id) {
124 delete_question($id) ;
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)";
140 if (!insert_record("question_multianswer", $multianswer)) {
141 $result->error
= "Could not insert cloze question options!";
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
;
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);
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] = '';
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]);
188 function save_session_and_responses(&$question, &$state) {
189 $responses = $state->responses
;
190 // encode - (hyphen) and , (comma) to - because they are used as
192 array_walk($responses, create_function('&$val, $key',
193 '$val = str_replace(array(",", "-"), array(",", "-"), $val);
194 $val = "$key-$val";'));
195 $responses = implode(',', $responses);
197 // Set the legacy answer field
198 if (!set_field('question_states', 'answer', $responses, 'id', $state->id
)) {
205 * Deletes question from the question-type specific tables
207 * @return boolean Success/Failure
208 * @param object $question The question being deleted
210 function delete_question($questionid) {
211 delete_records("question_multianswer", "question", $questionid);
215 function get_correct_responses(&$question, &$state) {
217 $responses = array();
218 foreach($question->options
->questions
as $key => $wrapped) {
220 if ($correct = $QTYPES[$wrapped->qtype
]->get_correct_responses($wrapped, $state)) {
221 $responses[$key] = $correct[''];
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.
233 function print_question_formulation_and_controls(&$question, &$state, $cmoptions, $options) {
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
;
243 // adding an icon with alt to warn user this is a fill in the gap question
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')."\" /> ";
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 />');
256 $qtextremaining = format_text($question->questiontext
,
257 $question->questiontextformat
, $formatoptions, $cmoptions->course
);
259 $strfeedback = get_string('feedback', 'quiz');
261 // The regex will recognize text snippets of type {#X}
262 // where the X can be any text not containg } or white-space characters.
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];
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);
276 $inputname = $nameprefix.$positionkey;
277 if (isset($state->responses
[$positionkey])) {
278 $response = $state->responses
[$positionkey];
283 // Determine feedback popup if any
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
) {
295 if ($correctanswers) {
296 foreach ($correctanswers as $ca) {
297 switch($wrapped->qtype
){
300 $correctanswer .= $delimiter.$ca;
303 if (isset($answers[$ca])){
304 $correctanswer .= $delimiter.$answers[$ca]->answer
;
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');
319 if ($options->feedback
) {
320 $chosenanswer = null;
321 switch ($wrapped->qtype
) {
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);
335 if (isset($answers[$response])) {
336 $chosenanswer = clone($answers[$response]);
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;
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');
355 $strfeedbackwrapped = get_string('feedback', 'quiz');
357 $popup = " onmouseover=\"return overlib('$feedback', STICKY, MOUSEOFF, CAPTION, '$strfeedbackwrapped', FGCOLOR, '#FFFFFF');\" ".
358 " onmouseout=\"return nd();\" ";
362 if ($options->feedback
&& $response != '') {
363 $style = 'class = "'.question_get_feedback_class($chosenanswer->fraction
).'"';
364 $feedbackimg = question_get_feedback_image($chosenanswer->fraction
);
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();\" ";
377 // Print the input control
378 switch ($wrapped->qtype
) {
382 foreach ($answers as $answer) {
383 if (strlen(trim($answer->answer
)) > $size ){
384 $size = strlen(trim($answer->answer
));
387 if (strlen(trim($response))> $size ){
388 $size = strlen(trim($response))+
1;
390 $size = $size +
rand(0,$size*0.15);
391 $size > 60 ?
$size = 60 : $size = $size;
392 $styleinfo = "size=\"$size\"";
394 * Uncomment the following lines if you want to limit for small sizes.
395 * Results may vary with browsers see MDL-3274
399 $styleinfo = 'style="width: 1.1em;"';
402 $styleinfo = 'style="width: 1.9em;"';
405 $styleinfo = 'style="width: 2.3em;"';
408 $styleinfo = 'style="width: 2.8em;"';
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\" />";
420 $outputoptions = '<option></option>'; // Default empty option
421 foreach ($answers as $mcanswer) {
423 if ($response == $mcanswer->id
) {
424 $selected = ' selected="selected"';
426 $outputoptions .= "<option value=\"$mcanswer->id\"$selected>" .
427 s($mcanswer->answer
, true) . '</option>';
429 // In the next line, $readonly is invalid HTML, but it works in
430 // all browsers. $disabled would be valid, but then the JS for
431 // displaying the feedback does not work. Of course, we should
432 // not be relying on JS (for accessibility reasons), but that is
435 // The span is used for safari, which does not allow styling of
437 echo "<span $style><select $popup $readonly $style name=\"$inputname\">";
439 echo '</select></span>';
440 if (!empty($feedback) && !empty($USER->screenreader
)) {
441 echo "<img src=\"$CFG->pixpath/i/feedback.gif\" alt=\"$feedback\" />";
447 $a->type
= $wrapped->qtype
;
448 $a->sub
= $positionkey;
449 print_error('unknownquestiontypeofsubquestion', 'qtype_multianswer','',$a);
452 echo "</label>"; // MDL-7497
455 if(! isset($question->options
->questions
[$positionkey])){
456 echo $regs[0]."</label>";
458 echo '</label><div class="error" >'.get_string('questionnotfound','qtype_multianswer',$positionkey).'</div>';
463 // Print the final piece of question text:
464 echo $qtextremaining;
465 $this->print_question_submit_buttons($question, $state, $cmoptions, $options);
469 function grade_responses(&$question, &$state, $cmoptions) {
471 $teststate = clone($state);
472 $state->raw_grade
= 0;
473 foreach($question->options
->questions
as $key => $wrapped) {
475 $state->responses
[$key] = $state->responses
[$key];
476 $teststate->responses
= array('' => $state->responses
[$key]);
477 $teststate->raw_grade
= 0;
478 if (false === $QTYPES[$wrapped->qtype
]
479 ->grade_responses($wrapped, $teststate, $cmoptions)) {
482 $state->raw_grade +
= $teststate->raw_grade
;
485 $state->raw_grade
/= $question->defaultgrade
;
486 $state->raw_grade
= min(max((float) $state->raw_grade
, 0.0), 1.0)
487 * $question->maxgrade
;
489 if (empty($state->raw_grade
)) {
490 $state->raw_grade
= 0.0;
492 $state->penalty
= $question->penalty
* $question->maxgrade
;
494 // mark the state as graded
495 $state->event
= ($state->event
== QUESTION_EVENTCLOSE
) ? QUESTION_EVENTCLOSEANDGRADE
: QUESTION_EVENTGRADE
;
500 function get_actual_response($question, $state) {
502 $teststate = clone($state);
503 foreach($question->options
->questions
as $key => $wrapped) {
504 $state->responses
[$key] = html_entity_decode($state->responses
[$key]);
505 $teststate->responses
= array('' => $state->responses
[$key]);
506 $correct = $QTYPES[$wrapped->qtype
]
507 ->get_actual_response($wrapped, $teststate);
508 // change separator here if you want
509 $responsesseparator = ',';
510 $responses[$key] = implode($responsesseparator, $correct);
515 /// BACKUP FUNCTIONS ////////////////////////////
518 * Backup the data in the question
520 * This is used in question/backuplib.php
522 function backup($bf,$preferences,$question,$level=6) {
526 $multianswers = get_records("question_multianswer","question",$question,"id");
527 //If there are multianswers
529 //Print multianswers header
530 $status = fwrite ($bf,start_tag("MULTIANSWERS",$level,true));
531 //Iterate over each multianswer
532 foreach ($multianswers as $multianswer) {
533 $status = fwrite ($bf,start_tag("MULTIANSWER",$level+
1,true));
534 //Print multianswer contents
535 fwrite ($bf,full_tag("ID",$level+
2,false,$multianswer->id
));
536 fwrite ($bf,full_tag("QUESTION",$level+
2,false,$multianswer->question
));
537 fwrite ($bf,full_tag("SEQUENCE",$level+
2,false,$multianswer->sequence
));
538 $status = fwrite ($bf,end_tag("MULTIANSWER",$level+
1,true));
540 //Print multianswers footer
541 $status = fwrite ($bf,end_tag("MULTIANSWERS",$level,true));
542 //Now print question_answers
543 $status = question_backup_answers($bf,$preferences,$question);
548 /// RESTORE FUNCTIONS /////////////////
551 * Restores the data in the question
553 * This is used in question/restorelib.php
555 function restore($old_question_id,$new_question_id,$info,$restore) {
559 //Get the multianswers array
560 $multianswers = $info['#']['MULTIANSWERS']['0']['#']['MULTIANSWER'];
561 //Iterate over multianswers
562 for($i = 0; $i < sizeof($multianswers); $i++
) {
563 $mul_info = $multianswers[$i];
566 $oldid = backup_todb($mul_info['#']['ID']['0']['#']);
568 //Now, build the question_multianswer record structure
569 $multianswer = new stdClass
;
570 $multianswer->question
= $new_question_id;
571 $multianswer->sequence
= backup_todb($mul_info['#']['SEQUENCE']['0']['#']);
573 //We have to recode the sequence field (a list of question ids)
574 //Extracts question id from sequence
575 $sequence_field = "";
577 $tok = strtok($multianswer->sequence
,",");
579 //Get the answer from backup_ids
580 $question = backup_getid($restore->backup_unique_code
,"question",$tok);
583 $sequence_field .= $question->new_id
;
586 $sequence_field .= ",".$question->new_id
;
592 //We have the answers field recoded to its new ids
593 $multianswer->sequence
= $sequence_field;
594 //The structure is equal to the db, so insert the question_multianswer
595 $newid = insert_record("question_multianswer", $multianswer);
597 //Save ids in backup_ids
599 backup_putid($restore->backup_unique_code
,"question_multianswer",
604 if (($i+
1) %
50 == 0) {
605 if (!defined('RESTORE_SILENTLY')) {
607 if (($i+
1) %
1000 == 0) {
618 function restore_map($old_question_id,$new_question_id,$info,$restore) {
622 //Get the multianswers array
623 $multianswers = $info['#']['MULTIANSWERS']['0']['#']['MULTIANSWER'];
624 //Iterate over multianswers
625 for($i = 0; $i < sizeof($multianswers); $i++
) {
626 $mul_info = $multianswers[$i];
629 $oldid = backup_todb($mul_info['#']['ID']['0']['#']);
631 //Now, build the question_multianswer record structure
632 $multianswer->question
= $new_question_id;
633 $multianswer->answers
= backup_todb($mul_info['#']['ANSWERS']['0']['#']);
634 $multianswer->positionkey
= backup_todb($mul_info['#']['POSITIONKEY']['0']['#']);
635 $multianswer->answertype
= backup_todb($mul_info['#']['ANSWERTYPE']['0']['#']);
636 $multianswer->norm
= backup_todb($mul_info['#']['NORM']['0']['#']);
638 //If we are in this method is because the question exists in DB, so its
639 //multianswer must exist too.
640 //Now, we are going to look for that multianswer in DB and to create the
641 //mappings in backup_ids to use them later where restoring states (user level).
643 //Get the multianswer from DB (by question and positionkey)
644 $db_multianswer = get_record ("question_multianswer","question",$new_question_id,
645 "positionkey",$multianswer->positionkey
);
647 if (($i+
1) %
50 == 0) {
648 if (!defined('RESTORE_SILENTLY')) {
650 if (($i+
1) %
1000 == 0) {
657 //We have the database multianswer, so update backup_ids
658 if ($db_multianswer) {
659 //We have the newid, update backup_ids
660 backup_putid($restore->backup_unique_code
,"question_multianswer",$oldid,
661 $db_multianswer->id
);
670 function restore_recode_answer($state, $restore) {
671 //The answer is a comma separated list of hypen separated sequence number and answers. We may have to recode the answers
674 $tok = strtok($state->answer
,",");
676 //Extract the multianswer_id and the answer
677 $exploded = explode("-",$tok);
678 $seqnum = $exploded[0];
679 $answer = $exploded[1];
680 // $sequence is an ordered array of the question ids.
681 if (!$sequence = get_field('question_multianswer', 'sequence', 'question', $state->question
)) {
682 print_error('missingoption', 'question', '', $state->question
);
684 $sequence = explode(',', $sequence);
685 // The id of the current question.
686 $wrappedquestionid = $sequence[$seqnum-1];
687 // now we can find the question
688 if (!$wrappedquestion = get_record('question', 'id', $wrappedquestionid)) {
689 notify("Can't find the subquestion $wrappedquestionid that is used as part $seqnum in cloze question $state->question");
691 // For multichoice question we need to recode the answer
692 if ($answer and $wrappedquestion->qtype
== 'multichoice') {
693 //The answer is an answer_id, look for it in backup_ids
694 if (!$ans = backup_getid($restore->backup_unique_code
,"question_answers",$answer)) {
695 echo 'Could not recode cloze multichoice answer '.$answer.'<br />';
697 $answer = $ans->new_id
;
699 //build the new answer field for each pair
701 $answer_field .= $seqnum."-".$answer;
704 $answer_field .= ",".$seqnum."-".$answer;
709 return $answer_field;
713 * Runs all the code required to set up and save an essay question for testing purposes.
714 * Alternate DB table prefix may be used to facilitate data deletion.
716 function generate_test($name, $courseid = null) {
717 list($form, $question) = parent
::generate_test($name, $courseid);
718 $question->category
= $form->category
;
719 $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}.
721 Note that addresses like www.moodle.org and smileys :-) all work as normal:
722 a) How good is this? {:MULTICHOICE:=Yes#Correct~No#We have a different opinion}
723 b) What grade would you give it? {3:NUMERICAL:=3:2}
727 $form->feedback
= "feedback";
728 $form->generalfeedback
= "General feedback";
730 $form->penalty
= 0.1;
731 $form->versioning
= 0;
734 $course = get_record('course', 'id', $courseid);
737 return $this->save_question($question, $form, $course);
741 //// END OF CLASS ////
744 //////////////////////////////////////////////////////////////////////////
745 //// INITIATION - Without this line the question type is not in use... ///
746 //////////////////////////////////////////////////////////////////////////
747 question_register_questiontype(new embedded_cloze_qtype());
749 /////////////////////////////////////////////////////////////
750 //// ADDITIONAL FUNCTIONS
751 //// The functions below deal exclusivly with editing
752 //// of questions with question type 'multianswer'.
753 //// Therefore they are kept in this file.
754 //// They are not in the class as they are not
755 //// likely to be subject for overriding.
756 /////////////////////////////////////////////////////////////
758 // ANSWER_ALTERNATIVE regexes
759 define("ANSWER_ALTERNATIVE_FRACTION_REGEX",
761 // for the syntax '(?<!' see http://www.perl.com/doc/manual/html/pod/perlre.html#item_C
762 define("ANSWER_ALTERNATIVE_ANSWER_REGEX",
763 '.+?(?<!\\\\|&|&)(?=[~#}]|$)');
764 define("ANSWER_ALTERNATIVE_FEEDBACK_REGEX",
765 '.*?(?<!\\\\)(?=[~}]|$)');
766 define("ANSWER_ALTERNATIVE_REGEX",
767 '(' . ANSWER_ALTERNATIVE_FRACTION_REGEX
.')?' .
768 '(' . ANSWER_ALTERNATIVE_ANSWER_REGEX
. ')' .
769 '(#(' . ANSWER_ALTERNATIVE_FEEDBACK_REGEX
.'))?');
771 // Parenthesis positions for ANSWER_ALTERNATIVE_REGEX
772 define("ANSWER_ALTERNATIVE_REGEX_PERCENTILE_FRACTION", 2);
773 define("ANSWER_ALTERNATIVE_REGEX_FRACTION", 1);
774 define("ANSWER_ALTERNATIVE_REGEX_ANSWER", 3);
775 define("ANSWER_ALTERNATIVE_REGEX_FEEDBACK", 5);
777 // NUMBER_FORMATED_ALTERNATIVE_ANSWER_REGEX is used
778 // for identifying numerical answers in ANSWER_ALTERNATIVE_REGEX_ANSWER
779 define("NUMBER_REGEX",
780 '-?(([0-9]+[.,]?[0-9]*|[.,][0-9]+)([eE][-+]?[0-9]+)?)');
781 define("NUMERICAL_ALTERNATIVE_REGEX",
782 '^(' . NUMBER_REGEX
. ')(:' . NUMBER_REGEX
. ')?$');
784 // Parenthesis positions for NUMERICAL_FORMATED_ALTERNATIVE_ANSWER_REGEX
785 define("NUMERICAL_CORRECT_ANSWER", 1);
786 define("NUMERICAL_ABS_ERROR_MARGIN", 6);
788 // Remaining ANSWER regexes
789 define("ANSWER_TYPE_DEF_REGEX",
790 '(NUMERICAL|NM)|(MULTICHOICE|MC)|(SHORTANSWER|SA|MW)');
791 define("ANSWER_START_REGEX",
792 '\{([0-9]*):(' . ANSWER_TYPE_DEF_REGEX
. '):');
794 define("ANSWER_REGEX",
796 . '(' . ANSWER_ALTERNATIVE_REGEX
798 . ANSWER_ALTERNATIVE_REGEX
801 // Parenthesis positions for singulars in ANSWER_REGEX
802 define("ANSWER_REGEX_NORM", 1);
803 define("ANSWER_REGEX_ANSWER_TYPE_NUMERICAL", 3);
804 define("ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE", 4);
805 define("ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER", 5);
806 define("ANSWER_REGEX_ALTERNATIVES", 6);
808 function qtype_multianswer_extract_question($text) {
809 $question = new stdClass
;
810 $question->qtype
= 'multianswer';
811 $question->questiontext
= $text;
812 $question->options
->questions
= array();
813 $question->defaultgrade
= 0; // Will be increased for each answer norm
816 ; preg_match('/'.ANSWER_REGEX
.'/', $question->questiontext
, $answerregs)
818 $wrapped = new stdClass
;
819 $wrapped->defaultgrade
= $answerregs[ANSWER_REGEX_NORM
]
820 or $wrapped->defaultgrade
= '1';
821 if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_NUMERICAL
])) {
822 $wrapped->qtype
= 'numerical';
823 $wrapped->multiplier
= array();
824 $wrapped->units
= array();
825 } else if(!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER
])) {
826 $wrapped->qtype
= 'shortanswer';
827 $wrapped->usecase
= 0;
828 } else if(!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE
])) {
829 $wrapped->qtype
= 'multichoice';
830 $wrapped->single
= 1;
831 $wrapped->answernumbering
= 0;
832 $wrapped->correctfeedback
= '';
833 $wrapped->partiallycorrectfeedback
= '';
834 $wrapped->incorrectfeedback
= '';
836 print_error('unknownquestiontype', 'question', '', $answerregs[2]);
840 // Each $wrapped simulates a $form that can be processed by the
841 // respective save_question and save_question_options methods of the
842 // wrapped questiontypes
843 $wrapped->answer
= array();
844 $wrapped->fraction
= array();
845 $wrapped->feedback
= array();
846 $wrapped->shuffleanswers
= 1;
847 $wrapped->questiontext
= $answerregs[0];
848 $wrapped->questiontextformat
= 0;
850 $remainingalts = $answerregs[ANSWER_REGEX_ALTERNATIVES
];
851 while (preg_match('/~?'.ANSWER_ALTERNATIVE_REGEX
.'/', $remainingalts, $altregs)) {
852 if ('=' == $altregs[ANSWER_ALTERNATIVE_REGEX_FRACTION
]) {
853 $wrapped->fraction
[] = '1';
854 } else if ($percentile = $altregs[ANSWER_ALTERNATIVE_REGEX_PERCENTILE_FRACTION
]){
855 $wrapped->fraction
[] = .01 * $percentile;
857 $wrapped->fraction
[] = '0';
859 if (isset($altregs[ANSWER_ALTERNATIVE_REGEX_FEEDBACK
])) {
860 $feedback = html_entity_decode($altregs[ANSWER_ALTERNATIVE_REGEX_FEEDBACK
], ENT_QUOTES
, 'UTF-8');
861 $feedback = str_replace('\}', '}', $feedback);
862 $wrapped->feedback
[] = str_replace('\#', '#', $feedback);
864 $wrapped->feedback
[] = '';
866 if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_NUMERICAL
])
867 && ereg(NUMERICAL_ALTERNATIVE_REGEX
, $altregs[ANSWER_ALTERNATIVE_REGEX_ANSWER
], $numregs)) {
868 $wrapped->answer
[] = $numregs[NUMERICAL_CORRECT_ANSWER
];
869 if ($numregs[NUMERICAL_ABS_ERROR_MARGIN
]) {
870 $wrapped->tolerance
[] =
871 $numregs[NUMERICAL_ABS_ERROR_MARGIN
];
873 $wrapped->tolerance
[] = 0;
875 } else { // Tolerance can stay undefined for non numerical questions
876 // Undo quoting done by the HTML editor.
877 $answer = html_entity_decode($altregs[ANSWER_ALTERNATIVE_REGEX_ANSWER
], ENT_QUOTES
, 'UTF-8');
878 $answer = str_replace('\}', '}', $answer);
879 $wrapped->answer
[] = str_replace('\#', '#', $answer);
881 $tmp = explode($altregs[0], $remainingalts, 2);
882 $remainingalts = $tmp[1];
885 $question->defaultgrade +
= $wrapped->defaultgrade
;
886 $question->options
->questions
[$positionkey] = clone($wrapped);
887 $question->questiontext
= implode("{#$positionkey}",
888 explode($answerregs[0], $question->questiontext
, 2));
890 $question->questiontext
= $question->questiontext
;