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 has_wildcards_in_responses($question, $subqid) {
27 global $QTYPES, $OUTPUT;
28 foreach ($question->options->questions as $subq){
29 if ($subq->id == $subqid){
30 return $QTYPES[$subq->qtype]->has_wildcards_in_responses($subq, $subqid);
33 echo $OUTPUT->notification('Could not find sub question!');
37 function requires_qtypes() {
38 return array('shortanswer', 'numerical', 'multichoice');
41 function get_question_options(&$question) {
42 global $QTYPES, $DB, $OUTPUT;
44 // Get relevant data indexed by positionkey from the multianswers table
45 if (!$sequence = $DB->get_field('question_multianswer', 'sequence', array('question' => $question->id))) {
46 echo $OUTPUT->notification(get_string('noquestions','qtype_multianswer',$question->name));
47 $question->options->questions['1']= '';
51 $wrappedquestions = $DB->get_records_list('question', 'id', explode(',', $sequence), 'id ASC');
53 // We want an array with question ids as index and the positions as values
54 $sequence = array_flip(explode(',', $sequence));
55 array_walk($sequence, create_function('&$val', '$val++;'));
56 //If a question is lost, the corresponding index is null
57 // so this null convention is used to test $question->options->questions
58 // before using the values.
59 // first all possible questions from sequence are nulled
60 // then filled with the data if available in $wrappedquestions
62 foreach($sequence as $seq){
63 $question->options->questions[$seq]= '';
65 if (isset($wrappedquestions) && is_array($wrappedquestions)){
66 foreach ($wrappedquestions as $wrapped) {
67 if (!$QTYPES[$wrapped->qtype]->get_question_options($wrapped)) {
68 echo $OUTPUT->notification("Unable to get options for questiontype {$wrapped->qtype} (id={$wrapped->id})");
70 // for wrapped questions the maxgrade is always equal to the defaultgrade,
71 // there is no entry in the question_instances table for them
72 $wrapped->maxgrade = $wrapped->defaultgrade;
74 $question->options->questions[$sequence[$wrapped->id]] = clone($wrapped); // ??? Why do we need a clone here?
78 if ($nbvaliquestion == 0 ) {
79 echo $OUTPUT->notification(get_string('noquestions','qtype_multianswer',$question->name));
85 function save_question_options($question) {
87 $result = new stdClass;
89 // This function needs to be able to handle the case where the existing set of wrapped
90 // questions does not match the new set of wrapped questions so that some need to be
91 // created, some modified and some deleted
92 // Unfortunately the code currently simply overwrites existing ones in sequence. This
93 // will make re-marking after a re-ordering of wrapped questions impossible and
94 // will also create difficulties if questiontype specific tables reference the id.
96 // First we get all the existing wrapped questions
97 if (!$oldwrappedids = $DB->get_field('question_multianswer', 'sequence', array('question' => $question->id))) {
98 $oldwrappedquestions = array();
100 $oldwrappedquestions = $DB->get_records_list('question', 'id', explode(',', $oldwrappedids), 'id ASC');
103 foreach($question->options->questions as $wrapped) {
104 if (!empty($wrapped)){
105 // if we still have some old wrapped question ids, reuse the next of them
107 if (is_array($oldwrappedquestions) && $oldwrappedquestion = array_shift($oldwrappedquestions)) {
108 $wrapped->id = $oldwrappedquestion->id;
109 if($oldwrappedquestion->qtype != $wrapped->qtype ) {
110 switch ($oldwrappedquestion->qtype) {
112 $DB->delete_records('question_multichoice', array('question' => $oldwrappedquestion->id));
115 $DB->delete_records('question_shortanswer', array('question' => $oldwrappedquestion->id));
118 $DB->delete_records('question_numerical', array('question' => $oldwrappedquestion->id));
121 print_error('qtypenotrecognized', 'qtype_multianswer','',$oldwrappedquestion->qtype);
129 $wrapped->name = $question->name;
130 $wrapped->parent = $question->id;
131 $previousid = $wrapped->id ;
132 $wrapped->category = $question->category . ',1'; // save_question strips this extra bit off again.
133 $wrapped = $QTYPES[$wrapped->qtype]->save_question($wrapped, clone($wrapped));
134 $sequence[] = $wrapped->id;
135 if ($previousid != 0 && $previousid != $wrapped->id ) {
136 // for some reasons a new question has been created
137 // so delete the old one
138 delete_question($previousid) ;
142 // Delete redundant wrapped questions
143 if(is_array($oldwrappedquestions) && count($oldwrappedquestions)){
144 foreach ($oldwrappedquestions as $oldwrappedquestion) {
145 delete_question($oldwrappedquestion->id) ;
149 if (!empty($sequence)) {
150 $multianswer = new stdClass;
151 $multianswer->question = $question->id;
152 $multianswer->sequence = implode(',', $sequence);
153 if ($oldid = $DB->get_field('question_multianswer', 'id', array('question' => $question->id))) {
154 $multianswer->id = $oldid;
155 $DB->update_record("question_multianswer", $multianswer);
157 $DB->insert_record("question_multianswer", $multianswer);
162 function save_question($authorizedquestion, $form) {
163 $question = qtype_multianswer_extract_question($form->questiontext);
164 if (isset($authorizedquestion->id)) {
165 $question->id = $authorizedquestion->id;
168 $question->category = $authorizedquestion->category;
169 $form->course = $course; // To pass the course object to
170 // save_question_options, where it is
171 // needed to call type specific
172 // save_question methods.
173 $form->defaultgrade = $question->defaultgrade;
174 $form->questiontext = $question->questiontext;
175 $form->questiontextformat = 0;
176 $form->options = clone($question->options);
177 unset($question->options);
178 return parent::save_question($question, $form);
181 function create_session_and_responses(&$question, &$state, $cmoptions, $attempt) {
182 $state->responses = array();
183 foreach ($question->options->questions as $key => $wrapped) {
184 $state->responses[$key] = '';
189 function restore_session_and_responses(&$question, &$state) {
190 $responses = explode(',', $state->responses['']);
191 $state->responses = array();
192 foreach ($responses as $response) {
193 $tmp = explode("-", $response);
194 // restore encoded characters
195 $state->responses[$tmp[0]] = str_replace(array(",", "-"),
196 array(",", "-"), $tmp[1]);
201 function save_session_and_responses(&$question, &$state) {
203 $responses = $state->responses;
204 // encode - (hyphen) and , (comma) to - because they are used as
206 array_walk($responses, create_function('&$val, $key',
207 '$val = str_replace(array(",", "-"), array(",", "-"), $val);
208 $val = "$key-$val";'));
209 $responses = implode(',', $responses);
211 // Set the legacy answer field
212 $DB->set_field('question_states', 'answer', $responses, array('id' => $state->id));
216 function delete_question($questionid, $contextid) {
218 $DB->delete_records("question_multianswer", array("question" => $questionid));
220 parent::delete_question($questionid, $contextid);
223 function get_correct_responses(&$question, &$state) {
225 $responses = array();
226 foreach($question->options->questions as $key => $wrapped) {
227 if (!empty($wrapped)){
228 if ($correct = $QTYPES[$wrapped->qtype]->get_correct_responses($wrapped, $state)) {
229 $responses[$key] = $correct[''];
231 // if there is no correct answer to this subquestion then there
232 // can not be a correct answer to the whole question either, so
233 // we have to return null.
241 function get_possible_responses(&$question) {
243 $responses = array();
244 foreach($question->options->questions as $key => $wrapped) {
245 if (!empty($wrapped)){
246 if ($correct = $QTYPES[$wrapped->qtype]->get_possible_responses($wrapped)) {
247 $responses += $correct;
249 // if there is no correct answer to this subquestion then there
250 // can not be a correct answer to the whole question either, so
251 // we have to return null.
258 function get_actual_response_details($question, $state){
261 foreach($question->options->questions as $key => $wrapped) {
262 if (!empty($wrapped)){
263 $stateforquestion = clone($state);
264 $stateforquestion->responses[''] = $state->responses[$key];
265 $details = array_merge($details, $QTYPES[$wrapped->qtype]->get_actual_response_details($wrapped, $stateforquestion));
271 function get_html_head_contributions(&$question, &$state) {
273 parent::get_html_head_contributions($question, $state);
274 $PAGE->requires->js('/lib/overlib/overlib.js', true);
275 $PAGE->requires->js('/lib/overlib/overlib_cssstyle.js', true);
278 function print_question_formulation_and_controls(&$question, &$state, $cmoptions, $options) {
279 global $QTYPES, $CFG, $USER, $OUTPUT, $PAGE;
281 static $overlibdivoutput = false;
282 if (!$overlibdivoutput) {
283 echo '<div id="overDiv" style="position:absolute; visibility:hidden; z-index:1000;"></div>'; // for overlib
284 $overlibdivoutput = true;
287 $readonly = empty($options->readonly) ? '' : 'readonly="readonly"';
288 $disabled = empty($options->readonly) ? '' : 'disabled="disabled"';
289 $formatoptions = new stdClass;
290 $formatoptions->noclean = true;
291 $formatoptions->para = false;
292 $nameprefix = $question->name_prefix;
294 // adding an icon with alt to warn user this is a fill in the gap question
296 if (!empty($USER->screenreader)) {
297 echo "<img src=\"".$OUTPUT->pix_url('icon', 'qtype_'.$question->qtype)."\" ".
298 "class=\"icon\" alt=\"".get_string('clozeaid','qtype_multichoice')."\" /> ";
301 echo '<div class="ablock clearfix">';
303 $qtextremaining = format_text($question->questiontext,
304 $question->questiontextformat, $formatoptions, $cmoptions->course);
306 $strfeedback = get_string('feedback', 'quiz');
308 // The regex will recognize text snippets of type {#X}
309 // where the X can be any text not containg } or white-space characters.
310 while (preg_match('~\{#([^[:space:]}]*)}~', $qtextremaining, $regs)) {
311 $qtextsplits = explode($regs[0], $qtextremaining, 2);
312 echo $qtextsplits[0];
313 echo "<label>"; // MDL-7497
314 $qtextremaining = $qtextsplits[1];
316 $positionkey = $regs[1];
317 if (isset($question->options->questions[$positionkey]) && $question->options->questions[$positionkey] != ''){
318 $wrapped = &$question->options->questions[$positionkey];
319 $answers = &$wrapped->options->answers;
320 // $correctanswers = $QTYPES[$wrapped->qtype]->get_correct_responses($wrapped, $state);
322 $inputname = $nameprefix.$positionkey;
323 if (isset($state->responses[$positionkey])) {
324 $response = $state->responses[$positionkey];
328 // echo "<p> multianswer positionkey $positionkey response $response state <pre>";print_r($state);echo "</pre></p>";
330 // Determine feedback popup if any
336 $strfeedbackwrapped = $strfeedback;
337 $testedstate = clone($state);
338 if ($correctanswers = $QTYPES[$wrapped->qtype]->get_correct_responses($wrapped, $state)) {
339 if ($options->readonly && $options->correct_responses) {
341 if ($correctanswers) {
342 foreach ($correctanswers as $ca) {
343 switch($wrapped->qtype){
346 $correctanswer .= $delimiter.$ca;
349 if (isset($answers[$ca])){
350 $correctanswer .= $delimiter.$answers[$ca]->answer;
358 if ($correctanswer != '' ) {
359 $feedback = '<div class="correctness">';
360 $feedback .= get_string('correctansweris', 'quiz', s($correctanswer));
361 $feedback .= '</div>';
365 if ($options->feedback) {
366 $chosenanswer = null;
367 switch ($wrapped->qtype) {
370 $testedstate = clone($state);
371 $testedstate->responses[''] = $response;
372 foreach ($answers as $answer) {
373 if($QTYPES[$wrapped->qtype]
374 ->test_response($wrapped, $testedstate, $answer)) {
375 $chosenanswer = clone($answer);
381 if (isset($answers[$response])) {
382 $chosenanswer = clone($answers[$response]);
389 // Set up a default chosenanswer so that all non-empty wrong
390 // answers are highlighted red
391 if (empty($chosenanswer) && $response != '') {
392 $chosenanswer = new stdClass;
393 $chosenanswer->fraction = 0.0;
396 if (!empty($chosenanswer->feedback)) {
397 $feedback = s(str_replace(array("\\", "'"), array("\\\\", "\\'"), $feedback.$chosenanswer->feedback));
398 if ($options->readonly && $options->correct_responses) {
399 $strfeedbackwrapped = get_string('correctanswerandfeedback', 'qtype_multianswer');
401 $strfeedbackwrapped = get_string('feedback', 'quiz');
403 $popup = " onmouseover=\"return overlib('$feedback', STICKY, MOUSEOFF, CAPTION, '$strfeedbackwrapped', FGCOLOR, '#FFFFFF');\" ".
404 " onmouseout=\"return nd();\" ";
408 if ($options->feedback && $response != '') {
409 $style = 'class = "'.question_get_feedback_class($chosenanswer->fraction).'"';
410 $feedbackimg = question_get_feedback_image($chosenanswer->fraction);
416 if ($feedback !='' && $popup == ''){
417 $strfeedbackwrapped = get_string('correctanswer', 'qtype_multianswer');
418 $feedback = s(str_replace(array("\\", "'"), array("\\\\", "\\'"), $feedback));
419 $popup = " onmouseover=\"return overlib('$feedback', STICKY, MOUSEOFF, CAPTION, '$strfeedbackwrapped', FGCOLOR, '#FFFFFF');\" ".
420 " onmouseout=\"return nd();\" ";
423 // Print the input control
424 switch ($wrapped->qtype) {
428 foreach ($answers as $answer) {
429 if (strlen(trim($answer->answer)) > $size ){
430 $size = strlen(trim($answer->answer));
433 if (strlen(trim($response))> $size ){
434 $size = strlen(trim($response))+1;
436 $size = $size + rand(0,$size*0.15);
437 $size > 60 ? $size = 60 : $size = $size;
438 $styleinfo = "size=\"$size\"";
440 * Uncomment the following lines if you want to limit for small sizes.
441 * Results may vary with browsers see MDL-3274
445 $styleinfo = 'style="width: 1.1em;"';
448 $styleinfo = 'style="width: 1.9em;"';
451 $styleinfo = 'style="width: 2.3em;"';
454 $styleinfo = 'style="width: 2.8em;"';
458 echo "<input $style $readonly $popup name=\"$inputname\"";
459 echo " type=\"text\" value=\"".s($response)."\" ".$styleinfo." /> ";
460 if (!empty($feedback) && !empty($USER->screenreader)) {
461 echo "<img src=\"" . $OUTPUT->pix_url('i/feedback') . "\" alt=\"$feedback\" />";
466 if ($wrapped->options->layout == 0 ){
467 $outputoptions = '<option></option>'; // Default empty option
468 foreach ($answers as $mcanswer) {
470 if ($response == $mcanswer->id) {
471 $selected = ' selected="selected"';
473 $outputoptions .= "<option value=\"$mcanswer->id\"$selected>" .
474 s($mcanswer->answer) . '</option>';
476 // In the next line, $readonly is invalid HTML, but it works in
477 // all browsers. $disabled would be valid, but then the JS for
478 // displaying the feedback does not work. Of course, we should
479 // not be relying on JS (for accessibility reasons), but that is
482 // The span is used for safari, which does not allow styling of
484 echo "<span $style><select $popup $readonly $style name=\"$inputname\">";
486 echo '</select></span>';
487 if (!empty($feedback) && !empty($USER->screenreader)) {
488 echo "<img src=\"" . $OUTPUT->pix_url('i/feedback') . "\" alt=\"$feedback\" />";
491 }else if ($wrapped->options->layout == 1 || $wrapped->options->layout == 2){
494 foreach ($answers as $mcanswer) {
498 $type = 'type="radio"';
499 $name = "name=\"{$inputname}\"";
500 if ($response == $mcanswer->id) {
501 $checked = 'checked="checked"';
505 $a->id = $question->name_prefix . $mcanswer->id;
507 $a->feedbackimg = '';
510 $a->control = "<input $readonly id=\"$a->id\" $name $checked $type value=\"$mcanswer->id\" />";
511 if ($options->correct_responses && $mcanswer->fraction > 0) {
512 $a->class = question_get_feedback_class(1);
514 if (($options->feedback && $chosen) || $options->correct_responses) {
515 if ($type == ' type="checkbox" ') {
516 $a->feedbackimg = question_get_feedback_image($mcanswer->fraction > 0 ? 1 : 0, $chosen && $options->feedback);
518 $a->feedbackimg = question_get_feedback_image($mcanswer->fraction, $chosen && $options->feedback);
522 // Print the answer text: no automatic numbering
524 $a->text = format_text($mcanswer->answer, $mcanswer->answerformat, $formatoptions, $cmoptions->course);
526 // Print feedback if feedback is on
527 if (($options->feedback || $options->correct_responses) && ($checked )) { //|| $options->readonly
528 $a->feedback = format_text($mcanswer->feedback, $mcanswer->feedbackformat, $formatoptions, $cmoptions->course);
536 <?php if ($wrapped->options->layout == 1 ){
538 <table class="answer">
539 <?php $row = 1; foreach ($anss as $answer) { ?>
540 <tr class="<?php echo 'r'.$row = $row ? 0 : 1; ?>">
541 <td class="c0 control">
542 <?php echo $answer->control; ?>
544 <td class="c1 text <?php echo $answer->class ?>">
545 <label for="<?php echo $answer->id ?>">
546 <?php echo $answer->text; ?>
547 <?php echo $answer->feedbackimg; ?>
550 <td class="c0 feedback">
551 <?php echo $answer->feedback; ?>
556 <?php }else if ($wrapped->options->layout == 2 ){
559 <table class="answer">
560 <tr class="<?php echo 'r'.$row = $row ? 0 : 1; ?>">
561 <?php $row = 1; foreach ($anss as $answer) { ?>
562 <td class="c0 control">
563 <?php echo $answer->control; ?>
565 <td class="c1 text <?php echo $answer->class ?>">
566 <label for="<?php echo $answer->id ?>">
567 <?php echo $answer->text; ?>
568 <?php echo $answer->feedbackimg; ?>
571 <td class="c0 feedback">
572 <?php echo $answer->feedback; ?>
580 echo "no valid layout";
586 $a->type = $wrapped->qtype ;
587 $a->sub = $positionkey;
588 print_error('unknownquestiontypeofsubquestion', 'qtype_multianswer','',$a);
591 echo "</label>"; // MDL-7497
594 if(! isset($question->options->questions[$positionkey])){
595 echo $regs[0]."</label>";
597 echo '</label><div class="error" >'.get_string('questionnotfound','qtype_multianswer',$positionkey).'</div>';
602 // Print the final piece of question text:
603 echo $qtextremaining;
604 $this->print_question_submit_buttons($question, $state, $cmoptions, $options);
608 function grade_responses(&$question, &$state, $cmoptions) {
610 $teststate = clone($state);
611 $state->raw_grade = 0;
612 foreach($question->options->questions as $key => $wrapped) {
613 if (!empty($wrapped)){
614 if(isset($state->responses[$key])){
615 $state->responses[$key] = $state->responses[$key];
617 $state->responses[$key] = '' ;
619 $teststate->responses = array('' => $state->responses[$key]);
620 $teststate->raw_grade = 0;
621 if (false === $QTYPES[$wrapped->qtype]
622 ->grade_responses($wrapped, $teststate, $cmoptions)) {
625 $state->raw_grade += $teststate->raw_grade;
628 $state->raw_grade /= $question->defaultgrade;
629 $state->raw_grade = min(max((float) $state->raw_grade, 0.0), 1.0)
630 * $question->maxgrade;
632 if (empty($state->raw_grade)) {
633 $state->raw_grade = 0.0;
635 $state->penalty = $question->penalty * $question->maxgrade;
637 // mark the state as graded
638 $state->event = ($state->event == QUESTION_EVENTCLOSE) ? QUESTION_EVENTCLOSEANDGRADE : QUESTION_EVENTGRADE;
643 function get_actual_response($question, $state) {
645 $teststate = clone($state);
646 foreach($question->options->questions as $key => $wrapped) {
647 $state->responses[$key] = html_entity_decode($state->responses[$key]);
648 $teststate->responses = array('' => $state->responses[$key]);
649 $correct = $QTYPES[$wrapped->qtype]
650 ->get_actual_response($wrapped, $teststate);
651 $responses[$key] = implode(';', $correct);
657 * @param object $question
658 * @return mixed either a integer score out of 1 that the average random
659 * guess by a student might give or an empty string which means will not
662 function get_random_guess_score($question) {
664 foreach (array_keys($question->options->questions) as $key){
665 $totalfraction += question_get_random_guess_score($question->options->questions[$key]);
667 return $totalfraction / count($question->options->questions);
671 * Runs all the code required to set up and save an essay question for testing purposes.
672 * Alternate DB table prefix may be used to facilitate data deletion.
674 function generate_test($name, $courseid = null) {
676 list($form, $question) = parent::generate_test($name, $courseid);
677 $question->category = $form->category;
678 $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}.
680 Note that addresses like www.moodle.org and smileys :-) all work as normal:
681 a) How good is this? {:MULTICHOICE:=Yes#Correct~No#We have a different opinion}
682 b) What grade would you give it? {3:NUMERICAL:=3:2}
686 $form->feedback = "feedback";
687 $form->generalfeedback = "General feedback";
689 $form->penalty = 0.1;
690 $form->versioning = 0;
693 $course = $DB->get_record('course', array('id' => $courseid));
696 return $this->save_question($question, $form);
700 //// END OF CLASS ////
703 //////////////////////////////////////////////////////////////////////////
704 //// INITIATION - Without this line the question type is not in use... ///
705 //////////////////////////////////////////////////////////////////////////
706 question_register_questiontype(new embedded_cloze_qtype());
708 /////////////////////////////////////////////////////////////
709 //// ADDITIONAL FUNCTIONS
710 //// The functions below deal exclusivly with editing
711 //// of questions with question type 'multianswer'.
712 //// Therefore they are kept in this file.
713 //// They are not in the class as they are not
714 //// likely to be subject for overriding.
715 /////////////////////////////////////////////////////////////
717 // ANSWER_ALTERNATIVE regexes
718 define("ANSWER_ALTERNATIVE_FRACTION_REGEX",
720 // for the syntax '(?<!' see http://www.perl.com/doc/manual/html/pod/perlre.html#item_C
721 define("ANSWER_ALTERNATIVE_ANSWER_REGEX",
722 '.+?(?<!\\\\|&|&)(?=[~#}]|$)');
723 define("ANSWER_ALTERNATIVE_FEEDBACK_REGEX",
724 '.*?(?<!\\\\)(?=[~}]|$)');
725 define("ANSWER_ALTERNATIVE_REGEX",
726 '(' . ANSWER_ALTERNATIVE_FRACTION_REGEX .')?' .
727 '(' . ANSWER_ALTERNATIVE_ANSWER_REGEX . ')' .
728 '(#(' . ANSWER_ALTERNATIVE_FEEDBACK_REGEX .'))?');
730 // Parenthesis positions for ANSWER_ALTERNATIVE_REGEX
731 define("ANSWER_ALTERNATIVE_REGEX_PERCENTILE_FRACTION", 2);
732 define("ANSWER_ALTERNATIVE_REGEX_FRACTION", 1);
733 define("ANSWER_ALTERNATIVE_REGEX_ANSWER", 3);
734 define("ANSWER_ALTERNATIVE_REGEX_FEEDBACK", 5);
736 // NUMBER_FORMATED_ALTERNATIVE_ANSWER_REGEX is used
737 // for identifying numerical answers in ANSWER_ALTERNATIVE_REGEX_ANSWER
738 define("NUMBER_REGEX",
739 '-?(([0-9]+[.,]?[0-9]*|[.,][0-9]+)([eE][-+]?[0-9]+)?)');
740 define("NUMERICAL_ALTERNATIVE_REGEX",
741 '^(' . NUMBER_REGEX . ')(:' . NUMBER_REGEX . ')?$');
743 // Parenthesis positions for NUMERICAL_FORMATED_ALTERNATIVE_ANSWER_REGEX
744 define("NUMERICAL_CORRECT_ANSWER", 1);
745 define("NUMERICAL_ABS_ERROR_MARGIN", 6);
747 // Remaining ANSWER regexes
748 define("ANSWER_TYPE_DEF_REGEX",
749 '(NUMERICAL|NM)|(MULTICHOICE|MC)|(MULTICHOICE_V|MCV)|(MULTICHOICE_H|MCH)|(SHORTANSWER|SA|MW)|(SHORTANSWER_C|SAC|MWC)');
750 define("ANSWER_START_REGEX",
751 '\{([0-9]*):(' . ANSWER_TYPE_DEF_REGEX . '):');
753 define("ANSWER_REGEX",
755 . '(' . ANSWER_ALTERNATIVE_REGEX
757 . ANSWER_ALTERNATIVE_REGEX
760 // Parenthesis positions for singulars in ANSWER_REGEX
761 define("ANSWER_REGEX_NORM", 1);
762 define("ANSWER_REGEX_ANSWER_TYPE_NUMERICAL", 3);
763 define("ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE", 4);
764 define("ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_REGULAR", 5);
765 define("ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_HORIZONTAL", 6);
766 define("ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER", 7);
767 define("ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER_C", 8);
768 define("ANSWER_REGEX_ALTERNATIVES", 9);
770 function qtype_multianswer_extract_question($text) {
771 // $text is an array [text][format][itemid]
772 $question = new stdClass;
773 $question->qtype = 'multianswer';
774 $question->questiontext = $text;
775 $question->generalfeedback['text'] = '';
776 $question->generalfeedback['format'] = '1';
777 $question->generalfeedback['itemid'] = '';
779 $question->options->questions = array();
780 $question->defaultgrade = 0; // Will be increased for each answer norm
782 for ($positionkey=1; preg_match('/'.ANSWER_REGEX.'/', $question->questiontext['text'], $answerregs); ++$positionkey ) {
783 $wrapped = new stdClass;
784 $wrapped->generalfeedback['text'] = '';
785 $wrapped->generalfeedback['format'] = '1';
786 $wrapped->generalfeedback['itemid'] = '';
787 if (isset($answerregs[ANSWER_REGEX_NORM])&& $answerregs[ANSWER_REGEX_NORM]!== ''){
788 $wrapped->defaultgrade = $answerregs[ANSWER_REGEX_NORM];
790 $wrapped->defaultgrade = '1';
792 if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_NUMERICAL])) {
793 $wrapped->qtype = 'numerical';
794 $wrapped->multiplier = array();
795 $wrapped->units = array();
796 $wrapped->instructions['text'] = '';
797 $wrapped->instructions['format'] = '1';
798 $wrapped->instructions['itemid'] = '';
799 } else if(!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER])) {
800 $wrapped->qtype = 'shortanswer';
801 $wrapped->usecase = 0;
802 } else if(!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER_C])) {
803 $wrapped->qtype = 'shortanswer';
804 $wrapped->usecase = 1;
805 } else if(!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE])) {
806 $wrapped->qtype = 'multichoice';
807 $wrapped->single = 1;
808 $wrapped->answernumbering = 0;
809 $wrapped->correctfeedback['text'] = '';
810 $wrapped->correctfeedback['format'] = '1';
811 $wrapped->correctfeedback['itemid'] = '';
812 $wrapped->partiallycorrectfeedback['text'] = '';
813 $wrapped->partiallycorrectfeedback['format'] = '1';
814 $wrapped->partiallycorrectfeedback['itemid'] = '';
815 $wrapped->incorrectfeedback['text'] = '';
816 $wrapped->incorrectfeedback['format'] = '1';
817 $wrapped->incorrectfeedback['itemid'] = '';
818 $wrapped->layout = 0;
819 } else if(!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_REGULAR])) {
820 $wrapped->qtype = 'multichoice';
821 $wrapped->single = 1;
822 $wrapped->answernumbering = 0;
823 $wrapped->correctfeedback['text'] = '';
824 $wrapped->correctfeedback['format'] = '1';
825 $wrapped->correctfeedback['itemid'] = '';
826 $wrapped->partiallycorrectfeedback['text'] = '';
827 $wrapped->partiallycorrectfeedback['format'] = '1';
828 $wrapped->partiallycorrectfeedback['itemid'] = '';
829 $wrapped->incorrectfeedback['text'] = '';
830 $wrapped->incorrectfeedback['format'] = '1';
831 $wrapped->incorrectfeedback['itemid'] = '';
832 $wrapped->layout = 1;
833 } else if(!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_HORIZONTAL])) {
834 $wrapped->qtype = 'multichoice';
835 $wrapped->single = 1;
836 $wrapped->answernumbering = 0;
837 $wrapped->correctfeedback['text'] = '';
838 $wrapped->correctfeedback['format'] = '1';
839 $wrapped->correctfeedback['itemid'] = '';
840 $wrapped->partiallycorrectfeedback['text'] = '';
841 $wrapped->partiallycorrectfeedback['format'] = '1';
842 $wrapped->partiallycorrectfeedback['itemid'] = '';
843 $wrapped->incorrectfeedback['text'] = '';
844 $wrapped->incorrectfeedback['format'] = '1';
845 $wrapped->incorrectfeedback['itemid'] = '';
846 $wrapped->layout = 2;
848 print_error('unknownquestiontype', 'question', '', $answerregs[2]);
852 // Each $wrapped simulates a $form that can be processed by the
853 // respective save_question and save_question_options methods of the
854 // wrapped questiontypes
855 $wrapped->answer = array();
856 $wrapped->fraction = array();
857 $wrapped->feedback = array();
858 $wrapped->shuffleanswers = 1;
859 $wrapped->questiontext['text'] = $answerregs[0];
860 $wrapped->questiontext['format'] = 0 ;
861 $wrapped->questiontext['itemid'] = '' ;
864 $remainingalts = $answerregs[ANSWER_REGEX_ALTERNATIVES];
865 while (preg_match('/~?'.ANSWER_ALTERNATIVE_REGEX.'/', $remainingalts, $altregs)) {
866 if ('=' == $altregs[ANSWER_ALTERNATIVE_REGEX_FRACTION]) {
867 $wrapped->fraction["$answerindex"] = '1';
868 } else if ($percentile = $altregs[ANSWER_ALTERNATIVE_REGEX_PERCENTILE_FRACTION]){
869 $wrapped->fraction["$answerindex"] = .01 * $percentile;
871 $wrapped->fraction["$answerindex"] = '0';
873 if (isset($altregs[ANSWER_ALTERNATIVE_REGEX_FEEDBACK])) {
874 $feedback = html_entity_decode($altregs[ANSWER_ALTERNATIVE_REGEX_FEEDBACK], ENT_QUOTES, 'UTF-8');
875 $feedback = str_replace('\}', '}', $feedback);
876 $wrapped->feedback["$answerindex"]['text'] = str_replace('\#', '#', $feedback);
877 $wrapped->feedback["$answerindex"]['format'] = '1';
878 $wrapped->feedback["$answerindex"]['itemid'] = '';
880 $wrapped->feedback["$answerindex"]['text'] = '';
881 $wrapped->feedback["$answerindex"]['format'] = '1';
882 $wrapped->feedback["$answerindex"]['itemid'] = '1';
885 if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_NUMERICAL])
886 && preg_match('~'.NUMERICAL_ALTERNATIVE_REGEX.'~', $altregs[ANSWER_ALTERNATIVE_REGEX_ANSWER], $numregs)) {
887 $wrapped->answer[] = $numregs[NUMERICAL_CORRECT_ANSWER];
888 if ($numregs[NUMERICAL_ABS_ERROR_MARGIN]) {
889 $wrapped->tolerance["$answerindex"] =
890 $numregs[NUMERICAL_ABS_ERROR_MARGIN];
892 $wrapped->tolerance["$answerindex"] = 0;
894 } else { // Tolerance can stay undefined for non numerical questions
895 // Undo quoting done by the HTML editor.
896 $answer = html_entity_decode($altregs[ANSWER_ALTERNATIVE_REGEX_ANSWER], ENT_QUOTES, 'UTF-8');
897 $answer = str_replace('\}', '}', $answer);
898 $wrapped->answer["$answerindex"] = str_replace('\#', '#', $answer);
900 $tmp = explode($altregs[0], $remainingalts, 2);
901 $remainingalts = $tmp[1];
905 $question->defaultgrade += $wrapped->defaultgrade;
906 $question->options->questions[$positionkey] = clone($wrapped);
907 $question->questiontext['text'] = implode("{#$positionkey}",
908 explode($answerregs[0], $question->questiontext['text'], 2));
909 // echo"<p>questiontext 2 <pre>";print_r($question->questiontext);echo"<pre></p>";
911 // echo"<p>questiontext<pre>";print_r($question->questiontext);echo"<pre></p>";
912 $question->questiontext = $question->questiontext;
913 // echo"<p>question<pre>";print_r($question);echo"<pre></p>";