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 $readonly = empty($options->readonly) ? '' : 'readonly="readonly"';
282 $disabled = empty($options->readonly) ? '' : 'disabled="disabled"';
283 $formatoptions = new stdClass;
284 $formatoptions->noclean = true;
285 $formatoptions->para = false;
286 $nameprefix = $question->name_prefix;
288 // adding an icon with alt to warn user this is a fill in the gap question
290 if (!empty($USER->screenreader)) {
291 echo "<img src=\"".$OUTPUT->pix_url('icon', 'qtype_'.$question->qtype)."\" ".
292 "class=\"icon\" alt=\"".get_string('clozeaid','qtype_multichoice')."\" /> ";
295 echo '<div class="ablock clearfix">';
297 $qtextremaining = format_text($question->questiontext,
298 $question->questiontextformat, $formatoptions, $cmoptions->course);
300 $strfeedback = get_string('feedback', 'quiz');
302 // The regex will recognize text snippets of type {#X}
303 // where the X can be any text not containg } or white-space characters.
304 while (preg_match('~\{#([^[:space:]}]*)}~', $qtextremaining, $regs)) {
305 $qtextsplits = explode($regs[0], $qtextremaining, 2);
306 echo $qtextsplits[0];
307 echo "<label>"; // MDL-7497
308 $qtextremaining = $qtextsplits[1];
310 $positionkey = $regs[1];
311 if (isset($question->options->questions[$positionkey]) && $question->options->questions[$positionkey] != ''){
312 $wrapped = &$question->options->questions[$positionkey];
313 $answers = &$wrapped->options->answers;
314 // $correctanswers = $QTYPES[$wrapped->qtype]->get_correct_responses($wrapped, $state);
316 $inputname = $nameprefix.$positionkey;
317 if (isset($state->responses[$positionkey])) {
318 $response = $state->responses[$positionkey];
322 // echo "<p> multianswer positionkey $positionkey response $response state <pre>";print_r($state);echo "</pre></p>";
324 // Determine feedback popup if any
330 $strfeedbackwrapped = $strfeedback;
331 $testedstate = clone($state);
332 if ($correctanswers = $QTYPES[$wrapped->qtype]->get_correct_responses($wrapped, $state)) {
333 if ($options->readonly && $options->correct_responses) {
335 if ($correctanswers) {
336 foreach ($correctanswers as $ca) {
337 switch($wrapped->qtype){
340 $correctanswer .= $delimiter.$ca;
343 if (isset($answers[$ca])){
344 $correctanswer .= $delimiter.$answers[$ca]->answer;
352 if ($correctanswer != '' ) {
353 $feedback = '<div class="correctness">';
354 $feedback .= get_string('correctansweris', 'quiz', s($correctanswer));
355 $feedback .= '</div>';
359 if ($options->feedback) {
360 $chosenanswer = null;
361 switch ($wrapped->qtype) {
364 $testedstate = clone($state);
365 $testedstate->responses[''] = $response;
366 foreach ($answers as $answer) {
367 if($QTYPES[$wrapped->qtype]
368 ->test_response($wrapped, $testedstate, $answer)) {
369 $chosenanswer = clone($answer);
375 if (isset($answers[$response])) {
376 $chosenanswer = clone($answers[$response]);
383 // Set up a default chosenanswer so that all non-empty wrong
384 // answers are highlighted red
385 if (empty($chosenanswer) && $response != '') {
386 $chosenanswer = new stdClass;
387 $chosenanswer->fraction = 0.0;
390 if (!empty($chosenanswer->feedback)) {
391 $feedback = s(str_replace(array("\\", "'"), array("\\\\", "\\'"), $feedback.$chosenanswer->feedback));
392 if ($options->readonly && $options->correct_responses) {
393 $strfeedbackwrapped = get_string('correctanswerandfeedback', 'qtype_multianswer');
395 $strfeedbackwrapped = get_string('feedback', 'quiz');
397 $popup = " onmouseover=\"return overlib('$feedback', STICKY, MOUSEOFF, CAPTION, '$strfeedbackwrapped', FGCOLOR, '#FFFFFF');\" ".
398 " onmouseout=\"return nd();\" ";
402 if ($options->feedback && $response != '') {
403 $style = 'class = "'.question_get_feedback_class($chosenanswer->fraction).'"';
404 $feedbackimg = question_get_feedback_image($chosenanswer->fraction);
410 if ($feedback !='' && $popup == ''){
411 $strfeedbackwrapped = get_string('correctanswer', 'qtype_multianswer');
412 $feedback = s(str_replace(array("\\", "'"), array("\\\\", "\\'"), $feedback));
413 $popup = " onmouseover=\"return overlib('$feedback', STICKY, MOUSEOFF, CAPTION, '$strfeedbackwrapped', FGCOLOR, '#FFFFFF');\" ".
414 " onmouseout=\"return nd();\" ";
417 // Print the input control
418 switch ($wrapped->qtype) {
422 foreach ($answers as $answer) {
423 if (strlen(trim($answer->answer)) > $size ){
424 $size = strlen(trim($answer->answer));
427 if (strlen(trim($response))> $size ){
428 $size = strlen(trim($response))+1;
430 $size = $size + rand(0,$size*0.15);
431 $size > 60 ? $size = 60 : $size = $size;
432 $styleinfo = "size=\"$size\"";
434 * Uncomment the following lines if you want to limit for small sizes.
435 * Results may vary with browsers see MDL-3274
439 $styleinfo = 'style="width: 1.1em;"';
442 $styleinfo = 'style="width: 1.9em;"';
445 $styleinfo = 'style="width: 2.3em;"';
448 $styleinfo = 'style="width: 2.8em;"';
452 echo "<input $style $readonly $popup name=\"$inputname\"";
453 echo " type=\"text\" value=\"".s($response)."\" ".$styleinfo." /> ";
454 if (!empty($feedback) && !empty($USER->screenreader)) {
455 echo "<img src=\"" . $OUTPUT->pix_url('i/feedback') . "\" alt=\"$feedback\" />";
460 if ($wrapped->options->layout == 0 ){
461 $outputoptions = '<option></option>'; // Default empty option
462 foreach ($answers as $mcanswer) {
464 if ($response == $mcanswer->id) {
465 $selected = ' selected="selected"';
467 $outputoptions .= "<option value=\"$mcanswer->id\"$selected>" .
468 s($mcanswer->answer) . '</option>';
470 // In the next line, $readonly is invalid HTML, but it works in
471 // all browsers. $disabled would be valid, but then the JS for
472 // displaying the feedback does not work. Of course, we should
473 // not be relying on JS (for accessibility reasons), but that is
476 // The span is used for safari, which does not allow styling of
478 echo "<span $style><select $popup $readonly $style name=\"$inputname\">";
480 echo '</select></span>';
481 if (!empty($feedback) && !empty($USER->screenreader)) {
482 echo "<img src=\"" . $OUTPUT->pix_url('i/feedback') . "\" alt=\"$feedback\" />";
485 }else if ($wrapped->options->layout == 1 || $wrapped->options->layout == 2){
488 foreach ($answers as $mcanswer) {
492 $type = 'type="radio"';
493 $name = "name=\"{$inputname}\"";
494 if ($response == $mcanswer->id) {
495 $checked = 'checked="checked"';
499 $a->id = $question->name_prefix . $mcanswer->id;
501 $a->feedbackimg = '';
504 $a->control = "<input $readonly id=\"$a->id\" $name $checked $type value=\"$mcanswer->id\" />";
505 if ($options->correct_responses && $mcanswer->fraction > 0) {
506 $a->class = question_get_feedback_class(1);
508 if (($options->feedback && $chosen) || $options->correct_responses) {
509 if ($type == ' type="checkbox" ') {
510 $a->feedbackimg = question_get_feedback_image($mcanswer->fraction > 0 ? 1 : 0, $chosen && $options->feedback);
512 $a->feedbackimg = question_get_feedback_image($mcanswer->fraction, $chosen && $options->feedback);
516 // Print the answer text: no automatic numbering
518 $a->text = format_text($mcanswer->answer, $mcanswer->answerformat, $formatoptions, $cmoptions->course);
520 // Print feedback if feedback is on
521 if (($options->feedback || $options->correct_responses) && ($checked )) { //|| $options->readonly
522 $a->feedback = format_text($mcanswer->feedback, $mcanswer->feedbackformat, $formatoptions, $cmoptions->course);
530 <?php if ($wrapped->options->layout == 1 ){
532 <table class="answer">
533 <?php $row = 1; foreach ($anss as $answer) { ?>
534 <tr class="<?php echo 'r'.$row = $row ? 0 : 1; ?>">
535 <td class="c0 control">
536 <?php echo $answer->control; ?>
538 <td class="c1 text <?php echo $answer->class ?>">
539 <label for="<?php echo $answer->id ?>">
540 <?php echo $answer->text; ?>
541 <?php echo $answer->feedbackimg; ?>
544 <td class="c0 feedback">
545 <?php echo $answer->feedback; ?>
550 <?php }else if ($wrapped->options->layout == 2 ){
553 <table class="answer">
554 <tr class="<?php echo 'r'.$row = $row ? 0 : 1; ?>">
555 <?php $row = 1; foreach ($anss as $answer) { ?>
556 <td class="c0 control">
557 <?php echo $answer->control; ?>
559 <td class="c1 text <?php echo $answer->class ?>">
560 <label for="<?php echo $answer->id ?>">
561 <?php echo $answer->text; ?>
562 <?php echo $answer->feedbackimg; ?>
565 <td class="c0 feedback">
566 <?php echo $answer->feedback; ?>
574 echo "no valid layout";
580 $a->type = $wrapped->qtype ;
581 $a->sub = $positionkey;
582 print_error('unknownquestiontypeofsubquestion', 'qtype_multianswer','',$a);
585 echo "</label>"; // MDL-7497
588 if(! isset($question->options->questions[$positionkey])){
589 echo $regs[0]."</label>";
591 echo '</label><div class="error" >'.get_string('questionnotfound','qtype_multianswer',$positionkey).'</div>';
596 // Print the final piece of question text:
597 echo $qtextremaining;
598 $this->print_question_submit_buttons($question, $state, $cmoptions, $options);
602 function grade_responses(&$question, &$state, $cmoptions) {
604 $teststate = clone($state);
605 $state->raw_grade = 0;
606 foreach($question->options->questions as $key => $wrapped) {
607 if (!empty($wrapped)){
608 if(isset($state->responses[$key])){
609 $state->responses[$key] = $state->responses[$key];
611 $state->responses[$key] = '' ;
613 $teststate->responses = array('' => $state->responses[$key]);
614 $teststate->raw_grade = 0;
615 if (false === $QTYPES[$wrapped->qtype]
616 ->grade_responses($wrapped, $teststate, $cmoptions)) {
619 $state->raw_grade += $teststate->raw_grade;
622 $state->raw_grade /= $question->defaultgrade;
623 $state->raw_grade = min(max((float) $state->raw_grade, 0.0), 1.0)
624 * $question->maxgrade;
626 if (empty($state->raw_grade)) {
627 $state->raw_grade = 0.0;
629 $state->penalty = $question->penalty * $question->maxgrade;
631 // mark the state as graded
632 $state->event = ($state->event == QUESTION_EVENTCLOSE) ? QUESTION_EVENTCLOSEANDGRADE : QUESTION_EVENTGRADE;
637 function get_actual_response($question, $state) {
639 $teststate = clone($state);
640 foreach($question->options->questions as $key => $wrapped) {
641 $state->responses[$key] = html_entity_decode($state->responses[$key]);
642 $teststate->responses = array('' => $state->responses[$key]);
643 $correct = $QTYPES[$wrapped->qtype]
644 ->get_actual_response($wrapped, $teststate);
645 $responses[$key] = implode(';', $correct);
651 * @param object $question
652 * @return mixed either a integer score out of 1 that the average random
653 * guess by a student might give or an empty string which means will not
656 function get_random_guess_score($question) {
658 foreach (array_keys($question->options->questions) as $key){
659 $totalfraction += question_get_random_guess_score($question->options->questions[$key]);
661 return $totalfraction / count($question->options->questions);
665 * Runs all the code required to set up and save an essay question for testing purposes.
666 * Alternate DB table prefix may be used to facilitate data deletion.
668 function generate_test($name, $courseid = null) {
670 list($form, $question) = parent::generate_test($name, $courseid);
671 $question->category = $form->category;
672 $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}.
674 Note that addresses like www.moodle.org and smileys :-) all work as normal:
675 a) How good is this? {:MULTICHOICE:=Yes#Correct~No#We have a different opinion}
676 b) What grade would you give it? {3:NUMERICAL:=3:2}
680 $form->feedback = "feedback";
681 $form->generalfeedback = "General feedback";
683 $form->penalty = 0.1;
684 $form->versioning = 0;
687 $course = $DB->get_record('course', array('id' => $courseid));
690 return $this->save_question($question, $form);
694 //// END OF CLASS ////
697 //////////////////////////////////////////////////////////////////////////
698 //// INITIATION - Without this line the question type is not in use... ///
699 //////////////////////////////////////////////////////////////////////////
700 question_register_questiontype(new embedded_cloze_qtype());
702 /////////////////////////////////////////////////////////////
703 //// ADDITIONAL FUNCTIONS
704 //// The functions below deal exclusivly with editing
705 //// of questions with question type 'multianswer'.
706 //// Therefore they are kept in this file.
707 //// They are not in the class as they are not
708 //// likely to be subject for overriding.
709 /////////////////////////////////////////////////////////////
711 // ANSWER_ALTERNATIVE regexes
712 define("ANSWER_ALTERNATIVE_FRACTION_REGEX",
714 // for the syntax '(?<!' see http://www.perl.com/doc/manual/html/pod/perlre.html#item_C
715 define("ANSWER_ALTERNATIVE_ANSWER_REGEX",
716 '.+?(?<!\\\\|&|&)(?=[~#}]|$)');
717 define("ANSWER_ALTERNATIVE_FEEDBACK_REGEX",
718 '.*?(?<!\\\\)(?=[~}]|$)');
719 define("ANSWER_ALTERNATIVE_REGEX",
720 '(' . ANSWER_ALTERNATIVE_FRACTION_REGEX .')?' .
721 '(' . ANSWER_ALTERNATIVE_ANSWER_REGEX . ')' .
722 '(#(' . ANSWER_ALTERNATIVE_FEEDBACK_REGEX .'))?');
724 // Parenthesis positions for ANSWER_ALTERNATIVE_REGEX
725 define("ANSWER_ALTERNATIVE_REGEX_PERCENTILE_FRACTION", 2);
726 define("ANSWER_ALTERNATIVE_REGEX_FRACTION", 1);
727 define("ANSWER_ALTERNATIVE_REGEX_ANSWER", 3);
728 define("ANSWER_ALTERNATIVE_REGEX_FEEDBACK", 5);
730 // NUMBER_FORMATED_ALTERNATIVE_ANSWER_REGEX is used
731 // for identifying numerical answers in ANSWER_ALTERNATIVE_REGEX_ANSWER
732 define("NUMBER_REGEX",
733 '-?(([0-9]+[.,]?[0-9]*|[.,][0-9]+)([eE][-+]?[0-9]+)?)');
734 define("NUMERICAL_ALTERNATIVE_REGEX",
735 '^(' . NUMBER_REGEX . ')(:' . NUMBER_REGEX . ')?$');
737 // Parenthesis positions for NUMERICAL_FORMATED_ALTERNATIVE_ANSWER_REGEX
738 define("NUMERICAL_CORRECT_ANSWER", 1);
739 define("NUMERICAL_ABS_ERROR_MARGIN", 6);
741 // Remaining ANSWER regexes
742 define("ANSWER_TYPE_DEF_REGEX",
743 '(NUMERICAL|NM)|(MULTICHOICE|MC)|(MULTICHOICE_V|MCV)|(MULTICHOICE_H|MCH)|(SHORTANSWER|SA|MW)|(SHORTANSWER_C|SAC|MWC)');
744 define("ANSWER_START_REGEX",
745 '\{([0-9]*):(' . ANSWER_TYPE_DEF_REGEX . '):');
747 define("ANSWER_REGEX",
749 . '(' . ANSWER_ALTERNATIVE_REGEX
751 . ANSWER_ALTERNATIVE_REGEX
754 // Parenthesis positions for singulars in ANSWER_REGEX
755 define("ANSWER_REGEX_NORM", 1);
756 define("ANSWER_REGEX_ANSWER_TYPE_NUMERICAL", 3);
757 define("ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE", 4);
758 define("ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_REGULAR", 5);
759 define("ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_HORIZONTAL", 6);
760 define("ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER", 7);
761 define("ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER_C", 8);
762 define("ANSWER_REGEX_ALTERNATIVES", 9);
764 function qtype_multianswer_extract_question($text) {
765 // $text is an array [text][format][itemid]
766 $question = new stdClass;
767 $question->qtype = 'multianswer';
768 $question->questiontext = $text;
769 $question->generalfeedback['text'] = '';
770 $question->generalfeedback['format'] = '1';
771 $question->generalfeedback['itemid'] = '';
773 $question->options->questions = array();
774 $question->defaultgrade = 0; // Will be increased for each answer norm
776 for ($positionkey=1; preg_match('/'.ANSWER_REGEX.'/', $question->questiontext['text'], $answerregs); ++$positionkey ) {
777 $wrapped = new stdClass;
778 $wrapped->generalfeedback['text'] = '';
779 $wrapped->generalfeedback['format'] = '1';
780 $wrapped->generalfeedback['itemid'] = '';
781 if (isset($answerregs[ANSWER_REGEX_NORM])&& $answerregs[ANSWER_REGEX_NORM]!== ''){
782 $wrapped->defaultgrade = $answerregs[ANSWER_REGEX_NORM];
784 $wrapped->defaultgrade = '1';
786 if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_NUMERICAL])) {
787 $wrapped->qtype = 'numerical';
788 $wrapped->multiplier = array();
789 $wrapped->units = array();
790 $wrapped->instructions['text'] = '';
791 $wrapped->instructions['format'] = '1';
792 $wrapped->instructions['itemid'] = '';
793 } else if(!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER])) {
794 $wrapped->qtype = 'shortanswer';
795 $wrapped->usecase = 0;
796 } else if(!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER_C])) {
797 $wrapped->qtype = 'shortanswer';
798 $wrapped->usecase = 1;
799 } else if(!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE])) {
800 $wrapped->qtype = 'multichoice';
801 $wrapped->single = 1;
802 $wrapped->answernumbering = 0;
803 $wrapped->correctfeedback['text'] = '';
804 $wrapped->correctfeedback['format'] = '1';
805 $wrapped->correctfeedback['itemid'] = '';
806 $wrapped->partiallycorrectfeedback['text'] = '';
807 $wrapped->partiallycorrectfeedback['format'] = '1';
808 $wrapped->partiallycorrectfeedback['itemid'] = '';
809 $wrapped->incorrectfeedback['text'] = '';
810 $wrapped->incorrectfeedback['format'] = '1';
811 $wrapped->incorrectfeedback['itemid'] = '';
812 $wrapped->layout = 0;
813 } else if(!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_REGULAR])) {
814 $wrapped->qtype = 'multichoice';
815 $wrapped->single = 1;
816 $wrapped->answernumbering = 0;
817 $wrapped->correctfeedback['text'] = '';
818 $wrapped->correctfeedback['format'] = '1';
819 $wrapped->correctfeedback['itemid'] = '';
820 $wrapped->partiallycorrectfeedback['text'] = '';
821 $wrapped->partiallycorrectfeedback['format'] = '1';
822 $wrapped->partiallycorrectfeedback['itemid'] = '';
823 $wrapped->incorrectfeedback['text'] = '';
824 $wrapped->incorrectfeedback['format'] = '1';
825 $wrapped->incorrectfeedback['itemid'] = '';
826 $wrapped->layout = 1;
827 } else if(!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_HORIZONTAL])) {
828 $wrapped->qtype = 'multichoice';
829 $wrapped->single = 1;
830 $wrapped->answernumbering = 0;
831 $wrapped->correctfeedback['text'] = '';
832 $wrapped->correctfeedback['format'] = '1';
833 $wrapped->correctfeedback['itemid'] = '';
834 $wrapped->partiallycorrectfeedback['text'] = '';
835 $wrapped->partiallycorrectfeedback['format'] = '1';
836 $wrapped->partiallycorrectfeedback['itemid'] = '';
837 $wrapped->incorrectfeedback['text'] = '';
838 $wrapped->incorrectfeedback['format'] = '1';
839 $wrapped->incorrectfeedback['itemid'] = '';
840 $wrapped->layout = 2;
842 print_error('unknownquestiontype', 'question', '', $answerregs[2]);
846 // Each $wrapped simulates a $form that can be processed by the
847 // respective save_question and save_question_options methods of the
848 // wrapped questiontypes
849 $wrapped->answer = array();
850 $wrapped->fraction = array();
851 $wrapped->feedback = array();
852 $wrapped->shuffleanswers = 1;
853 $wrapped->questiontext['text'] = $answerregs[0];
854 $wrapped->questiontext['format'] = 0 ;
855 $wrapped->questiontext['itemid'] = '' ;
858 $remainingalts = $answerregs[ANSWER_REGEX_ALTERNATIVES];
859 while (preg_match('/~?'.ANSWER_ALTERNATIVE_REGEX.'/', $remainingalts, $altregs)) {
860 if ('=' == $altregs[ANSWER_ALTERNATIVE_REGEX_FRACTION]) {
861 $wrapped->fraction["$answerindex"] = '1';
862 } else if ($percentile = $altregs[ANSWER_ALTERNATIVE_REGEX_PERCENTILE_FRACTION]){
863 $wrapped->fraction["$answerindex"] = .01 * $percentile;
865 $wrapped->fraction["$answerindex"] = '0';
867 if (isset($altregs[ANSWER_ALTERNATIVE_REGEX_FEEDBACK])) {
868 $feedback = html_entity_decode($altregs[ANSWER_ALTERNATIVE_REGEX_FEEDBACK], ENT_QUOTES, 'UTF-8');
869 $feedback = str_replace('\}', '}', $feedback);
870 $wrapped->feedback["$answerindex"]['text'] = str_replace('\#', '#', $feedback);
871 $wrapped->feedback["$answerindex"]['format'] = '1';
872 $wrapped->feedback["$answerindex"]['itemid'] = '';
874 $wrapped->feedback["$answerindex"]['text'] = '';
875 $wrapped->feedback["$answerindex"]['format'] = '1';
876 $wrapped->feedback["$answerindex"]['itemid'] = '1';
879 if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_NUMERICAL])
880 && preg_match('~'.NUMERICAL_ALTERNATIVE_REGEX.'~', $altregs[ANSWER_ALTERNATIVE_REGEX_ANSWER], $numregs)) {
881 $wrapped->answer[] = $numregs[NUMERICAL_CORRECT_ANSWER];
882 if ($numregs[NUMERICAL_ABS_ERROR_MARGIN]) {
883 $wrapped->tolerance["$answerindex"] =
884 $numregs[NUMERICAL_ABS_ERROR_MARGIN];
886 $wrapped->tolerance["$answerindex"] = 0;
888 } else { // Tolerance can stay undefined for non numerical questions
889 // Undo quoting done by the HTML editor.
890 $answer = html_entity_decode($altregs[ANSWER_ALTERNATIVE_REGEX_ANSWER], ENT_QUOTES, 'UTF-8');
891 $answer = str_replace('\}', '}', $answer);
892 $wrapped->answer["$answerindex"] = str_replace('\#', '#', $answer);
894 $tmp = explode($altregs[0], $remainingalts, 2);
895 $remainingalts = $tmp[1];
899 $question->defaultgrade += $wrapped->defaultgrade;
900 $question->options->questions[$positionkey] = clone($wrapped);
901 $question->questiontext['text'] = implode("{#$positionkey}",
902 explode($answerregs[0], $question->questiontext['text'], 2));
903 // echo"<p>questiontext 2 <pre>";print_r($question->questiontext);echo"<pre></p>";
905 // echo"<p>questiontext<pre>";print_r($question->questiontext);echo"<pre></p>";
906 $question->questiontext = $question->questiontext;
907 // echo"<p>question<pre>";print_r($question);echo"<pre></p>";